216 lines
7.7 KiB
TypeScript
216 lines
7.7 KiB
TypeScript
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 `<javaPath> -version` et renvoie la version majeure (ex. 21), ou null. */
|
|
async function javaMajorVersion(javaPath: string): Promise<number | null> {
|
|
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<string | null> {
|
|
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<string | null> {
|
|
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/<javaBin> sous un dossier extrait. */
|
|
async function findJavaBinary(root: string, javaBin: string): Promise<string | null> {
|
|
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<void> {
|
|
await mkdir(destDir, { recursive: true })
|
|
await new Promise<void>((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<string> {
|
|
// 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<void> {
|
|
await mkdir(paths.javaDir, { recursive: true })
|
|
await writeFile(MARKER(), JSON.stringify({ javaPath, source, min: JAVA_MAJOR }, null, 2))
|
|
}
|