UNPKG

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
/** * @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, };