import { spawn } from 'child_process' import { join, basename } from 'path' import { existsSync } from 'fs' import { readdir, mkdir, rm, writeFile, readFile } from 'fs/promises' import { paths } from './paths' import { downloadFile } from './download' import { emit } from './events' /** * Fournit un binaire Java 21+ (requis par ATM10 / NeoForge 1.21.1). * * Stratégie, du moins coûteux au plus coûteux : * 1. Java déjà mémorisé dans java/managed.json (provisionné ou détecté avant). * 2. Java >= 21 déjà présent sur la machine (JAVA_HOME, puis PATH). * 3. Sinon : téléchargement d'un JRE Temurin (Adoptium) — archive autonome, * extraite avec l'outil système (`tar` sur Linux/mac, `Expand-Archive` * PowerShell sur Windows). * * Le chemin retenu est mémorisé pour les lancements suivants. */ const JAVA_MAJOR = 21 const JAVA_MAX = 21 const MARKER = (): string => join(paths.javaDir, 'managed.json') /** Exécute ` -version` et renvoie la version majeure (ex. 21), ou null. */ async function javaMajorVersion(javaPath: string): Promise { return new Promise((resolve) => { let out = '' const child = spawn(javaPath, ['-version']) child.stdout?.on('data', (d: Buffer) => (out += d.toString())) child.stderr?.on('data', (d: Buffer) => (out += d.toString())) child.on('error', () => resolve(null)) child.on('close', () => { // ex: 'openjdk version "21.0.2"' ou '... "1.8.0_xyz"' const m = out.match(/version "(\d+)(?:\.(\d+))?/) if (!m) return resolve(null) let major = parseInt(m[1], 10) if (major === 1 && m[2]) major = parseInt(m[2], 10) // 1.8 -> 8 resolve(Number.isNaN(major) ? null : major) }) }) } /** Résout un exécutable via `where` (Windows) / `which` (unix). */ async function resolveOnPath(bin: string): Promise { const cmd = process.platform === 'win32' ? 'where' : 'which' return new Promise((resolve) => { let out = '' const child = spawn(cmd, [bin]) child.stdout?.on('data', (d: Buffer) => (out += d.toString())) child.on('error', () => resolve(null)) child.on('close', (code) => { if (code !== 0) return resolve(null) const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean) resolve(first ?? null) }) }) } /** * Cherche un Java >= JAVA_MAJOR déjà installé sur la machine. * Retourne le chemin du binaire ou null. */ async function findSystemJava(): Promise { const javaBin = detectPlatform().javaBin const candidates: string[] = [] if (process.env.JAVA_HOME) { candidates.push(join(process.env.JAVA_HOME, 'bin', javaBin)) } const onPath = await resolveOnPath(javaBin) if (onPath) candidates.push(onPath) for (const c of candidates) { if (!existsSync(c)) continue const major = await javaMajorVersion(c) if (major !== null && major >= JAVA_MAJOR && major <= JAVA_MAX) return c } return null } interface Platform { os: 'windows' | 'linux' | 'mac' arch: 'x64' | 'aarch64' archiveExt: 'zip' | 'tar.gz' javaBin: string } function detectPlatform(): Platform { const os = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'mac' : 'linux' const arch = process.arch === 'arm64' ? 'aarch64' : 'x64' return { os, arch, archiveExt: os === 'windows' ? 'zip' : 'tar.gz', javaBin: os === 'windows' ? 'java.exe' : 'java' } } /** Cherche récursivement bin/ sous un dossier extrait. */ async function findJavaBinary(root: string, javaBin: string): Promise { const stack = [root] while (stack.length) { const dir = stack.pop()! const entries = await readdir(dir, { withFileTypes: true }).catch(() => []) for (const e of entries) { const full = join(dir, e.name) if (e.isDirectory()) { stack.push(full) } else if (e.name === javaBin && basename(dir) === 'bin') { return full } } } return null } async function extractArchive(archive: string, destDir: string, ext: string): Promise { await mkdir(destDir, { recursive: true }) await new Promise((resolve, reject) => { const child = ext === 'zip' ? spawn( 'powershell', [ '-NoProfile', '-Command', `Expand-Archive -Path "${archive}" -DestinationPath "${destDir}" -Force` ], { stdio: 'ignore' } ) : spawn('tar', ['-xzf', archive, '-C', destDir], { stdio: 'ignore' }) child.on('error', reject) child.on('close', (code) => code === 0 ? resolve() : reject(new Error(`Extraction échouée (code ${code})`)) ) }) } /** * Retourne le chemin du binaire java géré, en l'installant si nécessaire. * Idempotent : si déjà installé, retourne immédiatement le chemin mémorisé. */ export async function ensureJava(): Promise { // 1) Déjà mémorisé (provisionné OU détecté lors d'un lancement précédent) ? if (existsSync(MARKER())) { try { const { javaPath } = JSON.parse(await readFile(MARKER(), 'utf-8')) as { javaPath: string } if (javaPath && existsSync(javaPath)) { const major = await javaMajorVersion(javaPath) if (major !== null && major >= JAVA_MAJOR && major <= JAVA_MAX) return javaPath // Version hors plage (ex: Java 22+) : invalider le cache et re-détecter await rm(MARKER(), { force: true }) } } catch { /* marqueur corrompu : on réinstalle */ } } // 2) Java >= 21 déjà présent sur la machine ? On l'utilise tel quel. emit.progress({ phase: 'java', message: 'Recherche de Java sur la machine…', progress: undefined }) const system = await findSystemJava() if (system) { await rememberJava(system, 'system') emit.progress({ phase: 'java', message: 'Java 21 détecté sur la machine.', progress: 1 }) return system } // 3) Sinon : on télécharge un JRE Temurin 21. const plat = detectPlatform() emit.progress({ phase: 'java', message: `Téléchargement de Java ${JAVA_MAJOR} (Temurin)…`, progress: 0 }) const url = `https://api.adoptium.net/v3/binary/latest/${JAVA_MAJOR}/ga/` + `${plat.os}/${plat.arch}/jre/hotspot/normal/eclipse` const installRoot = join(paths.javaDir, `temurin-${JAVA_MAJOR}`) await rm(installRoot, { recursive: true, force: true }) const archive = join(paths.javaDir, `temurin-${JAVA_MAJOR}.${plat.archiveExt}`) await downloadFile(url, archive, (f) => emit.progress({ phase: 'java', message: `Téléchargement de Java ${JAVA_MAJOR}…`, progress: f }) ) emit.progress({ phase: 'java', message: 'Extraction de Java…', progress: undefined }) await extractArchive(archive, installRoot, plat.archiveExt) await rm(archive, { force: true }) const javaPath = await findJavaBinary(installRoot, plat.javaBin) if (!javaPath) { const listing = await readdir(installRoot).catch(() => [] as string[]) throw new Error( `Binaire java introuvable après extraction. Contenu de ${installRoot} : ` + (listing.length ? listing.join(', ') : '(vide)') ) } // Vérifie que le java extrait fonctionne et est bien >= 21. const major = await javaMajorVersion(javaPath) if (major === null || major < JAVA_MAJOR) { throw new Error(`Java extrait inutilisable (version détectée : ${major ?? 'inconnue'}).`) } await rememberJava(javaPath, 'temurin') emit.progress({ phase: 'java', message: 'Java prêt.', progress: 1 }) return javaPath } /** Mémorise le chemin du java retenu pour les prochains lancements. */ async function rememberJava(javaPath: string, source: 'system' | 'temurin'): Promise { await mkdir(paths.javaDir, { recursive: true }) await writeFile(MARKER(), JSON.stringify({ javaPath, source, min: JAVA_MAJOR }, null, 2)) }