Files
WYF-koubo/scripts/build-oem-inno-installer.cjs
T
2026-06-19 18:45:55 +08:00

242 lines
7.9 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const root = path.join(__dirname, '..');
const packageJson = require(path.join(root, 'package.json'));
const buildDir = path.join(root, 'dist-release-final');
const unpackedDir = path.join(buildDir, 'win-unpacked');
const bundleDir = path.join(buildDir, 'resource-bundles');
const manifestPath = path.join(buildDir, 'resource-manifest.json');
const tempDir = path.join(root, 'build', 'oem-batch-temp');
function findIscc() {
const candidates = [
process.env.INNO_SETUP_ISCC,
'C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe',
'C:\\Program Files\\Inno Setup 6\\ISCC.exe',
'ISCC.exe',
].filter(Boolean);
for (const candidate of candidates) {
if (candidate.includes('\\') || candidate.includes('/')) {
if (fs.existsSync(candidate)) return candidate;
continue;
}
const result = spawnSync(candidate, ['/Qp'], {
encoding: 'utf8',
windowsHide: true,
});
if (result.status === 0 || result.status === 1) return candidate;
}
throw new Error('Inno Setup ISCC.exe not found.');
}
function getInnoInstallDir(isccPath) {
if (isccPath.includes('\\') || isccPath.includes('/')) {
return path.dirname(isccPath);
}
const candidates = [
'C:\\Program Files (x86)\\Inno Setup 6',
'C:\\Program Files\\Inno Setup 6',
];
return candidates.find(candidate => fs.existsSync(path.join(candidate, 'ISCC.exe'))) || '';
}
function getLanguagesSection(isccPath) {
const innoDir = getInnoInstallDir(isccPath);
const chineseLanguageFile = innoDir ? path.join(innoDir, 'Languages', 'ChineseSimplified.isl') : '';
if (chineseLanguageFile && fs.existsSync(chineseLanguageFile)) {
return '[Languages]\nName: "chinesesimp"; MessagesFile: "compiler:Languages\\\\ChineseSimplified.isl"';
}
return '';
}
function escapeInnoString(value) {
return String(value).replace(/"/g, '""');
}
function readJson(file, fallback) {
if (!fs.existsSync(file)) return fallback;
try {
return JSON.parse(fs.readFileSync(file, 'utf8'));
} catch {
return fallback;
}
}
function findAppExe() {
if (!fs.existsSync(unpackedDir)) {
throw new Error(`win-unpacked not found: ${unpackedDir}`);
}
const exeFiles = fs.readdirSync(unpackedDir)
.filter(name => name.toLowerCase().endsWith('.exe'))
.filter(name => !/^unins/i.test(name));
if (exeFiles.length === 0) {
throw new Error(`No app exe found in ${unpackedDir}`);
}
exeFiles.sort((a, b) => {
const aStat = fs.statSync(path.join(unpackedDir, a));
const bStat = fs.statSync(path.join(unpackedDir, b));
return bStat.size - aStat.size;
});
return exeFiles[0];
}
function getCurrentResourceArchives() {
const manifest = readJson(manifestPath, { bundles: {} });
const archives = Object.values(manifest.bundles || {})
.map(bundle => bundle && bundle.archive)
.filter(Boolean);
const archivePaths = Array.from(new Set(archives))
.map(archive => path.join(bundleDir, archive))
.sort();
const missing = archivePaths.filter(archivePath => !fs.existsSync(archivePath));
if (archivePaths.length === 0) {
throw new Error(`No resource archives found in manifest: ${manifestPath}`);
}
if (missing.length > 0) {
throw new Error(`Missing resource archives:\n${missing.join('\n')}`);
}
return archivePaths;
}
function getCurrentResourceArchiveEntries() {
const manifest = readJson(manifestPath, { bundles: {} });
return Object.entries(manifest.bundles || {})
.map(([name, bundle]) => ({
name,
archive: bundle && bundle.archive,
archivePath: bundle && bundle.archive ? path.join(bundleDir, bundle.archive) : '',
}))
.filter(entry => entry.archive && fs.existsSync(entry.archivePath))
.sort((a, b) => a.name.localeCompare(b.name));
}
function ensureOfflineBundlesHydrated() {
const scriptPath = path.join(__dirname, 'hydrate-oem-portable-bundles.cjs');
const result = spawnSync(process.execPath, [scriptPath], {
cwd: root,
encoding: 'utf8',
stdio: 'inherit',
windowsHide: true,
});
if (result.status !== 0) {
throw new Error(`hydrate-oem-portable-bundles failed with exit code ${result.status}`);
}
}
function dirSizeBytes(dir) {
let total = 0;
const walk = current => {
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
walk(full);
} else if (entry.isFile()) {
total += fs.statSync(full).size;
}
}
};
walk(dir);
return total;
}
function main() {
const appExe = findAppExe();
ensureOfflineBundlesHydrated();
const resourceArchives = getCurrentResourceArchives();
const appName = path.basename(appExe, '.exe');
const version = process.env.OEM_VERSION || process.env.APP_VERSION || packageJson.version || '1.0.0';
const outputBaseName = `${appName}-${version}-win-setup-x64`;
const issPath = path.join(tempDir, 'oem-full-offline-installer.iss');
const iscc = findIscc();
const languagesSection = getLanguagesSection(iscc);
fs.mkdirSync(tempDir, { recursive: true });
fs.mkdirSync(buildDir, { recursive: true });
const previousOutput = path.join(buildDir, `${outputBaseName}.exe`);
if (fs.existsSync(previousOutput)) {
fs.rmSync(previousOutput, { force: true });
}
const iss = `
[Setup]
AppId={{68912956-3e71-5e51-8eba-ab7cdc795c8c}
AppName=${escapeInnoString(appName)}
AppVersion=${escapeInnoString(version)}
AppPublisher=ModStartLib
DefaultDirName={autopf}\\${escapeInnoString(appName)}
DefaultGroupName=${escapeInnoString(appName)}
DisableDirPage=no
DisableProgramGroupPage=yes
UsePreviousAppDir=no
OutputDir=${escapeInnoString(buildDir)}
OutputBaseFilename=${escapeInnoString(outputBaseName)}
SetupIconFile=${escapeInnoString(path.join(root, 'electron', 'resources', 'build', 'logo.ico'))}
UninstallDisplayIcon={app}\\${escapeInnoString(appExe)}
Compression=lzma2/max
SolidCompression=no
LZMAUseSeparateProcess=yes
LZMADictionarySize=65536
DiskSpanning=no
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
WizardStyle=modern
PrivilegesRequired=lowest
CloseApplications=yes
RestartApplications=no
${languagesSection}
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Files]
Source: "${escapeInnoString(path.join(unpackedDir, '*'))}"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "${escapeInnoString(manifestPath)}"; DestDir: "{app}\\resources"; Flags: ignoreversion
[Icons]
Name: "{group}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"
Name: "{autodesktop}\\${escapeInnoString(appName)}"; Filename: "{app}\\${escapeInnoString(appExe)}"; WorkingDir: "{app}"; Tasks: desktopicon
[Run]
Filename: "{app}\\${escapeInnoString(appExe)}"; Description: "{cm:LaunchProgram,${escapeInnoString(appName)}}"; Flags: nowait postinstall skipifsilent
`.trimStart();
fs.writeFileSync(issPath, iss, 'utf8');
const unpackedGb = dirSizeBytes(unpackedDir) / 1024 / 1024 / 1024;
const archivesMb = resourceArchives.reduce((total, archivePath) => total + fs.statSync(archivePath).size, 0) / 1024 / 1024;
console.log(`[inno] source: ${unpackedDir} (${unpackedGb.toFixed(2)} GB)`);
console.log(`[inno] validated resource archives: ${resourceArchives.length} (${archivesMb.toFixed(2)} MB)`);
console.log('[inno] resource payload mode: full offline resources copied from win-unpacked');
console.log(`[inno] output: ${previousOutput}`);
const result = spawnSync(iscc, [issPath], {
cwd: root,
encoding: 'utf8',
stdio: 'inherit',
windowsHide: true,
});
if (result.status !== 0) {
throw new Error(`ISCC failed with exit code ${result.status}`);
}
const stat = fs.statSync(previousOutput);
console.log(`[inno] built: ${previousOutput} (${(stat.size / 1024 / 1024).toFixed(2)} MB)`);
}
main();