mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
278 lines
9.1 KiB
TypeScript
278 lines
9.1 KiB
TypeScript
import { dirname, isAbsolute, join } from 'node:path';
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const DEFAULT_TARGETS = [
|
|
'bun-darwin-x64',
|
|
'bun-darwin-arm64',
|
|
'bun-linux-x64-baseline',
|
|
'bun-linux-arm64',
|
|
'bun-windows-x64'
|
|
];
|
|
const SUPPORTED_PLATFORMS = new Set(['darwin', 'linux', 'windows']);
|
|
const SUPPORTED_ARCHES = new Set(['x64', 'arm64']);
|
|
const SUPPORTED_LINUX_X64_VARIANTS = new Set(['baseline', 'modern']);
|
|
|
|
function getArg(args: string[], name: string): string | undefined {
|
|
const idx = args.indexOf(name);
|
|
if (idx === -1 || idx + 1 >= args.length) {
|
|
return undefined;
|
|
}
|
|
return args[idx + 1];
|
|
}
|
|
|
|
function resolveHostPlatform(): string {
|
|
if (process.platform === 'win32') {
|
|
return 'windows';
|
|
}
|
|
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
return process.platform;
|
|
}
|
|
throw new Error(`Unsupported host platform: ${process.platform}`);
|
|
}
|
|
|
|
function resolveHostArch(): string {
|
|
if (SUPPORTED_ARCHES.has(process.arch)) {
|
|
return process.arch;
|
|
}
|
|
throw new Error(`Unsupported host arch: ${process.arch}`);
|
|
}
|
|
|
|
function resolveDefaultTarget(): string {
|
|
const platform = resolveHostPlatform();
|
|
const arch = resolveHostArch();
|
|
if (platform === 'linux' && arch === 'x64') {
|
|
return 'bun-linux-x64-baseline';
|
|
}
|
|
return `bun-${platform}-${arch}`;
|
|
}
|
|
|
|
function resolveTarget(target?: string): string {
|
|
if (!target) {
|
|
return resolveDefaultTarget();
|
|
}
|
|
|
|
const parts = target.split('-');
|
|
if (parts.length < 2 || parts.length > 4 || parts[0] !== 'bun') {
|
|
throw new Error(`Invalid target: ${target}`);
|
|
}
|
|
|
|
const platformPart = parts[1];
|
|
if (!SUPPORTED_PLATFORMS.has(platformPart)) {
|
|
throw new Error(`Unsupported platform in target: ${target}`);
|
|
}
|
|
|
|
const archPart = parts[2] ?? resolveHostArch();
|
|
if (!SUPPORTED_ARCHES.has(archPart)) {
|
|
throw new Error(`Unsupported arch in target: ${target}`);
|
|
}
|
|
|
|
const variantPart = parts[3];
|
|
if (!variantPart) {
|
|
return `bun-${platformPart}-${archPart}`;
|
|
}
|
|
|
|
if (platformPart !== 'linux' || archPart !== 'x64') {
|
|
throw new Error(`Unsupported variant in target: ${target}`);
|
|
}
|
|
|
|
if (!SUPPORTED_LINUX_X64_VARIANTS.has(variantPart)) {
|
|
throw new Error(`Unsupported linux x64 variant in target: ${target}`);
|
|
}
|
|
|
|
return `bun-${platformPart}-${archPart}-${variantPart}`;
|
|
}
|
|
|
|
function parseTarget(target: string): { platform: string; arch: string } {
|
|
const parts = target.split('-');
|
|
if ((parts.length !== 3 && parts.length !== 4) || parts[0] !== 'bun') {
|
|
throw new Error(`Invalid target: ${target}`);
|
|
}
|
|
|
|
const platformPart = parts[1];
|
|
const archPart = parts[2];
|
|
|
|
if (!SUPPORTED_PLATFORMS.has(platformPart)) {
|
|
throw new Error(`Unsupported platform in target: ${target}`);
|
|
}
|
|
|
|
if (!SUPPORTED_ARCHES.has(archPart)) {
|
|
throw new Error(`Unsupported arch in target: ${target}`);
|
|
}
|
|
|
|
const variantPart = parts[3];
|
|
if (variantPart) {
|
|
if (platformPart !== 'linux' || archPart !== 'x64') {
|
|
throw new Error(`Unsupported variant in target: ${target}`);
|
|
}
|
|
if (!SUPPORTED_LINUX_X64_VARIANTS.has(variantPart)) {
|
|
throw new Error(`Unsupported linux x64 variant in target: ${target}`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
platform: platformPart === 'windows' ? 'win32' : platformPart,
|
|
arch: archPart
|
|
};
|
|
}
|
|
|
|
function getFeatureFlag(platform: string, arch: string): string {
|
|
const platformToken = platform === 'win32' ? 'WIN32' : platform.toUpperCase();
|
|
return `HAPI_TARGET_${platformToken}_${arch.toUpperCase()}`;
|
|
}
|
|
|
|
function getPlatformDir(platform: string, arch: string): string {
|
|
if (platform === 'darwin') {
|
|
return arch === 'arm64' ? 'arm64-darwin' : 'x64-darwin';
|
|
}
|
|
|
|
if (platform === 'linux') {
|
|
return arch === 'arm64' ? 'arm64-linux' : 'x64-linux';
|
|
}
|
|
|
|
if (platform === 'win32') {
|
|
return 'x64-win32';
|
|
}
|
|
|
|
throw new Error(`Unsupported platform: ${platform}`);
|
|
}
|
|
|
|
function assertArchivesExist(projectRoot: string, platform: string, arch: string): void {
|
|
const platformDir = getPlatformDir(platform, arch);
|
|
const archives = [
|
|
join(projectRoot, 'tools', 'archives', `difftastic-${platformDir}.tar.gz`),
|
|
join(projectRoot, 'tools', 'archives', `ripgrep-${platformDir}.tar.gz`)
|
|
];
|
|
|
|
for (const archive of archives) {
|
|
if (!existsSync(archive)) {
|
|
throw new Error(`Missing archive: ${archive}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveOutdir(projectRoot: string, outdir: string): string {
|
|
if (isAbsolute(outdir)) {
|
|
return outdir;
|
|
}
|
|
return join(projectRoot, outdir);
|
|
}
|
|
|
|
function writeStubEmbeddedAssets(workspaceRoot: string): void {
|
|
const outputPath = join(workspaceRoot, 'hub', 'src', 'web', 'embeddedAssets.generated.ts');
|
|
const contents = [
|
|
'// This file is generated by cli/scripts/build-executable.ts when --with-web-assets is not used.',
|
|
'// It intentionally contains no embedded assets.',
|
|
'',
|
|
'export interface EmbeddedWebAsset {',
|
|
' path: string;',
|
|
' sourcePath: string;',
|
|
' mimeType: string;',
|
|
'}',
|
|
'',
|
|
'export const embeddedAssets: EmbeddedWebAsset[] = [];',
|
|
''
|
|
].join('\n');
|
|
writeFileSync(outputPath, contents, 'utf-8');
|
|
}
|
|
|
|
function isStubEmbeddedAssets(manifestPath: string): boolean {
|
|
try {
|
|
const contents = readFileSync(manifestPath, 'utf-8');
|
|
return contents.includes('intentionally contains no embedded assets');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function ensureEmbeddedAssetsManifest(workspaceRoot: string, includeWebAssets: boolean): void {
|
|
const manifestPath = join(workspaceRoot, 'hub', 'src', 'web', 'embeddedAssets.generated.ts');
|
|
if (includeWebAssets) {
|
|
if (!existsSync(manifestPath)) {
|
|
throw new Error(
|
|
'Missing embedded web asset manifest. Run `bun run build:web` and `cd hub && bun run generate:embedded-web-assets`, or `bun run build:single-exe` from the repo root.'
|
|
);
|
|
}
|
|
if (isStubEmbeddedAssets(manifestPath)) {
|
|
throw new Error(
|
|
'Embedded web asset manifest is a stub. Run `bun run build:web` and `cd hub && bun run generate:embedded-web-assets`, or `bun run build:single-exe` from the repo root.'
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
writeStubEmbeddedAssets(workspaceRoot);
|
|
}
|
|
|
|
async function buildTarget(projectRoot: string, target: string, outdir: string, name: string): Promise<void> {
|
|
const { platform, arch } = parseTarget(target);
|
|
assertArchivesExist(projectRoot, platform, arch);
|
|
const outputName = platform === 'win32' ? `${name}.exe` : name;
|
|
const outfile = join(outdir, target, outputName);
|
|
mkdirSync(dirname(outfile), { recursive: true });
|
|
const featureFlag = getFeatureFlag(platform, arch);
|
|
|
|
const cmd = [
|
|
process.execPath,
|
|
'build',
|
|
'--compile',
|
|
'--no-compile-autoload-dotenv',
|
|
`--feature=${featureFlag}`,
|
|
`--target=${target}`,
|
|
`--outfile=${outfile}`,
|
|
join(projectRoot, 'src', 'bootstrap.ts')
|
|
];
|
|
|
|
console.log(`[build:exe] ${cmd.join(' ')}`);
|
|
|
|
const proc = Bun.spawn({
|
|
cmd,
|
|
env: process.env,
|
|
stdout: 'inherit',
|
|
stderr: 'inherit',
|
|
cwd: projectRoot
|
|
});
|
|
|
|
const exitCode = await proc.exited;
|
|
if (exitCode !== 0) {
|
|
throw new Error(`Build failed for target ${target} (exit ${exitCode})`);
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const args = process.argv.slice(2);
|
|
const target = getArg(args, '--target');
|
|
const outdirArg = getArg(args, '--outdir') ?? 'dist-exe';
|
|
const name = getArg(args, '--name') ?? 'hapi';
|
|
const buildAll = args.includes('--all');
|
|
const includeWebAssets = args.includes('--with-web-assets');
|
|
|
|
if (args.includes('--target') && !target) {
|
|
console.error('Usage: bun run scripts/build-executable.ts [--target <bun-platform[-arch[-variant]]>] [--outdir dist-exe] [--name hapi] [--with-web-assets]');
|
|
console.error(' or: bun run scripts/build-executable.ts --all [--outdir dist-exe] [--name hapi] [--with-web-assets]');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (!buildAll && !target) {
|
|
console.log('[build:exe] No --target provided; defaulting to host platform + arch.');
|
|
}
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = join(scriptDir, '..');
|
|
const workspaceRoot = join(projectRoot, '..');
|
|
const outdir = resolveOutdir(projectRoot, outdirArg);
|
|
const resolvedTarget = buildAll ? undefined : resolveTarget(target);
|
|
const targets = buildAll ? DEFAULT_TARGETS : [resolvedTarget!];
|
|
|
|
ensureEmbeddedAssetsManifest(workspaceRoot, includeWebAssets);
|
|
|
|
for (const targetName of targets) {
|
|
await buildTarget(projectRoot, targetName, outdir, name);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`[build:exe] ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exit(1);
|
|
});
|