Initial clean project import
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user