first commit

This commit is contained in:
lucasdpt
2026-06-14 12:32:29 +02:00
commit cc4f90e840
32 changed files with 9729 additions and 0 deletions
+209
View File
@@ -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))
}