first commit
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
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 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) 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)) return javaPath
|
||||
} 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))
|
||||
}
|
||||
Reference in New Issue
Block a user