feat(desktop): packaging (C2) : prepare-server, fetch-node, electron-builder
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / No em/en dashes (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

scripts/prepare-server.mjs : rebuild forcé shared+server (évite la non-idempotence du prepack), npm pack du daemon (tarball autonome), extraction, strip des devDependencies (dont @arboretum/shared inliné, non publié), npm install --omit=dev des deps runtime avec le bon prebuild node-pty (cross via npm_config_platform/arch). scripts/fetch-node.mjs : Node standalone épinglé (22.21.1) vérifié SHA256 -> build/node.

electron-builder.yml : dist/ en ASAR ; build/server et build/node en extraResources (hors ASAR pour les .node) ; cibles Linux (AppImage+deb), Windows (nsis+portable), macOS (dmg/zip non signé) ; publish generic -> release Gitea. Scripts dist:linux/win/mac.

Vérifié : fetch-node (Node bundlé + node:sqlite), prepare-server (structure complète), et la chaîne runtime complète (Node bundlé exécute le daemon empaqueté : handshake + session bash via node-pty OK). Le wrapping electron-builder est une étape machine.
This commit is contained in:
2026-07-17 17:59:32 +02:00
parent ae7ede6684
commit 82682b0a37
5 changed files with 4576 additions and 4 deletions

View File

@@ -0,0 +1,59 @@
appId: fr.lidge.arboretum
productName: Arboretum
copyright: Copyright Johan Leroy
directories:
output: release
buildResources: resources
# Le bundle esbuild (dist/) va dans l'ASAR ; le daemon et le runtime Node restent des fichiers
# reels sur disque (extraResources, hors ASAR) : les binaires natifs (.node de node-pty) se
# chargent proprement, ce qu'un .asar interdirait.
files:
- dist/**/*
- "!**/*.map"
asar: true
extraResources:
- from: build/server
to: server
- from: build/node
to: node
linux:
target: [AppImage, deb]
category: Development
icon: resources/icons
artifactName: ${productName}-${version}-${arch}.${ext}
deb:
# git est requis pour les operations de worktree ; claude n'est pas dans les depots (documente).
depends: [git]
win:
target:
- target: nsis
arch: [x64]
- target: portable
arch: [x64]
icon: resources/icons/icon.ico
artifactName: ${productName}-${version}-${arch}.${ext}
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
mac:
target: [dmg, zip]
category: public.app-category.developer-tools
icon: resources/icons/icon.icns
# macOS best-effort : non signe (documente : clic droit -> Ouvrir, ou xattr -dr com.apple.quarantine)
identity: null
hardenedRuntime: false
# Auto-update (electron-updater, cable en C5) : provider generic pointant sur les assets de release
# Gitea. Genere latest*.yml a cote des artefacts.
publish:
provider: generic
url: https://git.lidge.fr/johanleroy/arboretum/releases/download/desktop-latest

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,17 @@
"scripts": {
"typecheck": "tsc -p . --noEmit",
"build": "npm run typecheck && node esbuild.mjs",
"dev": "node esbuild.mjs && electron ."
"dev": "node esbuild.mjs && electron .",
"prepare:server": "node scripts/prepare-server.mjs",
"prepare:node": "node scripts/fetch-node.mjs",
"dist:linux": "npm run build && npm run prepare:server -- --platform=linux && npm run prepare:node -- --platform=linux && electron-builder --linux",
"dist:win": "npm run build && npm run prepare:server -- --platform=win32 && npm run prepare:node -- --platform=win32 && electron-builder --win",
"dist:mac": "npm run build && npm run prepare:server -- --platform=darwin && npm run prepare:node -- --platform=darwin && electron-builder --mac"
},
"devDependencies": {
"@types/node": "^22.10.0",
"electron": "^33.0.0",
"electron-builder": "^25.1.0",
"esbuild": "^0.21.0",
"typescript": "^5.7.0"
}

View File

@@ -0,0 +1,49 @@
// Télécharge un runtime Node standalone (>= 22.16, épinglé) dans build/node, avec vérification
// SHA256. Le daemon tourne SUR ce Node (pas celui d'Electron) pour garantir node:sqlite sans flag
// et l'ABI node-pty attendue (prefixe `node.`). Options : --platform / --arch (défaut : hôte).
import { execFileSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const NODE_VERSION = '22.21.1';
const HERE = dirname(fileURLToPath(import.meta.url));
const BUILD = join(HERE, '..', 'build');
const NODE_DIR = join(BUILD, 'node');
const args = process.argv.slice(2);
const arg = (name, def) => args.find((a) => a.startsWith(`--${name}=`))?.split('=')[1] ?? def;
const platform = arg('platform', process.platform);
const arch = arg('arch', process.arch);
const OS = { linux: 'linux', darwin: 'darwin', win32: 'win' }[platform];
if (!OS) throw new Error(`plateforme non supportee: ${platform}`);
const ext = platform === 'win32' ? 'zip' : 'tar.xz';
const name = `node-v${NODE_VERSION}-${OS}-${arch}`;
const base = `https://nodejs.org/dist/v${NODE_VERSION}`;
async function get(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`GET ${url} -> ${res.status}`);
return res;
}
rmSync(NODE_DIR, { recursive: true, force: true });
mkdirSync(NODE_DIR, { recursive: true });
const tarball = Buffer.from(await (await get(`${base}/${name}.${ext}`)).arrayBuffer());
const shasums = await (await get(`${base}/SHASUMS256.txt`)).text();
const expected = shasums.split('\n').find((l) => l.endsWith(`${name}.${ext}`))?.split(/\s+/)[0];
const actual = createHash('sha256').update(tarball).digest('hex');
if (!expected) throw new Error(`SHA introuvable pour ${name}.${ext}`);
if (expected !== actual) throw new Error(`SHA256 mismatch pour ${name}.${ext}`);
const archive = join(BUILD, `${name}.${ext}`);
writeFileSync(archive, tarball);
if (ext === 'zip') execFileSync('unzip', ['-q', archive, '-d', BUILD], { stdio: 'inherit' });
else execFileSync('tar', ['-xJf', archive, '-C', BUILD], { stdio: 'inherit' });
// aplatir node-vX-os-arch/ -> build/node/
execFileSync('bash', ['-c', `cp -R "${join(BUILD, name)}/." "${NODE_DIR}/" && rm -rf "${join(BUILD, name)}" "${archive}"`], { stdio: 'inherit' });
console.log(`fetch-node: Node ${NODE_VERSION} (${OS}-${arch}) -> build/node`);

View File

@@ -0,0 +1,56 @@
// Prépare le daemon pour l'empaquetage Electron : réutilise INTÉGRALEMENT le pipeline npm du paquet
// publié (hooks prepack copy-web / inline-shared / copy-meta), donc aucune divergence de code.
// 1) build (shared + server + web) puis `npm pack` -> tarball 100 % autonome
// 2) extraction dans build/server/package
// 3) `npm install --omit=dev` -> deps runtime (node-pty avec le prebuild de la plateforme cible)
// Options : --platform=win32|darwin|linux et --arch=x64|arm64 pour un prebuild node-pty croisé
// (via npm_config_platform / npm_config_arch au moment du npm install).
import { execFileSync } from 'node:child_process';
import { mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const DESKTOP = join(HERE, '..');
const REPO = join(DESKTOP, '..', '..');
const SERVER_DIR = join(DESKTOP, 'build', 'server');
const args = process.argv.slice(2);
const arg = (name) => args.find((a) => a.startsWith(`--${name}=`))?.split('=')[1];
const platform = arg('platform');
const arch = arg('arch');
const run = (cmd, cmdArgs, cwd, env) =>
execFileSync(cmd, cmdArgs, { cwd, stdio: 'inherit', env: { ...process.env, ...env } });
rmSync(SERVER_DIR, { recursive: true, force: true });
mkdirSync(SERVER_DIR, { recursive: true });
// 1) build + pack (le tarball embarque dist + _shared inliné + public via prepack).
// Rebuild FORCÉ de shared+server : le prepack (inline-shared) mute le dist du serveur en place
// (imports réécrits vers ./_shared) ; un tsc -b incrémental ne le régénère pas au run suivant,
// d'où un pack qui échoue. --force garantit un dist propre à imports bare avant chaque pack.
run('npx', ['tsc', '-b', '--force', 'packages/shared', 'packages/server'], REPO);
run('npm', ['run', 'build', '-w', '@arboretum/web'], REPO);
run('npm', ['pack', '-w', '@johanleroy/git-arboretum', '--pack-destination', SERVER_DIR], REPO);
// 2) extraire le tarball -> build/server/package
const tgz = readdirSync(SERVER_DIR).find((f) => f.endsWith('.tgz'));
if (!tgz) throw new Error('tarball introuvable apres npm pack');
run('tar', ['-xzf', join(SERVER_DIR, tgz), '-C', SERVER_DIR]);
rmSync(join(SERVER_DIR, tgz));
// 3) deps runtime (node-pty prebuild). Cross-compile via npm_config_platform/arch si demandé.
const pkgDir = join(SERVER_DIR, 'package');
// Retirer les devDependencies du paquet extrait : inutiles au runtime, et @arboretum/shared
// (workspace non publié, déjà inliné dans dist/_shared) n'existe sur aucun registre -> 404.
const pkgJsonPath = join(pkgDir, 'package.json');
const pj = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
delete pj.devDependencies;
writeFileSync(pkgJsonPath, `${JSON.stringify(pj, null, 2)}\n`);
const env = {};
if (platform) env.npm_config_platform = platform;
if (arch) env.npm_config_arch = arch;
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], pkgDir, env);
console.log(`prepare-server: build/server/package pret${platform ? ` (${platform}-${arch ?? 'host'})` : ''}`);