| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import { execFile, spawn } from 'node:child_process'; | |
| import { constants } from 'node:fs'; | |
| import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; | |
| import { tmpdir } from 'node:os'; | |
| import { basename, dirname, join, resolve } from 'node:path'; | |
| import { pathToFileURL } from 'node:url'; | |
| import { parseArgs, promisify } from 'node:util'; | |
| import { | |
| controlledProcessEnvironment, | |
| execTar, | |
| verifySourceCandidate, | |
| } from '../asf-source-release.mjs'; | |
| import { npmSpawnOptions } from '../npm-spawn.mjs'; | |
| import { parseProductReleaseVersion } from '../release-version.mjs'; | |
| import { buildCli, desktopShell, rustTarget } from './build-cli.mjs'; | |
| import { nativeCliTargets, packNativeCli } from './pack-cli.mjs'; | |
| import { verifyNotices } from './notices.mjs'; | |
| import { buildV8 } from './build-v8.mjs'; | |
| const run = promisify(execFile); | |
| export function previewVersion(sourceVersion, buildId) { | |
| if ( | |
| parseProductReleaseVersion(sourceVersion).prerelease.length > 0 || | |
| typeof buildId !== 'string' || | |
| !buildId | |
| ) { | |
| throw new Error('A stable source version and a SemVer build-id are required'); | |
| } | |
| const version = `${sourceVersion}-rust-preview.${buildId}`; | |
| if (version.length > 256) throw new Error('Native preview version exceeds 256 characters'); | |
| parseProductReleaseVersion(version); | |
| return version; | |
| } | |
| /** Build only the verified archive, never caller-supplied executable bytes. Does not publish. */ | |
| export async function releaseNativeCli({ source, keys, target, validator, output, buildId }) { | |
| const platform = Object.hasOwn(nativeCliTargets, target) ? nativeCliTargets[target] : undefined; | |
| if (!platform || !source || !output || !buildId) { | |
| throw new Error('source, supported target, output, and build-id are required'); | |
| } | |
| const native = platform.os === process.platform && platform.cpu === process.arch; | |
| if (!native && !validator) throw new Error('Cross-target packaging requires a local validator'); | |
| const stage = await mkdtemp(join(tmpdir(), 'maka-native-source-')); | |
| try { | |
| // Snapshot before validation so later changes to the input cannot change the build. | |
| const archivePath = join(stage, basename(source)); | |
| for (const suffix of ['', '.sha512', ...(keys ? ['.asc'] : [])]) { | |
| await copyFile(resolve(source) + suffix, archivePath + suffix, constants.COPYFILE_EXCL); | |
| } | |
| const candidate = await verifySourceCandidate({ | |
| archivePath, | |
| ...(keys ? { keysPath: resolve(keys) } : {}), | |
| }); | |
| const version = previewVersion(candidate.version, buildId); | |
| const extraction = join(stage, 'source'); | |
| await mkdir(extraction); | |
| execTar(archivePath, ['-xzf', '-C', extraction], { | |
| timeout: 180_000, | |
| windowsHide: true, | |
| }); | |
| const repositoryRoot = join(extraction, candidate.rootDirectory); | |
| // JS must come from this install, not an ambient MAKA_JS_DEPS checkout. | |
| const env = controlledProcessEnvironment({ | |
| excludedPrefixes: ['BINDGEN_EXTRA_CLANG_ARGS'], | |
| excludedNames: [ | |
| 'MAKA_JS_DEPS', | |
| 'CARGO_BUILD_TARGET', | |
| 'MAKA_NATIVE_PACKAGE_VERSION', | |
| 'RUSTY_V8_ARCHIVE', | |
| 'RUSTY_V8_SRC_BINDING_PATH', | |
| 'RUSTY_V8_MIRROR', | |
| 'V8_FROM_SOURCE', | |
| 'V8_FORCE_DEBUG', | |
| 'GN_ARGS', | |
| 'EXTRA_GN_ARGS', | |
| 'DISABLE_CLANG', | |
| 'CLANG_BASE_PATH', | |
| ], | |
| overrides: { | |
| MAKA_JS_DEPS: repositoryRoot, | |
| MAKA_NATIVE_PACKAGE_VERSION: version, | |
| // cargo-config2 (used by zigbuild) treats an empty config wrapper as an | |
| // executable, but honors Cargo's empty environment override correctly. | |
| RUSTC_WRAPPER: '', | |
| RUSTC_WORKSPACE_WRAPPER: '', | |
| }, | |
| }); | |
| const metadata = JSON.parse( | |
| ( | |
| await run('cargo', ['metadata', '--locked', '--no-deps', '--format-version', '1'], { | |
| cwd: repositoryRoot, | |
| env, | |
| encoding: 'utf8', | |
| maxBuffer: 4 * 1024 * 1024, | |
| timeout: 180_000, | |
| windowsHide: true, | |
| }) | |
| ).stdout, | |
| ); | |
| const cli = metadata.packages.find((pkg) => pkg.name === 'maka-cli'); | |
| if (cli?.version !== candidate.version) { | |
| throw new Error('Native CLI version does not match the source archive'); | |
| } | |
| const reviewedNotices = await verifyNotices(repositoryRoot); | |
| if (platform.os === 'darwin') { | |
| const sdk = ( | |
| await run('xcrun', ['--sdk', 'macosx', '--show-sdk-version'], { env }) | |
| ).stdout.trim(); | |
| if (!(Number.parseInt(sdk, 10) >= 26)) | |
| throw new Error('Native macOS packaging requires Xcode 26 or newer; selected SDK: ' + sdk); | |
| } | |
| // Apply build-time dependency patches without running package lifecycle hooks. | |
| await execute( | |
| 'npm', | |
| ['ci', '--include=dev', '--include=optional', '--ignore-scripts', '--no-audit', '--no-fund'], | |
| npmSpawnOptions({ cwd: repositoryRoot, env }), | |
| ); | |
| await execute(process.execPath, ['scripts/apply-dependency-patches.mjs'], { | |
| cwd: repositoryRoot, | |
| env, | |
| }); | |
| const v8 = await buildV8({ | |
| root: repositoryRoot, | |
| directory: join(stage, 'v8'), | |
| cache: resolve(env.CARGO_TARGET_DIR || 'target', 'v8'), | |
| target: rustTarget(platform.os, platform.cpu), | |
| env, | |
| metadata: JSON.parse( | |
| ( | |
| await run( | |
| 'cargo', | |
| [ | |
| 'metadata', | |
| '--locked', | |
| '--format-version', | |
| '1', | |
| '--filter-platform', | |
| rustTarget(platform.os, platform.cpu), | |
| ], | |
| { | |
| cwd: repositoryRoot, | |
| env, | |
| maxBuffer: 16 * 1024 * 1024, | |
| timeout: 600_000, | |
| }, | |
| ) | |
| ).stdout, | |
| ), | |
| }); | |
| const notices = join(stage, 'THIRD_PARTY_NOTICES.txt'); | |
| const sysroot = ( | |
| await run('rustc', ['--print', 'sysroot'], { cwd: repositoryRoot, env }) | |
| ).stdout.trim(); | |
| const rustNotices = await readFile( | |
| join(sysroot, 'share/doc/rust/COPYRIGHT-library.html'), | |
| 'utf8', | |
| ); | |
| await writeFile( | |
| notices, | |
| [await readFile(reviewedNotices, 'utf8'), v8.notices, rustNotices].join('\n\n'), | |
| ); | |
| const binary = await buildCli({ | |
| release: true, | |
| target: rustTarget(platform.os, platform.cpu), | |
| repositoryRoot, | |
| env: { ...env, ...v8.environment }, | |
| }); | |
| if (native) { | |
| const { stdout } = await run( | |
| join(dirname(binary), desktopShell(platform.os)), | |
| ['--version'], | |
| { | |
| timeout: 15_000, | |
| windowsHide: true, | |
| }, | |
| ); | |
| if (stdout.trim() !== 'maka-desktop-shell ' + candidate.version) | |
| throw new Error('Built desktop shell reports another source version'); | |
| const terminal = await run(binary, ['terminal-shell', '--version'], { | |
| timeout: 15_000, | |
| windowsHide: true, | |
| }); | |
| if (!terminal.stdout.trim().endsWith(' ' + candidate.version)) | |
| throw new Error('Built terminal shell reports another source version'); | |
| const worker = join( | |
| dirname(binary), | |
| platform.os === 'win32' ? 'maka-python-worker.exe' : 'maka-python-worker', | |
| ); | |
| const workerVersion = await run(worker, ['--version'], { | |
| timeout: 15_000, | |
| windowsHide: true, | |
| }); | |
| if (workerVersion.stdout.trim() !== 'maka-python-worker ' + candidate.version) | |
| throw new Error('Built Python worker reports another source version'); | |
| const executables = [ | |
| binary, | |
| ...(platform.os === 'win32' ? [join(dirname(binary), 'maka-service.exe')] : []), | |
| ]; | |
| for (const [index, executable] of executables.entries()) { | |
| const { stdout } = await run(executable, ['--version'], { | |
| timeout: 15_000, | |
| windowsHide: true, | |
| }); | |
| if (stdout.trim() !== 'maka ' + candidate.version) | |
| throw new Error('Built CLI reports another source version'); | |
| await verifyCode(executable, join(stage, 'smoke-' + index + '.sqlite')); | |
| } | |
| } | |
| return await packNativeCli({ | |
| target, | |
| version, | |
| binary, | |
| notices, | |
| validator: validator ?? binary, | |
| output, | |
| repositoryRoot, | |
| source: { archive: basename(source), version: candidate.version, sha512: candidate.digest }, | |
| }); | |
| } finally { | |
| await rm(stage, { recursive: true, force: true }); | |
| } | |
| } | |
| async function verifyCode(executable, log) { | |
| const stdout = await new Promise((resolveOutput, reject) => { | |
| const child = execFile( | |
| executable, | |
| ['code', '--log', log], | |
| { encoding: 'utf8', timeout: 30_000, maxBuffer: 1024 * 1024, windowsHide: true }, | |
| (error, stdout) => (error ? reject(error) : resolveOutput(stdout)), | |
| ); | |
| child.stdin.end( | |
| 'if (Math.abs(Math.sin(1) - 0.8414709848078965) > 1e-15 || Math.cos(0) !== 1 || !new Intl.DateTimeFormat("en").format(new Date(0)) || Temporal.PlainDate.from("2026-09-28").year !== 2026) throw new Error("V8 smoke failed");', | |
| ); | |
| child.stdin.on('error', reject); | |
| }); | |
| if (JSON.parse(stdout).output?.ok !== true) throw new Error('Built CLI failed its real V8 smoke'); | |
| } | |
| async function execute(command, args, options) { | |
| await new Promise((resolveCommand, reject) => { | |
| const child = spawn(command, args, { ...options, stdio: 'inherit', windowsHide: true }); | |
| child.once('error', reject); | |
| child.once('exit', (code, signal) => { | |
| if (code === 0) resolveCommand(); | |
| else reject(new Error('Native source build failed: ' + command + ' ' + (signal ?? code))); | |
| }); | |
| }); | |
| } | |
| if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { | |
| const { values } = parseArgs({ | |
| options: Object.fromEntries( | |
| ['source', 'keys', 'target', 'validator', 'output', 'build-id'].map((name) => [ | |
| name, | |
| { type: 'string' }, | |
| ]), | |
| ), | |
| }); | |
| console.log(JSON.stringify(await releaseNativeCli({ ...values, buildId: values['build-id'] }))); | |
| } |