mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-20 00:11:37 +00:00
436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
import { execSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const packageRoot = path.resolve(__dirname, '..');
|
|
const workspaceRoot = path.resolve(packageRoot, '..');
|
|
const gitnexusRoot = path.join(workspaceRoot, 'gitnexus');
|
|
const gitnexusWebRoot = path.join(workspaceRoot, 'gitnexus-web');
|
|
const releaseRoot = path.join(packageRoot, 'release');
|
|
const gitnexusPackageJson = JSON.parse(
|
|
fs.readFileSync(path.join(gitnexusRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const gitnexusPackageLock = JSON.parse(
|
|
fs.readFileSync(path.join(gitnexusRoot, 'package-lock.json'), 'utf8'),
|
|
);
|
|
const desktopPackageJson = JSON.parse(
|
|
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'),
|
|
);
|
|
const desktopPackageLock = JSON.parse(
|
|
fs.readFileSync(path.join(packageRoot, 'package-lock.json'), 'utf8'),
|
|
);
|
|
const electronVersion =
|
|
desktopPackageJson.devDependencies?.electron?.replace(/^[^\d]*/, '') ?? '41.2.1';
|
|
const electronBuilderVersion =
|
|
desktopPackageJson.devDependencies?.['electron-builder']?.replace(/^[^\d]*/, '') ?? '26.8.1';
|
|
const bundledNodeExecutableName = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
const electronBuilderCliPath = path.join(packageRoot, 'node_modules', 'electron-builder', 'cli.js');
|
|
const builderUtilRequire = createRequire(
|
|
path.join(packageRoot, 'node_modules', 'builder-util', 'out', 'util.js'),
|
|
);
|
|
const requiredBuilderRuntimeModules = ['app-builder-bin'];
|
|
const appBuilderLibVersion =
|
|
desktopPackageLock.packages?.['node_modules/app-builder-lib']?.version ??
|
|
electronBuilderVersion.replace(/^[^\d]*/, '');
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const outputDir = path.join(releaseRoot, stamp);
|
|
const latestReleasePointerPath = path.join(releaseRoot, '.latest-unpacked-release');
|
|
const electronBuilderConfigPath = path.join(packageRoot, 'electron-builder.yml');
|
|
const requiredNsisTemplateFiles = [
|
|
path.join(packageRoot, 'node_modules', 'app-builder-lib', 'templates', 'nsis', 'messages.yml'),
|
|
path.join(
|
|
packageRoot,
|
|
'node_modules',
|
|
'app-builder-lib',
|
|
'templates',
|
|
'nsis',
|
|
'assistedMessages.yml',
|
|
),
|
|
];
|
|
const supportedTargetHosts = {
|
|
'--linux': 'linux',
|
|
'--mac': 'darwin',
|
|
'--win': 'win32',
|
|
};
|
|
const artifactFileExtensions = new Set(['.AppImage', '.dmg', '.exe', '.msi', '.zip']);
|
|
const gitnexusRuntimeDependencyNames = Object.keys(gitnexusPackageJson.dependencies ?? {});
|
|
const packagedResourceEntries = [
|
|
{
|
|
from: path.join(gitnexusWebRoot, 'dist'),
|
|
to: 'gitnexus-web',
|
|
},
|
|
{
|
|
from: path.join(gitnexusRoot, 'dist'),
|
|
to: path.join('gitnexus', 'dist'),
|
|
},
|
|
{
|
|
from: path.join(gitnexusRoot, 'hooks'),
|
|
to: path.join('gitnexus', 'hooks'),
|
|
},
|
|
{
|
|
from: path.join(gitnexusRoot, 'skills'),
|
|
to: path.join('gitnexus', 'skills'),
|
|
},
|
|
{
|
|
from: path.join(gitnexusRoot, 'vendor'),
|
|
to: path.join('gitnexus', 'vendor'),
|
|
},
|
|
{
|
|
from: path.join(gitnexusRoot, 'package.json'),
|
|
to: path.join('gitnexus', 'package.json'),
|
|
},
|
|
];
|
|
|
|
const toBuilderRelativePath = (targetPath) => {
|
|
return path.relative(packageRoot, targetPath) || '.';
|
|
};
|
|
|
|
const getNodeModulePath = (packageName) => {
|
|
return path.join(gitnexusRoot, 'node_modules', ...packageName.split('/'));
|
|
};
|
|
|
|
const getRuntimeDependencyClosure = () => {
|
|
const visited = new Set();
|
|
const queue = [...gitnexusRuntimeDependencyNames];
|
|
|
|
while (queue.length > 0) {
|
|
const packageName = queue.shift();
|
|
|
|
if (!packageName || visited.has(packageName)) {
|
|
continue;
|
|
}
|
|
|
|
visited.add(packageName);
|
|
|
|
const lockEntry = gitnexusPackageLock.packages?.[`node_modules/${packageName}`];
|
|
const dependencyNames = Object.keys({
|
|
...(lockEntry?.dependencies ?? {}),
|
|
...(lockEntry?.optionalDependencies ?? {}),
|
|
});
|
|
|
|
for (const dependencyName of dependencyNames) {
|
|
if (!visited.has(dependencyName)) {
|
|
queue.push(dependencyName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [...visited].filter((packageName) => fs.existsSync(getNodeModulePath(packageName))).sort();
|
|
};
|
|
|
|
const packagedRuntimeNodeModules = getRuntimeDependencyClosure();
|
|
|
|
const builderArgs = process.argv.slice(2);
|
|
const requestedTargets = builderArgs.filter((argument) => argument in supportedTargetHosts);
|
|
|
|
const builderEnvironment = {
|
|
GITNEXUS_DESKTOP_GITNEXUS_DIST: toBuilderRelativePath(path.join(gitnexusRoot, 'dist')),
|
|
GITNEXUS_DESKTOP_GITNEXUS_HOOKS: toBuilderRelativePath(path.join(gitnexusRoot, 'hooks')),
|
|
GITNEXUS_DESKTOP_GITNEXUS_NODE_MODULES: toBuilderRelativePath(
|
|
path.join(gitnexusRoot, 'node_modules'),
|
|
),
|
|
GITNEXUS_DESKTOP_GITNEXUS_PACKAGE_JSON: toBuilderRelativePath(
|
|
path.join(gitnexusRoot, 'package.json'),
|
|
),
|
|
GITNEXUS_DESKTOP_GITNEXUS_SKILLS: toBuilderRelativePath(path.join(gitnexusRoot, 'skills')),
|
|
GITNEXUS_DESKTOP_GITNEXUS_VENDOR: toBuilderRelativePath(path.join(gitnexusRoot, 'vendor')),
|
|
GITNEXUS_DESKTOP_NODE_EXECUTABLE: process.execPath,
|
|
GITNEXUS_DESKTOP_NODE_RESOURCE_PATH: `gitnexus-node/${bundledNodeExecutableName}`,
|
|
GITNEXUS_DESKTOP_WEB_DIST: toBuilderRelativePath(path.join(gitnexusWebRoot, 'dist')),
|
|
};
|
|
|
|
const builderCommand = [
|
|
`node "${electronBuilderCliPath}"`,
|
|
`--config "${electronBuilderConfigPath}"`,
|
|
`-c.electronVersion=${electronVersion}`,
|
|
`-c.directories.output="${outputDir}"`,
|
|
'--publish never',
|
|
...builderArgs,
|
|
].join(' ');
|
|
|
|
const runCommand = (command, cwd, extraEnv = {}) => {
|
|
execSync(command, {
|
|
cwd,
|
|
env: {
|
|
...process.env,
|
|
...extraEnv,
|
|
},
|
|
stdio: 'inherit',
|
|
});
|
|
};
|
|
|
|
const tryRunCommand = (command, cwd, extraEnv = {}) => {
|
|
try {
|
|
runCommand(command, cwd, extraEnv);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const mirrorDirectory = (sourceDirectory, destinationDirectory) => {
|
|
if (process.platform === 'win32') {
|
|
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
|
|
try {
|
|
execSync(
|
|
`robocopy "${sourceDirectory}" "${destinationDirectory}" /MIR /NFL /NDL /NJH /NJS /NP`,
|
|
{
|
|
cwd: packageRoot,
|
|
stdio: 'inherit',
|
|
},
|
|
);
|
|
} catch (error) {
|
|
const exitCode = error.status ?? 16;
|
|
|
|
if (exitCode > 7) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
fs.rmSync(destinationDirectory, { force: true, recursive: true });
|
|
fs.cpSync(sourceDirectory, destinationDirectory, { force: true, recursive: true });
|
|
};
|
|
|
|
const canResolveBuilderRuntimeModule = (moduleName) => {
|
|
try {
|
|
builderUtilRequire.resolve(moduleName);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const overlayDirectoryContents = (sourceDirectory, destinationDirectory) => {
|
|
fs.mkdirSync(destinationDirectory, { recursive: true });
|
|
|
|
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
fs.cpSync(path.join(sourceDirectory, entry.name), path.join(destinationDirectory, entry.name), {
|
|
force: true,
|
|
recursive: true,
|
|
});
|
|
}
|
|
};
|
|
|
|
const resolvePackagedResourceRoots = () => {
|
|
const candidateRoots = [
|
|
path.join(outputDir, 'win-unpacked', 'resources'),
|
|
path.join(outputDir, 'linux-unpacked', 'resources'),
|
|
path.join(
|
|
outputDir,
|
|
`${desktopPackageJson.productName ?? 'GitNexus Desktop'}.app`,
|
|
'Contents',
|
|
'Resources',
|
|
),
|
|
];
|
|
|
|
return candidateRoots.filter((candidateRoot) => fs.existsSync(candidateRoot));
|
|
};
|
|
|
|
const syncPackagedRuntimeResources = () => {
|
|
const resourceRoots = resolvePackagedResourceRoots();
|
|
|
|
if (resourceRoots.length === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const resourceRoot of resourceRoots) {
|
|
const packagedNodeModulesRoot = path.join(resourceRoot, 'gitnexus', 'node_modules');
|
|
|
|
fs.rmSync(packagedNodeModulesRoot, { force: true, recursive: true });
|
|
fs.mkdirSync(packagedNodeModulesRoot, { recursive: true });
|
|
|
|
const sourceNodeModulesLock = path.join(gitnexusRoot, 'node_modules', '.package-lock.json');
|
|
const destinationNodeModulesLock = path.join(packagedNodeModulesRoot, '.package-lock.json');
|
|
|
|
if (fs.existsSync(sourceNodeModulesLock)) {
|
|
fs.cpSync(sourceNodeModulesLock, destinationNodeModulesLock, { force: true });
|
|
}
|
|
|
|
for (const packageName of packagedRuntimeNodeModules) {
|
|
const sourcePackagePath = getNodeModulePath(packageName);
|
|
const destinationPackagePath = path.join(packagedNodeModulesRoot, ...packageName.split('/'));
|
|
|
|
mirrorDirectory(sourcePackagePath, destinationPackagePath);
|
|
}
|
|
|
|
for (const entry of packagedResourceEntries) {
|
|
const destinationPath = path.join(resourceRoot, entry.to);
|
|
|
|
if (fs.statSync(entry.from).isDirectory()) {
|
|
mirrorDirectory(entry.from, destinationPath);
|
|
continue;
|
|
}
|
|
|
|
fs.rmSync(destinationPath, { force: true, recursive: true });
|
|
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
fs.cpSync(entry.from, destinationPath, { force: true });
|
|
}
|
|
}
|
|
};
|
|
|
|
const repairAppBuilderLibPackage = () => {
|
|
const repairDirectory = fs.mkdtempSync(
|
|
path.join(os.tmpdir(), 'gitnexus-desktop-app-builder-lib-'),
|
|
);
|
|
|
|
try {
|
|
const tarballName = `app-builder-lib-${appBuilderLibVersion}.tgz`;
|
|
const tarballPath = path.join(repairDirectory, tarballName);
|
|
const extractedPackageRoot = path.join(repairDirectory, 'package');
|
|
const installedPackageRoot = path.join(packageRoot, 'node_modules', 'app-builder-lib');
|
|
|
|
runCommand(
|
|
`npm pack app-builder-lib@${appBuilderLibVersion} --pack-destination "${repairDirectory}"`,
|
|
packageRoot,
|
|
);
|
|
runCommand(`tar -xzf "${tarballPath}" -C "${repairDirectory}"`, packageRoot);
|
|
overlayDirectoryContents(extractedPackageRoot, installedPackageRoot);
|
|
} finally {
|
|
fs.rmSync(repairDirectory, { force: true, recursive: true });
|
|
}
|
|
};
|
|
|
|
const assertSupportedHostForRequestedTargets = () => {
|
|
const unsupportedTargets = requestedTargets.filter(
|
|
(target) => supportedTargetHosts[target] !== process.platform,
|
|
);
|
|
|
|
if (unsupportedTargets.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const messages = unsupportedTargets.map((target) => {
|
|
if (target === '--mac') {
|
|
return 'macOS DMG builds must run on a macOS host. Use the Desktop Packaging GitHub Actions workflow for .dmg artifacts.';
|
|
}
|
|
|
|
if (target === '--linux') {
|
|
return 'Linux AppImage builds should run on a Linux host. Use the Desktop Packaging GitHub Actions workflow for Linux installers.';
|
|
}
|
|
|
|
return 'Windows NSIS builds must run on a Windows host.';
|
|
});
|
|
|
|
throw new Error(messages.join('\n'));
|
|
};
|
|
|
|
const ensureDesktopToolchainHealthy = () => {
|
|
const missingBuilderRuntimeModules = requiredBuilderRuntimeModules.filter(
|
|
(moduleName) => !canResolveBuilderRuntimeModule(moduleName),
|
|
);
|
|
const missingNsisTemplates = requestedTargets.includes('--win')
|
|
? requiredNsisTemplateFiles.filter((filePath) => !fs.existsSync(filePath))
|
|
: [];
|
|
|
|
if (missingBuilderRuntimeModules.length === 0 && missingNsisTemplates.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (missingNsisTemplates.length > 0) {
|
|
console.warn(
|
|
'[build] electron-builder NSIS templates are missing. Restoring app-builder-lib package contents first...',
|
|
);
|
|
|
|
try {
|
|
repairAppBuilderLibPackage();
|
|
} catch {
|
|
// Fall through to install-based repair paths below.
|
|
}
|
|
}
|
|
|
|
const remainingMissingBuilderRuntimeModules = requiredBuilderRuntimeModules.filter(
|
|
(moduleName) => !canResolveBuilderRuntimeModule(moduleName),
|
|
);
|
|
const remainingMissingNsisTemplates = requestedTargets.includes('--win')
|
|
? requiredNsisTemplateFiles.filter((filePath) => !fs.existsSync(filePath))
|
|
: [];
|
|
|
|
if (
|
|
remainingMissingBuilderRuntimeModules.length === 0 &&
|
|
remainingMissingNsisTemplates.length === 0
|
|
) {
|
|
return;
|
|
}
|
|
|
|
console.warn(
|
|
'[build] electron-builder installation is incomplete. Restoring desktop dependencies with npm ci...',
|
|
);
|
|
runCommand('npm ci', packageRoot);
|
|
|
|
const unresolvedBuilderRuntimeModules = requiredBuilderRuntimeModules.filter(
|
|
(moduleName) => !canResolveBuilderRuntimeModule(moduleName),
|
|
);
|
|
const unresolvedTemplates = requiredNsisTemplateFiles.filter(
|
|
(filePath) => !fs.existsSync(filePath),
|
|
);
|
|
|
|
if (unresolvedBuilderRuntimeModules.length > 0 || unresolvedTemplates.length > 0) {
|
|
throw new Error(
|
|
[
|
|
'electron-builder is missing required runtime modules after reinstall:',
|
|
...unresolvedBuilderRuntimeModules,
|
|
...unresolvedTemplates,
|
|
].join('\n'),
|
|
);
|
|
}
|
|
};
|
|
|
|
const listArtifacts = (directoryPath) => {
|
|
const results = [];
|
|
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(directoryPath, entry.name);
|
|
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === 'win-unpacked') {
|
|
results.push(fullPath);
|
|
continue;
|
|
}
|
|
|
|
results.push(...listArtifacts(fullPath));
|
|
continue;
|
|
}
|
|
|
|
if (artifactFileExtensions.has(path.extname(entry.name))) {
|
|
results.push(fullPath);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
};
|
|
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
fs.writeFileSync(latestReleasePointerPath, `${outputDir}\n`);
|
|
|
|
assertSupportedHostForRequestedTargets();
|
|
ensureDesktopToolchainHealthy();
|
|
|
|
runCommand('node scripts/ensure-gitnexus-runtime.mjs', packageRoot);
|
|
|
|
runCommand('npm run bundle', packageRoot);
|
|
|
|
runCommand('npm run build', gitnexusWebRoot);
|
|
|
|
runCommand(builderCommand, packageRoot, builderEnvironment);
|
|
syncPackagedRuntimeResources();
|
|
|
|
const artifacts = listArtifacts(outputDir);
|
|
|
|
console.log(`[build] artifacts written to release/${stamp}`);
|
|
|
|
if (artifacts.length > 0) {
|
|
console.log('[build] artifacts:');
|
|
for (const artifact of artifacts) {
|
|
console.log(` - ${artifact}`);
|
|
}
|
|
}
|