feat(launcher): bundle a private Bun runtime

This commit is contained in:
2026-09-10 12:22:29 -07:00
parent 614a8ca912
commit 84b234cc19
35 changed files with 5484 additions and 76 deletions
+1 -1
View File
@@ -979,7 +979,7 @@ function renderReleaseNotes(
'- Windows: `SubMiner-*.exe` and `SubMiner-*-win.zip`',
'- Optional extras: `subminer-assets.tar.gz` and the `subminer` launcher',
'',
'Note: the `subminer` wrapper script uses Bun (`#!/usr/bin/env bun`), so `bun` must be installed and on `PATH`.',
"The launcher installed by desktop setup uses the app's bundled Bun runtime. Only the separately downloaded `subminer` script requires Bun installed on `PATH`.",
'',
].join('\n');
}
+18 -1
View File
@@ -86,15 +86,32 @@ async function verifyMacOSWindowHelper(
return true;
}
async function afterPack(context) {
async function stageBundledBunRuntime(context, deps = {}) {
const stageBunRuntime =
deps.stageBunRuntime ?? (await import('./stage-bun-runtime.mjs')).stageBunRuntime;
const productFilename = context.packager?.appInfo?.productFilename;
await stageBunRuntime({
appOutDir: context.appOutDir,
platform: context.electronPlatformName,
arch: context.arch,
productFilename:
typeof productFilename === 'string' && productFilename.trim()
? productFilename.trim()
: 'SubMiner',
});
}
async function afterPack(context, deps = {}) {
await stageLinuxAppImageSharedLibrary(context);
await verifyMacOSWindowHelper(context);
await stageBundledBunRuntime(context, deps);
}
module.exports = {
LINUX_FFMPEG_LIBRARY,
MACOS_WINDOW_HELPER,
resolveMacOSAppBundlePath,
stageBundledBunRuntime,
stageLinuxAppImageSharedLibrary,
verifyMacOSWindowHelper,
default: afterPack,
+60 -1
View File
@@ -13,7 +13,22 @@ const {
} = require('./electron-builder-after-pack.cjs') as {
LINUX_FFMPEG_LIBRARY: string;
MACOS_WINDOW_HELPER: string;
default: (context: { appOutDir: string; electronPlatformName: string }) => Promise<void>;
default: (
context: {
appOutDir: string;
arch?: number;
electronPlatformName: string;
packager?: { appInfo?: { productFilename?: string } };
},
deps?: {
stageBunRuntime?: (options: {
appOutDir: string;
platform: string;
arch: number | undefined;
productFilename: string;
}) => Promise<void>;
},
) => Promise<void>;
stageLinuxAppImageSharedLibrary: (context: {
appOutDir: string;
electronPlatformName: string;
@@ -156,3 +171,47 @@ test('afterPack propagates Linux staging failures', async () => {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
test('afterPack preserves Linux staging and forwards the electron-builder target to Bun staging', async () => {
const workspace = createWorkspace('subminer-after-pack-target');
const appOutDir = path.join(workspace, 'SubMiner-linux-arm64');
const sourceLibraryPath = path.join(appOutDir, LINUX_FFMPEG_LIBRARY);
const targetLibraryPath = path.join(appOutDir, 'usr', 'lib', LINUX_FFMPEG_LIBRARY);
let stagedOptions:
| {
appOutDir: string;
platform: string;
arch: number | undefined;
productFilename: string;
}
| undefined;
fs.mkdirSync(appOutDir, { recursive: true });
fs.writeFileSync(sourceLibraryPath, 'bundled ffmpeg', 'utf8');
try {
await afterPack(
{
appOutDir,
arch: 3,
electronPlatformName: 'linux',
packager: { appInfo: { productFilename: 'SubMiner Preview' } },
},
{
stageBunRuntime: async (options) => {
stagedOptions = options;
},
},
);
assert.deepEqual(stagedOptions, {
appOutDir,
platform: 'linux',
arch: 3,
productFilename: 'SubMiner Preview',
});
assert.equal(fs.readFileSync(targetLibraryPath, 'utf8'), 'bundled ffmpeg');
} finally {
fs.rmSync(workspace, { recursive: true, force: true });
}
});
+452
View File
@@ -0,0 +1,452 @@
import { createHash, randomUUID } from 'node:crypto';
import { createReadStream, createWriteStream } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { pipeline } from 'node:stream/promises';
import { Readable } from 'node:stream';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
export const DEFAULT_MANIFEST_PATH = path.join(repoRoot, 'build', 'bun-source-manifest.json');
export const DEFAULT_RUNTIME_MANIFEST_PATH = path.join(
repoRoot,
'build',
'bun-runtime-manifest.json',
);
export const DEFAULT_PACKAGE_JSON_PATH = path.join(repoRoot, 'package.json');
export const DEFAULT_OUTPUT_DIR = path.join(repoRoot, 'release');
export const DEFAULT_CACHE_DIR = path.join(repoRoot, '.tmp', 'bun-corresponding-source');
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function requiredString(record, key, source) {
const value = record[key];
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`${source} must contain a non-empty ${key} string.`);
}
return value;
}
function assertSafeRelativePath(value, source) {
if (path.isAbsolute(value) || value.split(/[\\/]/).includes('..')) {
throw new Error(`${source} contains an unsafe path: ${value}`);
}
}
export function parseSourceManifest(value) {
if (!isRecord(value) || value.schemaVersion !== 1) {
throw new Error('Bun source manifest must use schemaVersion 1.');
}
const version = requiredString(value, 'version', 'Bun source manifest');
const bunRevision = requiredString(value, 'bunRevision', 'Bun source manifest');
const archiveName = requiredString(value, 'archiveName', 'Bun source manifest');
if (!/^\d+\.\d+\.\d+$/.test(version) || !/^[a-f0-9]{40}$/.test(bunRevision)) {
throw new Error('Bun source manifest has an invalid version or bunRevision.');
}
if (path.basename(archiveName) !== archiveName || !archiveName.endsWith('.tar.gz')) {
throw new Error('Bun source manifest archiveName must be a safe .tar.gz filename.');
}
if (!Array.isArray(value.sources) || value.sources.length === 0) {
throw new Error('Bun source manifest must contain sources.');
}
const names = new Set();
const destinations = new Set();
const sources = value.sources.map((entry, index) => {
if (!isRecord(entry)) throw new Error(`Bun source ${index} must be an object.`);
const name = requiredString(entry, 'name', `Bun source ${index}`);
const repository = requiredString(entry, 'repository', `Bun source ${name}`);
const revision = requiredString(entry, 'revision', `Bun source ${name}`);
const destination = requiredString(entry, 'destination', `Bun source ${name}`);
if (!/^[\w.-]+\/[\w.-]+$/.test(repository) || !/^[a-f0-9]{40}$/.test(revision)) {
throw new Error(`Bun source ${name} has an invalid repository or revision.`);
}
assertSafeRelativePath(destination, `Bun source ${name}`);
if (names.has(name) || destinations.has(destination)) {
throw new Error(`Bun source manifest repeats ${name} or ${destination}.`);
}
names.add(name);
destinations.add(destination);
const transport = entry.transport ?? 'archive';
if (transport !== 'archive' && transport !== 'git-sparse') {
throw new Error(`Bun source ${name} has unsupported transport ${transport}.`);
}
const sha256 =
transport === 'archive' ? requiredString(entry, 'sha256', `Bun source ${name}`) : null;
if (sha256 !== null && !/^[a-f0-9]{64}$/.test(sha256)) {
throw new Error(`Bun source ${name} has an invalid SHA-256 digest.`);
}
if (!Array.isArray(entry.licensePaths) || entry.licensePaths.length === 0) {
throw new Error(`Bun source ${name} must declare licensePaths.`);
}
const licensePaths = entry.licensePaths.map((licensePath) => {
if (typeof licensePath !== 'string' || licensePath.length === 0) {
throw new Error(`Bun source ${name} has an invalid license path.`);
}
assertSafeRelativePath(licensePath, `Bun source ${name}`);
return licensePath;
});
const exclude = Array.isArray(entry.exclude) ? entry.exclude : [];
for (const excludedPath of exclude) assertSafeRelativePath(excludedPath, `Bun source ${name}`);
return {
...entry,
name,
repository,
revision,
destination,
transport,
sha256,
licensePaths,
exclude,
};
});
const bun = sources.find((source) => source.name === 'bun');
if (!bun || bun.revision !== bunRevision || bun.destination !== 'bun') {
throw new Error('Bun source manifest must map bunRevision to the bun source at bun/.');
}
return { ...value, version, bunRevision, archiveName, sources };
}
export function parseRegisteredRepositories(cmakeText) {
const registrations = new Map();
const uncommented = cmakeText.replace(/#[^\n]*/g, '');
for (const match of uncommented.matchAll(/register_repository\(([\s\S]*?)\)/g)) {
const body = match[1];
const name = /\bNAME\s+([^\s#)]+)/.exec(body)?.[1];
const repository = /\bREPOSITORY\s+([^\s#)]+)/.exec(body)?.[1];
const reference = /\b(COMMIT|TAG)\s+(?:#[^\n]*\n\s*)?([^\s#)]+)/.exec(body);
if (name && repository && reference) {
registrations.set(name, {
repository,
kind: reference[1].toLowerCase(),
reference: reference[2],
});
}
}
return registrations;
}
export function validateRuntimeAlignment(manifest, packageJson, runtimeManifest) {
if (!isRecord(packageJson) || packageJson.packageManager !== `bun@${manifest.version}`) {
throw new Error(
`package.json must pin bun@${manifest.version} to match the Bun source manifest.`,
);
}
if (!isRecord(runtimeManifest)) throw new Error('Bun runtime manifest must be an object.');
for (const key of ['version', 'bunRevision']) {
if (runtimeManifest[key] !== manifest[key]) {
throw new Error(`Bun runtime manifest ${key} does not match the Bun source manifest.`);
}
}
if (runtimeManifest.correspondingSourceAsset !== manifest.archiveName) {
throw new Error(
'Bun runtime manifest correspondingSourceAsset does not match the Bun source manifest.',
);
}
}
export function validateBunPins(manifest, cmakeTexts, setupWebKitText) {
const actual = new Map();
for (const cmakeText of cmakeTexts) {
for (const [name, registration] of parseRegisteredRepositories(cmakeText)) {
if (actual.has(name)) throw new Error(`Bun registers ${name} more than once.`);
actual.set(name, registration);
}
}
const expected = new Map(
manifest.sources
.filter((source) => source.destination.startsWith('bun/vendor/') && source.name !== 'WebKit')
.map((source) => [source.name, source]),
);
for (const [name, registration] of actual) {
const source = expected.get(name);
if (!source) throw new Error(`Source manifest omits Bun dependency ${name}.`);
if (source.repository !== registration.repository) {
throw new Error(`Source manifest repository mismatch for ${name}.`);
}
const expectedReference = source.upstreamReference ?? source.revision;
if (expectedReference !== registration.reference) {
throw new Error(`Source manifest revision mismatch for ${name}.`);
}
expected.delete(name);
}
if (expected.size > 0) {
throw new Error(
`Source manifest has unregistered Bun dependencies: ${[...expected.keys()].join(', ')}.`,
);
}
const webKitPin = /set\(WEBKIT_VERSION\s+([a-f0-9]{40})\)/.exec(setupWebKitText)?.[1];
const webKit = manifest.sources.find((source) => source.name === 'WebKit');
if (!webKitPin || !webKit || webKit.revision !== webKitPin) {
throw new Error('Source manifest WebKit revision does not match SetupWebKit.cmake.');
}
}
async function sha256File(filePath) {
const hash = createHash('sha256');
for await (const chunk of createReadStream(filePath)) hash.update(chunk);
return hash.digest('hex');
}
async function run(command, args, options = {}) {
await new Promise((resolve, reject) => {
const child = spawn(command, args, { stdio: 'inherit', ...options });
child.once('error', reject);
child.once('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${command} exited with status ${code}.`));
});
});
}
async function downloadArchive(source, cacheDir, fetchImpl) {
const archivePath = path.join(cacheDir, `${source.name}-${source.revision}.tar.gz`);
try {
if ((await sha256File(archivePath)) === source.sha256) return archivePath;
await fs.rm(archivePath, { force: true });
} catch (error) {
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
}
const url = `https://codeload.github.com/${source.repository}/tar.gz/${source.revision}`;
const temporaryPath = `${archivePath}.download-${randomUUID()}`;
const response = await fetchImpl(url);
if (!response.ok || !response.body)
throw new Error(`Unable to download ${url}: HTTP ${response.status}`);
await pipeline(
Readable.fromWeb(response.body),
createWriteStream(temporaryPath, { flags: 'wx' }),
);
const actualSha256 = await sha256File(temporaryPath);
if (actualSha256 !== source.sha256) {
await fs.rm(temporaryPath, { force: true });
throw new Error(
`Source checksum mismatch for ${source.name}: expected ${source.sha256}, received ${actualSha256}.`,
);
}
await fs.rename(temporaryPath, archivePath);
return archivePath;
}
async function materializeArchive(source, root, cacheDir, fetchImpl) {
const archivePath = await downloadArchive(source, cacheDir, fetchImpl);
const destination = path.join(root, source.destination);
await fs.mkdir(destination, { recursive: true });
await run('tar', ['-xzf', archivePath, '-C', destination, '--strip-components=1']);
}
async function materializeGitSparse(source, root, cacheDir) {
const checkout = path.join(cacheDir, `${source.name}-${source.revision}-git`);
await fs.rm(checkout, { recursive: true, force: true });
await run('git', [
'clone',
'--filter=blob:none',
'--no-checkout',
'--depth=1',
`https://github.com/${source.repository}.git`,
checkout,
]);
await run('git', ['-C', checkout, 'fetch', '--depth=1', 'origin', source.revision]);
await run('git', ['-C', checkout, 'sparse-checkout', 'init', '--no-cone']);
const sparseRules = ['/*', ...source.exclude.map((entry) => `!/${entry}/`), ''];
await fs.writeFile(
path.join(checkout, '.git', 'info', 'sparse-checkout'),
sparseRules.join('\n'),
);
await run('git', ['-C', checkout, 'checkout', '--detach', source.revision]);
const actualRevision = (await fs.readFile(path.join(checkout, '.git', 'HEAD'), 'utf8')).trim();
if (actualRevision !== source.revision)
throw new Error(`Git checkout mismatch for ${source.name}.`);
await fs.rm(path.join(checkout, '.git'), { recursive: true, force: true });
await fs.mkdir(path.dirname(path.join(root, source.destination)), { recursive: true });
await fs.rename(checkout, path.join(root, source.destination));
}
async function applyBunDependencyPatches(root, manifest) {
const bunRoot = path.join(root, 'bun');
for (const source of manifest.sources) {
if (!source.destination.startsWith('bun/vendor/') || source.name === 'WebKit') continue;
const destination = path.join(root, source.destination);
const patchDirectory = path.join(bunRoot, 'patches', source.name);
let entries = [];
try {
entries = await fs.readdir(patchDirectory, { withFileTypes: true });
} catch (error) {
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
}
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
const patchPath = path.join(patchDirectory, entry.name);
if (entry.isFile() && entry.name.endsWith('.patch')) {
await run(
'git',
['apply', '--ignore-whitespace', '--ignore-space-change', '--no-index', patchPath],
{ cwd: destination },
);
} else if (entry.isFile()) {
await fs.copyFile(patchPath, path.join(destination, entry.name));
}
}
const cmakeReference = source.upstreamReference
? `refs/tags/${source.upstreamReference}`
: source.revision;
await fs.writeFile(path.join(destination, '.ref'), `${cmakeReference}\n`);
}
}
async function validateAndCollectLicenses(root, manifest) {
const licensesRoot = path.join(root, 'THIRD-PARTY-LICENSES');
await fs.mkdir(licensesRoot, { recursive: true });
for (const source of manifest.sources) {
const target = path.join(licensesRoot, source.name);
await fs.mkdir(target, { recursive: true });
for (const licensePath of source.licensePaths) {
const sourcePath = path.join(root, source.destination, licensePath);
const stat = await fs.stat(sourcePath).catch(() => null);
if (!stat?.isFile() || stat.size === 0) {
throw new Error(`Missing required license material for ${source.name}: ${licensePath}`);
}
const safeName = licensePath.replaceAll('/', '__');
await fs.copyFile(sourcePath, path.join(target, safeName));
}
}
}
function rebuildReadme(manifest) {
const webKit = manifest.sources.find((source) => source.name === 'WebKit');
const tinycc = manifest.sources.find((source) => source.name === 'tinycc');
return `# Bun ${manifest.version} corresponding source and rebuild materials
This archive matches the official Bun ${manifest.version} binaries whose \`bun --revision\` output names commit \`${manifest.bunRevision}\`. The GitHub release tag points to \`${manifest.releaseTagCommit}\`, one later commit, so this package intentionally uses the binary revision.
The archive includes Bun's complete source tree, Bun's build scripts and dependency patches, every external repository registered by Bun's CMake build at its exact pin, and Oven's WebKit fork at \`${webKit.revision}\`. Large WebKit test-only trees are excluded. The JavaScriptCore, WTF, WebCore, build-tool, configuration, and resource trees used to build the library are included. TinyCC is \`${tinycc.revision}\`.
## Rebuild with modified JavaScriptCore
Install the prerequisites recorded in \`bun/.buildkite/Dockerfile\`, \`bun/scripts/bootstrap.sh\`, and the WebKit platform build scripts. Bun ${manifest.version} used LLVM 19.1.7, CMake 3.30.5 in its Linux build image, and Bun 1.1.38 as the bootstrap runtime. Its Rust input was nightly and was not pinned to a dated toolchain in the release source.
From this archive root on Linux or macOS:
\`\`\`sh
cd bun
bun install --frozen-lockfile
bun run jsc:build
bun run build:release:local -- -DVERSION=${manifest.version} -DREVISION=${manifest.bunRevision}
\`\`\`
The first command uses the bootstrap Bun. \`jsc:build\` builds the included \`vendor/WebKit\` checkout into \`vendor/WebKit/WebKitBuild/Release\`. \`build:release:local\` links Bun against that local JavaScriptCore build. The explicit version and revision replace metadata that Bun normally reads from its Git checkout. The included \`vendor/*/.ref\` files prevent Bun's CMake rules from replacing the packaged dependency sources, and this package has already applied the files under \`bun/patches/<dependency>/\` in the same order as \`bun/cmake/scripts/GitClone.cmake\`.
The archive vendors the source repositories that Bun's CMake build links into the executable. It preserves Bun's \`bun.lock\` files and lol-html's \`Cargo.lock\`, but it does not vendor npm packages, crates.io packages used to build lol-html, compilers, SDKs, or other build tools. The rebuild therefore needs network access for those pinned package-manager inputs. License notices embedded in those downloaded packages are outside the collected \`THIRD-PARTY-LICENSES\` directory's scope.
Windows uses the prerequisites in \`bun/docs/project/building-windows.mdx\` and WebKit's \`windows-release.ps1\`. The local-JavaScriptCore path above has not been verified on Windows.
These instructions describe the source and build entry points. Toolchain and generated-output differences mean a rebuild is not expected to be byte-for-byte identical to Oven's release binary. No claim about legal compliance or reproducible builds is made here.
`;
}
async function createDeterministicArchive(stagingParent, rootName, outputPath) {
const temporaryTar = `${outputPath}.tar-${randomUUID()}`;
const temporaryGzip = `${outputPath}.gzip-${randomUUID()}`;
try {
await run('tar', [
'--sort=name',
'--mtime=@0',
'--owner=0',
'--group=0',
'--numeric-owner',
'-cf',
temporaryTar,
'-C',
stagingParent,
rootName,
]);
const gzip = spawn('gzip', ['-n', '-9', '-c', temporaryTar], {
stdio: ['ignore', 'pipe', 'inherit'],
});
const completion = new Promise((resolve, reject) => {
gzip.once('error', reject);
gzip.once('close', resolve);
});
const [, code] = await Promise.all([
pipeline(gzip.stdout, createWriteStream(temporaryGzip, { flags: 'wx' })),
completion,
]);
if (code !== 0) throw new Error(`gzip exited with status ${code}.`);
await fs.rm(outputPath, { force: true });
await fs.rename(temporaryGzip, outputPath);
} finally {
await Promise.all([
fs.rm(temporaryTar, { force: true }),
fs.rm(temporaryGzip, { force: true }),
]);
}
}
export async function packageBunSource({
manifestPath = DEFAULT_MANIFEST_PATH,
runtimeManifestPath = DEFAULT_RUNTIME_MANIFEST_PATH,
packageJsonPath = DEFAULT_PACKAGE_JSON_PATH,
outputDir = DEFAULT_OUTPUT_DIR,
cacheDir = DEFAULT_CACHE_DIR,
fetchImpl = globalThis.fetch,
} = {}) {
const [manifestText, runtimeManifestText, packageJsonText] = await Promise.all([
fs.readFile(manifestPath, 'utf8'),
fs.readFile(runtimeManifestPath, 'utf8'),
fs.readFile(packageJsonPath, 'utf8'),
]);
const manifest = parseSourceManifest(JSON.parse(manifestText));
validateRuntimeAlignment(manifest, JSON.parse(packageJsonText), JSON.parse(runtimeManifestText));
if (typeof fetchImpl !== 'function') throw new Error('No fetch implementation is available.');
await fs.mkdir(cacheDir, { recursive: true });
const stagingParent = await fs.mkdtemp(path.join(cacheDir, 'assemble-'));
const rootName = path.basename(manifest.archiveName, '.tar.gz');
const root = path.join(stagingParent, rootName);
await fs.mkdir(root);
try {
const bun = manifest.sources.find((source) => source.name === 'bun');
await materializeArchive(bun, root, cacheDir, fetchImpl);
const cmakeFiles = (await fs.readdir(path.join(root, 'bun', 'cmake', 'targets')))
.filter((name) => name.endsWith('.cmake'))
.map((name) => fs.readFile(path.join(root, 'bun', 'cmake', 'targets', name), 'utf8'));
validateBunPins(
manifest,
await Promise.all(cmakeFiles),
await fs.readFile(path.join(root, 'bun', 'cmake', 'tools', 'SetupWebKit.cmake'), 'utf8'),
);
for (const source of manifest.sources.filter((entry) => entry.name !== 'bun')) {
if (source.transport === 'git-sparse') await materializeGitSparse(source, root, cacheDir);
else await materializeArchive(source, root, cacheDir, fetchImpl);
}
await applyBunDependencyPatches(root, manifest);
await validateAndCollectLicenses(root, manifest);
await fs.writeFile(
path.join(root, 'SOURCE-INVENTORY.json'),
`${JSON.stringify(manifest, null, 2)}\n`,
);
await fs.writeFile(path.join(root, 'README-REBUILD.md'), rebuildReadme(manifest));
await fs.mkdir(outputDir, { recursive: true });
const outputPath = path.join(outputDir, manifest.archiveName);
await createDeterministicArchive(stagingParent, rootName, outputPath);
const digest = await sha256File(outputPath);
await fs.writeFile(`${outputPath}.sha256`, `${digest} ${manifest.archiveName}\n`);
return { outputPath, sha256: digest };
} finally {
await fs.rm(stagingParent, { recursive: true, force: true });
}
}
if (import.meta.main) {
const result = await packageBunSource();
console.log(`${result.sha256} ${path.basename(result.outputPath)}`);
}
+125
View File
@@ -0,0 +1,125 @@
import { describe, expect, test } from 'bun:test';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
parseRegisteredRepositories,
parseSourceManifest,
validateBunPins,
validateRuntimeAlignment,
} from './package-bun-source.mjs';
const projectRoot = path.resolve(import.meta.dir, '..');
describe('Bun corresponding-source manifest', () => {
test('pins the source to the revision reported by the distributed binary', async () => {
const manifest = parseSourceManifest(
JSON.parse(
await fs.readFile(path.join(projectRoot, 'build/bun-source-manifest.json'), 'utf8'),
),
);
expect(manifest.version).toBe('1.3.5');
expect(manifest.bunRevision).toBe('1e86cebd74a5723e818b5c0555276b646bcf0e4c');
expect(manifest.releaseTagCommit).toBe('fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab');
expect(manifest.sources.find((source) => source.name === 'WebKit')?.revision).toBe(
'6d0f3aac0b817cc01a846b3754b21271adedac12',
);
expect(manifest.sources.find((source) => source.name === 'tinycc')?.revision).toBe(
'29985a3b59898861442fa3b43f663fc1af2591d7',
);
});
test('parses commit and tag registrations from Bun CMake', () => {
const registrations = parseRegisteredRepositories(`
register_repository(
NAME tinycc
REPOSITORY oven-sh/tinycc
COMMIT
# A comment between the field and its value is valid CMake.
29985a3b59898861442fa3b43f663fc1af2591d7
)
register_repository(
NAME brotli
REPOSITORY google/brotli
TAG v1.1.0
)
`);
expect(registrations.get('tinycc')).toEqual({
repository: 'oven-sh/tinycc',
kind: 'commit',
reference: '29985a3b59898861442fa3b43f663fc1af2591d7',
});
expect(registrations.get('brotli')).toEqual({
repository: 'google/brotli',
kind: 'tag',
reference: 'v1.1.0',
});
});
test('rejects stale runtime or package-manager pins', async () => {
const manifest = parseSourceManifest(
JSON.parse(
await fs.readFile(path.join(projectRoot, 'build/bun-source-manifest.json'), 'utf8'),
),
);
const runtimeManifest = {
version: manifest.version,
bunRevision: manifest.bunRevision,
correspondingSourceAsset: manifest.archiveName,
};
expect(() =>
validateRuntimeAlignment(manifest, { packageManager: 'bun@1.3.5' }, runtimeManifest),
).not.toThrow();
expect(() =>
validateRuntimeAlignment(manifest, { packageManager: 'bun@1.3.6' }, runtimeManifest),
).toThrow('package.json must pin bun@1.3.5');
expect(() =>
validateRuntimeAlignment(
manifest,
{ packageManager: 'bun@1.3.5' },
{
...runtimeManifest,
correspondingSourceAsset: 'stale.tar.gz',
},
),
).toThrow('correspondingSourceAsset does not match');
});
test('rejects drift in CMake dependency and WebKit pins', async () => {
const manifest = parseSourceManifest(
JSON.parse(
await fs.readFile(path.join(projectRoot, 'build/bun-source-manifest.json'), 'utf8'),
),
);
const registrations = manifest.sources
.filter((source) => source.destination.startsWith('bun/vendor/') && source.name !== 'WebKit')
.map(
(source) => `register_repository(
NAME ${source.name}
REPOSITORY ${source.repository}
${source.upstreamReference ? 'TAG' : 'COMMIT'} ${source.upstreamReference ?? source.revision}
)`,
)
.join('\n');
expect(() =>
validateBunPins(
manifest,
[registrations],
'set(WEBKIT_VERSION 6d0f3aac0b817cc01a846b3754b21271adedac12)',
),
).not.toThrow();
expect(() =>
validateBunPins(
manifest,
[registrations.replace('29985a3b59898861442fa3b43f663fc1af2591d7', '0'.repeat(40))],
'set(WEBKIT_VERSION 6d0f3aac0b817cc01a846b3754b21271adedac12)',
),
).toThrow('revision mismatch for tinycc');
expect(() =>
validateBunPins(manifest, [registrations], `set(WEBKIT_VERSION ${'0'.repeat(40)})`),
).toThrow('WebKit revision does not match');
});
});
+389
View File
@@ -0,0 +1,389 @@
import { createHash, randomUUID } from 'node:crypto';
import { createReadStream, createWriteStream } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { pipeline } from 'node:stream/promises';
import { Readable } from 'node:stream';
import { promisify } from 'node:util';
import { execFile } from 'node:child_process';
const execFileAsync = promisify(execFile);
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
export const DEFAULT_PACKAGE_JSON_PATH = path.join(repoRoot, 'package.json');
export const DEFAULT_MANIFEST_PATH = path.join(repoRoot, 'build', 'bun-runtime-manifest.json');
export const DEFAULT_CACHE_DIR = path.join(repoRoot, '.tmp', 'bun-runtime');
export const DEFAULT_LICENSES_SOURCE_DIR = path.join(repoRoot, 'resources', 'bun', 'licenses');
export const STAGED_METADATA_FILE = 'metadata.json';
export const REQUIRED_LICENSE_FILES = [
'Bun-LICENSE.md',
'LGPL-2.0.txt',
'LGPL-2.1.txt',
'SOURCE.md',
'THIRD-PARTY-NOTICES.md',
];
const RELEASE_BASE_URL = 'https://github.com/oven-sh/bun/releases/download';
const SUPPORTED_PLATFORMS = new Set(['darwin', 'linux', 'win32']);
const ARCH_BY_BUILDER_VALUE = new Map([
[1, 'x64'],
[3, 'arm64'],
['x64', 'x64'],
['arm64', 'arm64'],
]);
function isRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function readRequiredString(record, key, source) {
const value = record[key];
if (typeof value !== 'string' || value.length === 0) {
throw new Error(`${source} must contain a non-empty ${key} string.`);
}
return value;
}
function readCommit(record, key, source) {
const value = readRequiredString(record, key, source);
if (!/^[a-f0-9]{40}$/.test(value)) {
throw new Error(`${source} must contain a 40-character ${key} commit.`);
}
return value;
}
export function normalizeTarget(platform, arch) {
if (!SUPPORTED_PLATFORMS.has(platform)) {
throw new Error(`Unsupported Bun runtime target platform: ${platform}`);
}
const normalizedArch = ARCH_BY_BUILDER_VALUE.get(arch);
if (!normalizedArch) {
throw new Error(`Unsupported Bun runtime target architecture for ${platform}: ${String(arch)}`);
}
if (platform === 'win32' && normalizedArch !== 'x64') {
throw new Error(`Unsupported Bun runtime target: ${platform}-${normalizedArch}`);
}
return {
platform,
arch: normalizedArch,
key: `${platform}-${normalizedArch}`,
executableName: platform === 'win32' ? 'bun.exe' : 'bun',
};
}
export function parsePackageManagerVersion(packageJson) {
if (!isRecord(packageJson)) {
throw new Error('package.json must contain a JSON object.');
}
const packageManager = readRequiredString(packageJson, 'packageManager', 'package.json');
const match = /^bun@(\d+\.\d+\.\d+)$/.exec(packageManager);
if (!match) {
throw new Error(
`package.json packageManager must pin Bun exactly, received ${packageManager}.`,
);
}
return match[1];
}
function parseArtifact(value, key) {
if (!isRecord(value)) {
throw new Error(`Bun runtime manifest artifact ${key} must be an object.`);
}
const file = readRequiredString(value, 'file', `Bun runtime manifest artifact ${key}`);
const sha256 = readRequiredString(value, 'sha256', `Bun runtime manifest artifact ${key}`);
if (!/^[a-f0-9]{64}$/.test(sha256)) {
throw new Error(`Bun runtime manifest artifact ${key} has an invalid SHA-256 digest.`);
}
if (path.basename(file) !== file || !file.endsWith('.zip')) {
throw new Error(`Bun runtime manifest artifact ${key} has an unsafe file name.`);
}
return { file, sha256 };
}
export function parseRuntimeManifest(manifest, version) {
if (!isRecord(manifest) || manifest.schemaVersion !== 1) {
throw new Error('Bun runtime manifest must use schemaVersion 1.');
}
const manifestVersion = readRequiredString(manifest, 'version', 'Bun runtime manifest');
if (manifestVersion !== version) {
throw new Error(
`Bun runtime manifest version ${manifestVersion} does not match packageManager bun@${version}.`,
);
}
if (!isRecord(manifest.artifacts)) {
throw new Error('Bun runtime manifest must contain an artifacts object.');
}
return {
version,
bunRevision: readCommit(manifest, 'bunRevision', 'Bun runtime manifest'),
releaseTagCommit: readCommit(manifest, 'releaseTagCommit', 'Bun runtime manifest'),
artifacts: manifest.artifacts,
licenseInventoryStatus: readRequiredString(
manifest,
'licenseInventoryStatus',
'Bun runtime manifest',
),
sourceManifest: readRequiredString(manifest, 'sourceManifest', 'Bun runtime manifest'),
correspondingSourceAsset: readRequiredString(
manifest,
'correspondingSourceAsset',
'Bun runtime manifest',
),
};
}
export async function loadRuntimeConfig({
packageJsonPath = DEFAULT_PACKAGE_JSON_PATH,
manifestPath = DEFAULT_MANIFEST_PATH,
} = {}) {
const [packageJsonText, manifestText] = await Promise.all([
fs.readFile(packageJsonPath, 'utf8'),
fs.readFile(manifestPath, 'utf8'),
]);
const version = parsePackageManagerVersion(JSON.parse(packageJsonText));
return parseRuntimeManifest(JSON.parse(manifestText), version);
}
export function resolveArtifact(config, platform, arch) {
const target = normalizeTarget(platform, arch);
const artifactValue = config.artifacts[target.key];
if (artifactValue === undefined) {
throw new Error(`Bun runtime manifest has no artifact for ${target.key}.`);
}
const artifact = parseArtifact(artifactValue, target.key);
return {
...target,
...artifact,
version: config.version,
url: `${RELEASE_BASE_URL}/bun-v${config.version}/${artifact.file}`,
};
}
export async function sha256File(filePath) {
const hash = createHash('sha256');
for await (const chunk of createReadStream(filePath)) {
hash.update(chunk);
}
return hash.digest('hex');
}
export async function ensureCachedArchive(
artifact,
{ cacheDir = DEFAULT_CACHE_DIR, fetchImpl = globalThis.fetch } = {},
) {
if (typeof fetchImpl !== 'function') {
throw new Error('No fetch implementation is available to download Bun.');
}
const versionCacheDir = path.join(cacheDir, artifact.version);
const archivePath = path.join(versionCacheDir, artifact.file);
await fs.mkdir(versionCacheDir, { recursive: true });
try {
if ((await sha256File(archivePath)) === artifact.sha256) return archivePath;
await fs.unlink(archivePath);
} catch (error) {
if (!isRecord(error) || error.code !== 'ENOENT') throw error;
}
const temporaryPath = `${archivePath}.download-${randomUUID()}`;
try {
const response = await fetchImpl(artifact.url);
if (!response.ok || !response.body) {
throw new Error(`Unable to download ${artifact.url}: HTTP ${response.status}`);
}
await pipeline(
Readable.fromWeb(response.body),
createWriteStream(temporaryPath, { flags: 'wx' }),
);
const actualSha256 = await sha256File(temporaryPath);
if (actualSha256 !== artifact.sha256) {
throw new Error(
`Bun archive checksum mismatch for ${artifact.file}: expected ${artifact.sha256}, received ${actualSha256}.`,
);
}
await fs.rename(temporaryPath, archivePath);
return archivePath;
} catch (error) {
await fs.rm(temporaryPath, { force: true });
throw error;
}
}
function isSafeZipEntry(entry) {
if (entry.startsWith('/') || /^[A-Za-z]:/.test(entry)) return false;
return !entry.replaceAll('\\', '/').split('/').includes('..');
}
function waitForProcess(child, description) {
let stderr = '';
child.stderr.setEncoding('utf8');
child.stderr.on('data', (chunk) => {
stderr += chunk;
});
return new Promise((resolve, reject) => {
child.once('error', reject);
child.once('close', (code) => {
if (code === 0) resolve();
else reject(new Error(`${description}: ${stderr.trim() || `process exited ${code}`}`));
});
});
}
export function buildWindowsExtractionCommand(archivePath, member, outputPath) {
const script = [
'Add-Type -AssemblyName System.IO.Compression.FileSystem',
'$archive = [IO.Compression.ZipFile]::OpenRead($env:SUBMINER_BUN_ARCHIVE_PATH)',
'try {',
' $entry = $archive.Entries | Where-Object { $_.FullName -ceq $env:SUBMINER_BUN_ARCHIVE_MEMBER }',
' if ($null -eq $entry) { throw "Archive member not found: $env:SUBMINER_BUN_ARCHIVE_MEMBER" }',
' $inputStream = $entry.Open()',
' $outputStream = [IO.File]::Create($env:SUBMINER_BUN_OUTPUT_PATH)',
' try { $inputStream.CopyTo($outputStream) } finally { $outputStream.Dispose(); $inputStream.Dispose() }',
'} finally { $archive.Dispose() }',
].join('; ');
return {
command: 'powershell.exe',
args: [
'-NoLogo',
'-NoProfile',
'-NonInteractive',
'-EncodedCommand',
Buffer.from(script, 'utf16le').toString('base64'),
],
environment: {
SUBMINER_BUN_ARCHIVE_PATH: archivePath,
SUBMINER_BUN_ARCHIVE_MEMBER: member,
SUBMINER_BUN_OUTPUT_PATH: outputPath,
},
};
}
async function extractZipMemberOnWindows(archivePath, member, outputPath) {
const command = buildWindowsExtractionCommand(archivePath, member, outputPath);
const powershell = spawn(command.command, command.args, {
env: { ...process.env, ...command.environment },
stdio: ['ignore', 'ignore', 'pipe'],
});
await waitForProcess(powershell, `Unable to extract ${member}`);
}
export async function extractZipMember(archivePath, member, outputPath) {
if (!isSafeZipEntry(member)) {
throw new Error(`Refusing to extract unsafe Bun archive member ${member}.`);
}
await fs.mkdir(path.dirname(outputPath), { recursive: true });
const temporaryPath = `${outputPath}.extract-${randomUUID()}`;
if (process.platform === 'win32') {
try {
await extractZipMemberOnWindows(archivePath, member, temporaryPath);
await fs.chmod(temporaryPath, 0o755);
await fs.rename(temporaryPath, outputPath);
} catch (error) {
await fs.rm(temporaryPath, { force: true });
throw error;
}
return;
}
const { stdout } = await execFileAsync('unzip', ['-Z1', archivePath], {
encoding: 'utf8',
maxBuffer: 1024 * 1024,
});
const entries = stdout.split(/\r?\n/).filter(Boolean);
if (entries.some((entry) => !isSafeZipEntry(entry))) {
throw new Error(`Bun archive ${archivePath} contains an unsafe path.`);
}
if (!entries.includes(member)) {
throw new Error(`Bun archive ${archivePath} does not contain ${member}.`);
}
const output = createWriteStream(temporaryPath, { flags: 'wx', mode: 0o755 });
const unzip = spawn('unzip', ['-p', archivePath, member], { stdio: ['ignore', 'pipe', 'pipe'] });
try {
await Promise.all([
pipeline(unzip.stdout, output),
waitForProcess(unzip, `Unable to extract ${member}`),
]);
await fs.chmod(temporaryPath, 0o755);
await fs.rename(temporaryPath, outputPath);
} catch (error) {
await fs.rm(temporaryPath, { force: true });
throw error;
}
}
export function resolveResourcesDirectory(appOutDir, platform, productFilename = 'SubMiner') {
if (platform !== 'darwin') return path.join(appOutDir, 'resources');
const appBundlePath = appOutDir.endsWith('.app')
? appOutDir
: path.join(appOutDir, `${productFilename}.app`);
return path.join(appBundlePath, 'Contents', 'Resources');
}
export async function stageBunLicenses(
runtimeDirectory,
licensesSourceDir = DEFAULT_LICENSES_SOURCE_DIR,
) {
const licensesDirectory = path.join(runtimeDirectory, 'licenses');
await Promise.all(
REQUIRED_LICENSE_FILES.map((fileName) => fs.access(path.join(licensesSourceDir, fileName))),
);
await fs.cp(licensesSourceDir, licensesDirectory, { recursive: true, force: true });
return licensesDirectory;
}
export async function stageBunRuntime(
{ appOutDir, platform, arch, productFilename = 'SubMiner' },
{
configLoader = loadRuntimeConfig,
archiveLoader = ensureCachedArchive,
extractor = extractZipMember,
licenseStager = stageBunLicenses,
} = {},
) {
const config = await configLoader();
const artifact = resolveArtifact(config, platform, arch);
const archivePath = await archiveLoader(artifact);
const archiveDirectory = path.basename(artifact.file, '.zip');
const archiveExecutableName = platform === 'win32' ? 'bun.exe' : 'bun';
const member = `${archiveDirectory}/${archiveExecutableName}`;
const runtimeDirectory = path.join(
resolveResourcesDirectory(appOutDir, platform, productFilename),
'bun',
);
const executablePath = path.join(runtimeDirectory, artifact.executableName);
await extractor(archivePath, member, executablePath);
await licenseStager(runtimeDirectory);
const metadata = {
name: 'Bun',
version: config.version,
bunRevision: config.bunRevision,
releaseTagCommit: config.releaseTagCommit,
target: artifact.key,
artifact: artifact.file,
artifactSha256: artifact.sha256,
sourceUrl: artifact.url,
licenseInventoryStatus: config.licenseInventoryStatus,
correspondingSourceAsset: config.correspondingSourceAsset,
sourceInstructions: 'licenses/SOURCE.md',
thirdPartyNotices: 'licenses/THIRD-PARTY-NOTICES.md',
};
await fs.writeFile(
path.join(runtimeDirectory, STAGED_METADATA_FILE),
`${JSON.stringify(metadata, null, 2)}\n`,
{ mode: 0o644 },
);
return {
executablePath,
metadataPath: path.join(runtimeDirectory, STAGED_METADATA_FILE),
artifact,
};
}
+307
View File
@@ -0,0 +1,307 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
buildWindowsExtractionCommand,
ensureCachedArchive,
extractZipMember,
loadRuntimeConfig,
normalizeTarget,
parsePackageManagerVersion,
parseRuntimeManifest,
resolveArtifact,
stageBunLicenses,
stageBunRuntime,
} from './stage-bun-runtime.mjs';
function sha256(content: Uint8Array): string {
return createHash('sha256').update(content).digest('hex');
}
function runtimeConfig() {
return {
version: '1.3.5',
bunRevision: '1e86cebd74a5723e818b5c0555276b646bcf0e4c',
releaseTagCommit: 'fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab',
artifacts: {
'darwin-arm64': { file: 'bun-darwin-aarch64.zip', sha256: '1'.repeat(64) },
'darwin-x64': { file: 'bun-darwin-x64-baseline.zip', sha256: '2'.repeat(64) },
'linux-arm64': { file: 'bun-linux-aarch64.zip', sha256: '3'.repeat(64) },
'linux-x64': { file: 'bun-linux-x64-baseline.zip', sha256: '4'.repeat(64) },
'win32-x64': { file: 'bun-windows-x64-baseline.zip', sha256: '5'.repeat(64) },
},
licenseInventoryStatus: 'complete-for-bun-1.3.5-declared-linked-libraries',
sourceManifest: 'build/bun-source-manifest.json',
correspondingSourceAsset: 'bun-v1.3.5-source.tar.gz',
};
}
test('normalizeTarget maps each supported electron-builder target without using the host', () => {
assert.deepEqual(normalizeTarget('linux', 1), {
platform: 'linux',
arch: 'x64',
key: 'linux-x64',
executableName: 'bun',
});
assert.equal(normalizeTarget('linux', 3).key, 'linux-arm64');
assert.equal(normalizeTarget('darwin', 'x64').key, 'darwin-x64');
assert.equal(normalizeTarget('darwin', 'arm64').key, 'darwin-arm64');
assert.deepEqual(normalizeTarget('win32', 1), {
platform: 'win32',
arch: 'x64',
key: 'win32-x64',
executableName: 'bun.exe',
});
assert.throws(() => normalizeTarget('freebsd', 'x64'), /Unsupported Bun runtime target platform/);
assert.throws(() => normalizeTarget('win32', 'arm64'), /Unsupported Bun runtime target/);
assert.throws(() => normalizeTarget('linux', 0), /Unsupported Bun runtime target architecture/);
});
test('resolveArtifact chooses baseline x64 builds and standard arm64 builds', () => {
const config = runtimeConfig();
assert.equal(resolveArtifact(config, 'linux', 'x64').file, 'bun-linux-x64-baseline.zip');
assert.equal(resolveArtifact(config, 'darwin', 'x64').file, 'bun-darwin-x64-baseline.zip');
assert.equal(resolveArtifact(config, 'win32', 'x64').file, 'bun-windows-x64-baseline.zip');
assert.equal(resolveArtifact(config, 'linux', 'arm64').file, 'bun-linux-aarch64.zip');
assert.equal(resolveArtifact(config, 'darwin', 'arm64').file, 'bun-darwin-aarch64.zip');
});
test('tracked runtime manifest covers every supported target at the packageManager version', async () => {
const config = await loadRuntimeConfig();
assert.equal(config.version, '1.3.5');
for (const [platform, arch] of [
['linux', 'x64'],
['linux', 'arm64'],
['darwin', 'x64'],
['darwin', 'arm64'],
['win32', 'x64'],
]) {
const artifact = resolveArtifact(config, platform, arch);
assert.match(artifact.sha256, /^[a-f0-9]{64}$/);
assert.equal(
artifact.url,
`https://github.com/oven-sh/bun/releases/download/bun-v1.3.5/${artifact.file}`,
);
}
});
test('manifest version must match the exact packageManager Bun pin', () => {
assert.equal(parsePackageManagerVersion({ packageManager: 'bun@1.3.5' }), '1.3.5');
assert.throws(
() => parsePackageManagerVersion({ packageManager: 'bun@^1.3.5' }),
/must pin Bun exactly/,
);
assert.throws(
() =>
parseRuntimeManifest(
{
schemaVersion: 1,
version: '1.3.6',
bunRevision: '1e86cebd74a5723e818b5c0555276b646bcf0e4c',
releaseTagCommit: 'fa5a5bbe556a4bda5bde77b4013aa6c3bb4ec9ab',
artifacts: {},
licenseInventoryStatus: 'partial',
sourceManifest: 'build/bun-source-manifest.json',
correspondingSourceAsset: 'bun-v1.3.5-source.tar.gz',
},
'1.3.5',
),
/does not match packageManager bun@1\.3\.5/,
);
});
test('ensureCachedArchive verifies downloads and reuses a verified cache entry offline', async () => {
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'subminer-bun-cache-'));
const bytes = new TextEncoder().encode('verified Bun archive fixture');
const artifact = {
version: '1.3.5',
file: 'bun-linux-x64-baseline.zip',
sha256: sha256(bytes),
url: 'https://example.invalid/bun.zip',
};
let downloads = 0;
const fetchImpl = async () => {
downloads += 1;
return new Response(bytes);
};
try {
const firstPath = await ensureCachedArchive(artifact, { cacheDir: workspace, fetchImpl });
const secondPath = await ensureCachedArchive(artifact, {
cacheDir: workspace,
fetchImpl: async () => {
throw new Error('verified cache should not fetch');
},
});
assert.equal(firstPath, secondPath);
assert.equal(downloads, 1);
assert.deepEqual(await fs.readFile(firstPath), Buffer.from(bytes));
} finally {
await fs.rm(workspace, { recursive: true, force: true });
}
});
test('ensureCachedArchive rejects a checksum mismatch without caching the download', async () => {
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'subminer-bun-cache-mismatch-'));
const artifact = {
version: '1.3.5',
file: 'bun-linux-x64-baseline.zip',
sha256: '0'.repeat(64),
url: 'https://example.invalid/bun.zip',
};
try {
await assert.rejects(
ensureCachedArchive(artifact, {
cacheDir: workspace,
fetchImpl: async () => new Response('tampered'),
}),
/checksum mismatch/,
);
const cacheEntries = await fs.readdir(path.join(workspace, '1.3.5'));
assert.deepEqual(cacheEntries, []);
} finally {
await fs.rm(workspace, { recursive: true, force: true });
}
});
test('extractZipMember extracts only the requested path and makes the runtime executable', async () => {
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'subminer-bun-extract-'));
const archivePath = path.join(workspace, 'fixture.zip');
const outputPath = path.join(workspace, 'output', 'bun');
const archiveBase64 =
'UEsDBAoAAAAAAMxcKl3rs337EgAAABIAAAAaABwAYnVuLWxpbnV4LXg2NC1iYXNlbGluZS9idW5VVAkAAyD5omog+aJqdXgLAAEE6AMAAAToAwAAYnVuIGZpeHR1cmUgYmluYXJ5UEsBAh4DCgAAAAAAzFwqXeuzffsSAAAAEgAAABoAGAAAAAAAAQAAAKSBAAAAAGJ1bi1saW51eC14NjQtYmFzZWxpbmUvYnVuVVQFAAMg+aJqdXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAYAAAAGYAAAAAAA==';
try {
await fs.writeFile(archivePath, Buffer.from(archiveBase64, 'base64'));
await extractZipMember(archivePath, 'bun-linux-x64-baseline/bun', outputPath);
assert.equal(await fs.readFile(outputPath, 'utf8'), 'bun fixture binary');
assert.equal((await fs.stat(outputPath)).mode & 0o777, 0o755);
await assert.rejects(
extractZipMember(archivePath, '../bun', path.join(workspace, 'unsafe')),
/unsafe Bun archive member/,
);
} finally {
await fs.rm(workspace, { recursive: true, force: true });
}
});
test('Windows extraction keeps paths and archive members out of PowerShell command text', () => {
const archivePath = String.raw`C:\Release Builds\bun $(archive) '1.3.5'.zip`;
const member = 'bun-windows-x64-baseline/bun.exe';
const outputPath = String.raw`C:\Staged App & Tools\resources\bun\bun.exe`;
const command = buildWindowsExtractionCommand(archivePath, member, outputPath);
const encodedCommand = command.args.at(-1);
assert.equal(command.command, 'powershell.exe');
assert.equal(command.args.at(-2), '-EncodedCommand');
assert.ok(encodedCommand);
const script = Buffer.from(encodedCommand, 'base64').toString('utf16le');
assert.match(script, /\$env:SUBMINER_BUN_ARCHIVE_PATH/);
assert.match(script, /\$env:SUBMINER_BUN_ARCHIVE_MEMBER/);
assert.match(script, /\$env:SUBMINER_BUN_OUTPUT_PATH/);
assert.doesNotMatch(script, /Release Builds|Staged App|bun-windows-x64-baseline/);
assert.deepEqual(command.environment, {
SUBMINER_BUN_ARCHIVE_PATH: archivePath,
SUBMINER_BUN_ARCHIVE_MEMBER: member,
SUBMINER_BUN_OUTPUT_PATH: outputPath,
});
});
test('stageBunLicenses copies the tracked inventory into resources/bun/licenses', async () => {
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'subminer-bun-licenses-'));
const sourceDirectory = path.join(workspace, 'tracked-licenses');
const runtimeDirectory = path.join(workspace, 'resources', 'bun');
try {
await fs.mkdir(sourceDirectory, { recursive: true });
for (const fileName of [
'Bun-LICENSE.md',
'LGPL-2.0.txt',
'LGPL-2.1.txt',
'SOURCE.md',
'THIRD-PARTY-NOTICES.md',
]) {
await fs.writeFile(path.join(sourceDirectory, fileName), `${fileName}\n`);
}
const licensesDirectory = await stageBunLicenses(runtimeDirectory, sourceDirectory);
assert.equal(licensesDirectory, path.join(runtimeDirectory, 'licenses'));
assert.equal(
await fs.readFile(path.join(licensesDirectory, 'THIRD-PARTY-NOTICES.md'), 'utf8'),
'THIRD-PARTY-NOTICES.md\n',
);
} finally {
await fs.rm(workspace, { recursive: true, force: true });
}
});
test('stageBunRuntime places target executables and metadata in app resources with safe modes', async () => {
const workspace = await fs.mkdtemp(path.join(os.tmpdir(), 'subminer-bun-stage-'));
const cases = [
{
platform: 'linux',
arch: 'x64',
appOutDir: path.join(workspace, 'linux'),
relativeExecutable: path.join('resources', 'bun', 'bun'),
member: 'bun-linux-x64-baseline/bun',
},
{
platform: 'darwin',
arch: 'arm64',
appOutDir: path.join(workspace, 'darwin'),
relativeExecutable: path.join('SubMiner.app', 'Contents', 'Resources', 'bun', 'bun'),
member: 'bun-darwin-aarch64/bun',
},
{
platform: 'win32',
arch: 'x64',
appOutDir: path.join(workspace, 'windows'),
relativeExecutable: path.join('resources', 'bun', 'bun.exe'),
member: 'bun-windows-x64-baseline/bun.exe',
},
] as const;
try {
for (const targetCase of cases) {
let extractedMember = '';
const result = await stageBunRuntime(targetCase, {
configLoader: async () => runtimeConfig(),
archiveLoader: async () => path.join(workspace, 'fixture.zip'),
extractor: async (_archivePath: string, member: string, outputPath: string) => {
extractedMember = member;
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, 'bun fixture', { mode: 0o755 });
},
licenseStager: async (runtimeDirectory: string) => {
const licensesDirectory = path.join(runtimeDirectory, 'licenses');
await fs.mkdir(licensesDirectory, { recursive: true });
await fs.writeFile(path.join(licensesDirectory, 'Bun-LICENSE.md'), 'MIT fixture');
return licensesDirectory;
},
});
const expectedExecutable = path.join(targetCase.appOutDir, targetCase.relativeExecutable);
assert.equal(result.executablePath, expectedExecutable);
assert.equal(extractedMember, targetCase.member);
assert.equal((await fs.stat(expectedExecutable)).mode & 0o777, 0o755);
assert.equal((await fs.stat(result.metadataPath)).mode & 0o777, 0o644);
const metadata = JSON.parse(await fs.readFile(result.metadataPath, 'utf8'));
assert.equal(metadata.version, '1.3.5');
assert.equal(metadata.bunRevision, '1e86cebd74a5723e818b5c0555276b646bcf0e4c');
assert.equal(metadata.artifactSha256, result.artifact.sha256);
assert.equal(metadata.target, result.artifact.key);
assert.equal(
await fs.readFile(
path.join(path.dirname(expectedExecutable), 'licenses', 'Bun-LICENSE.md'),
'utf8',
),
'MIT fixture',
);
}
} finally {
await fs.rm(workspace, { recursive: true, force: true });
}
});