temurin-jdk-downloader
Version:
Utilities for downloading, extracting, and finding Eclipse Adoptium (Temurin) OpenJDK binary releases for Windows.
341 lines (303 loc) • 12.8 kB
JavaScript
/**
* @fileoverview
* Utilities for downloading, extracting, and identifying Eclipse Adoptium OpenJDK binary releases (Temurin).
* Includes robust file download, ZIP extraction, and auto-detection of Java executable paths.
*
* @customtypedef {Object} DownloadFileOptions
* @property {number} [timeout=120000] - Timeout in milliseconds before the request is aborted (default: 2 minutes).
* @property {(percent:number, downloadedBytes:number, totalBytes:number) => void} [onProgress] - Optional callback for reporting download progress.
*
* @customtypedef {Object} TemurinJDKOptions
* @property {number} [version=25] - Temurin (OpenJDK) major version.
* @property {string} targetExtractDir - Directory to extract the downloaded JDK archive into.
*/
const http = require('http');
const https = require('https');
const fs = require('fs');
const AdmZip = require('adm-zip');
const path = require('path');
const os = require('os');
const cheerio = require('cheerio');
const axios = require('axios');
const {pipeline} = require('stream');
const {spawnSync} = require('child_process');
const TEMURIN_BINARIES = {
8: "temurin8-binaries",
11: "temurin11-binaries",
16: "temurin16-binaries",
17: "temurin17-binaries",
18: "temurin18-binaries",
19: "temurin19-binaries",
20: "temurin20-binaries",
21: "temurin21-binaries",
22: "temurin22-binaries",
23: "temurin23-binaries",
24: "temurin24-binaries",
25: "temurin25-binaries"
};
/**
* Downloads a file from a given URL to a specified local file path.
*
* Supports:
* - Large files (streamed with minimal memory), HTTP/HTTPS protocols
* - Up to 5 redirects
* - Timeout (default 2 minutes)
* - Partial file cleanup on errors
* - Optional progress callback
*
* @param {string} url - The URL to download from.
* @param {string} destPath - The destination file path for the downloaded file.
* @param {DownloadFileOptions} [opts] - Optional download options.
* @returns {Promise<void>}
* @throws {Error} On download error, non-200 response, too many redirects, or timeout.
*
* @example
* await downloadFile(
* 'https://example.com/file.zip',
* '/tmp/file.zip',
* {timeout: 60000, onProgress: (p,b,t) => console.log(p)}
* );
*/
async function downloadFile(url, destPath, opts = {}) {
let redirects = 0, maxRedirects = 5;
let done = false;
let lastProgress = 0;
const timeout = opts.timeout || 120_000; // 2min default
async function doDownload(url) {
return new Promise((resolve, reject) => {
const requester = url.startsWith('https:') ? https : http;
const req = requester.get(url, (res) => {
// Handle redirects
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
res.resume(); // drain
if (++redirects > maxRedirects)
return reject(new Error('Too many redirects'));
return resolve(doDownload(res.headers.location));
}
if (res.statusCode !== 200) {
res.resume();
return reject(new Error(`Failed to download: HTTP ${res.statusCode}`));
}
const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
let downloadedBytes = 0;
// Open file stream for writing (flags 'w' means always overwrite).
const file = fs.createWriteStream(destPath, {flags: 'w'});
// Progress reporting
if (typeof opts.onProgress === 'function' && totalBytes) {
res.on('data', chunk => {
downloadedBytes += chunk.length;
const percent = Math.floor((downloadedBytes / totalBytes) * 100);
// Only emit on each percentage step
if (percent !== lastProgress) {
lastProgress = percent;
opts.onProgress(percent, downloadedBytes, totalBytes);
}
});
}
// Pipe
pipeline(res, file, err => {
if (err) {
// (possibly partial file present)
fs.unlink(destPath, () => {
});
if (!done) {
done = true;
reject(err);
}
} else {
if (!done) {
done = true;
resolve();
}
}
});
});
req.setTimeout(timeout, () => {
req.abort();
fs.unlink(destPath, () => {
});
if (!done) {
done = true;
reject(new Error("Request timed out"));
}
});
req.on('error', err => {
fs.unlink(destPath, () => {
});
if (!done) {
done = true;
reject(err);
}
});
});
}
await doDownload(url);
}
/**
* Extracts all contents of a ZIP archive to a given destination directory.
*
* @param {string} zipPath - The file path to the ZIP archive.
* @param {string} extractTo - The destination directory for extracted files. Will be created if non-existent.
* @returns {Promise<void>}
* @throws {Error} If extraction fails or the ZIP file is invalid/corrupt.
*
* @example
* await extractZip('archive.zip', '/tmp/unpacked');
*/
function extractZip(zipPath, extractTo) {
return new Promise((resolve, reject) => {
try {
const zip = new AdmZip(zipPath);
zip.extractAllTo(extractTo, true);
resolve();
} catch (err) {
reject(err);
}
});
}
/**
* Downloads and installs a specified version of Eclipse Adoptium Temurin (OpenJDK) for Windows.
*
* This function automatically:
* - Locates the latest available Windows .zip release for the desired Temurin (OpenJDK) version,
* - Downloads the archive,
* - Extracts it into the chosen target directory,
* - Searches for the `java.exe` binary,
* - Prints installation and detected version details,
* - Deletes the downloaded archive after extraction.
*
* @param {Object} options - Configuration for downloading and extracting the JDK.
* @param {number} [options.version=25] - The desired Temurin (OpenJDK) major version (e.g., 17, 21, 25).
* @param {string} options.targetExtractDir - The path to the directory where the JDK will be extracted.
* @returns {Promise<{version: string|null, javaPath: string, binPath: string}>} Resolves with information about the installed JDK, including its detected version, java.exe path, and bin directory.
* @throws {Error} If the version is not supported, download or extraction fails, or the java.exe binary cannot be found.
*
* @example
* await downloadTemurinJDK({ version: 21, targetExtractDir: 'C:/jdk-installs' });
*/
async function downloadTemurinJDK({version = 25, targetExtractDir}) {
if (!TEMURIN_BINARIES[version]) {
const supportedVersions = Object.keys(TEMURIN_BINARIES).join(', ');
throw new Error(`Unknown Temurin version: ${version}. Supported versions: ${supportedVersions}`);
}
const temurinBinary = TEMURIN_BINARIES[version];
targetExtractDir = path.join(targetExtractDir, temurinBinary);
console.log(`Starting download of Eclipse Adoptium JDK ${version}...`);
const url = await getTemurinBinaryURL(temurinBinary);
if (!url) {
throw new Error(`Could not locate download URL for Temurin ${version}`);
}
console.log("Resolved download URL:", url);
const ZIP_PATH = path.join(__dirname, `${temurinBinary}.zip`);
await downloadFile(url, ZIP_PATH, {
onProgress: (percent) => {
process.stdout.write(`\rDownloading: ${percent}%`);
}
});
console.log("\nDownload complete. Extracting...");
if (!fs.existsSync(targetExtractDir)) {
fs.mkdirSync(targetExtractDir, {recursive: true});
}
await extractZip(ZIP_PATH, targetExtractDir);
console.log("Extraction complete!");
// Show user info about install location
console.log("JDK installed at:", targetExtractDir);
// Try to find the bin path (for JAVA_HOME)
const subfolders = fs.readdirSync(targetExtractDir);
const openjdkFolder = subfolders.find(f => /^jdk|^openjdk/i.test(f));
const binPath = openjdkFolder
? path.join(targetExtractDir, openjdkFolder, 'bin')
: path.join(targetExtractDir, 'bin');
console.log(`Java binaries located in: ${binPath}`);
fs.unlinkSync(ZIP_PATH);
const JAVA_PATH = findJavaExe(targetExtractDir);
if (!JAVA_PATH) {
throw new Error('Could not find java.exe after extraction!');
}
const result = spawnSync(JAVA_PATH, ['-version'], {encoding: 'utf8'});
if (result.error) {
throw new Error(`Error running Java: ${result.error}`);
}
const versionOutput = result.stderr.trim();
const versionLine = versionOutput.split('\n')[0];
const match = versionLine.match(/version "?([\d.]+)"?/);
if (match) {
console.log('\nDetected Java version:', match[1]);
} else {
console.log(versionOutput);
console.log('\nUnable to determine Java version.');
}
return {
version: match ? match[1] : null,
javaPath: JAVA_PATH,
binPath: binPath,
}
}
/**
* Resolves the download URL of the latest Windows .zip build of a given Temurin release stream
* by scraping the Adoptium/Temurin GitHub releases page.
* Prefers builds for Windows x64 with Hotspot and not test/static-libs.
*
* @param {string} temurinBinary - e.g. "temurin17-binaries"
* @returns {Promise<string|undefined>} Resolved direct download URL, or undefined if none found.
* @throws {Error} On HTTP/network errors during release lookup.
*
* @example
* const url = await getTemurinBinaryURL('temurin21-binaries');
*/
async function getTemurinBinaryURL(temurinBinary) {
let response = await axios.get(`https://github.com/adoptium/${temurinBinary}/releases`)
let $ = cheerio.load(response.data);
const releaseIds = []
$('section h2').each(function (index, el) {
const releaseId = $(el).text()
if (!releaseIds.includes(releaseId)) {
releaseIds.push(releaseId)
}
})
for (const releaseId of releaseIds) {
response = await axios.get(`https://github.com/adoptium/${temurinBinary}/releases/expanded_assets/${releaseId}`)
$ = cheerio.load(response.data);
let dlURL = null
$('a').each(function (index, element) {
const el = $(element)
const link = el.attr('href')
const name = el.text().trim()
if (name && name.toLowerCase().includes('windows') && name.endsWith('.zip') && !name.includes('testimage') && !name.includes('static-libs') && name.includes('hotspot')) {
dlURL = link
}
})
if (dlURL) {
return `https://github.com${dlURL}`
}
}
}
/**
* Recursively searches a base directory for a Windows Java executable ("java.exe") inside a bin/ subfolder.
* - Checks both direct bin/ and first-level child subfolders' bin/ subdirs.
*
* @param {string} baseDir - Path to the extracted JDK root directory.
* @returns {string|null} Absolute path to java.exe if found, otherwise null.
*
* @example
* const javaExe = findJavaExe('C:/jdk-installs/temurin21-binaries');
*/
function findJavaExe(baseDir) {
let directPath = path.join(baseDir, 'bin', 'java.exe');
if (fs.existsSync(directPath)) return directPath;
const subdirs = fs.readdirSync(baseDir, {withFileTypes: true})
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
for (const subdir of subdirs) {
const candidate = path.join(baseDir, subdir, 'bin', 'java.exe');
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
module.exports = {
downloadTemurinJDK,
downloadFile,
extractZip,
findJavaExe,
getTemurinBinaryURL,
};