242 lines
7.9 KiB
JavaScript
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();
|