/* * 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 { createHash, randomBytes } from 'node:crypto'; import { createReadStream } from 'node:fs'; import { access, chmod, copyFile, cp, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile, } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; import { resolveProductReleaseIdentity } from './product-release-identity.mjs'; import { isMakaDevelopmentArtifact, isThirdPartyDevelopmentArtifact, releaseNpmEnvironment, resolveReleaseWorkspacePackages, resolveWorkspaceReleaseFiles, workspaceReleaseManifest, } from './release-cli-file-policy.mjs'; const execFileAsync = promisify(execFile); const requireFromHere = createRequire(import.meta.url); const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); const releaseDirectory = join(repoRoot, 'apps', 'desktop', 'release'); const dependencyPatchesDirectory = join(repoRoot, 'patches'); const cliPackageName = 'maka-agent'; const requiredSigningEnvironment = [ 'CSC_LINK', 'CSC_KEY_PASSWORD', 'APPLE_API_KEY', 'APPLE_API_KEY_ID', 'APPLE_API_ISSUER', ]; export const OFFICIAL_NODE_RUNTIME_ENTITLEMENTS = Object.freeze([ 'com.apple.security.cs.allow-dyld-environment-variables', 'com.apple.security.cs.allow-jit', 'com.apple.security.cs.allow-unsigned-executable-memory', 'com.apple.security.cs.disable-executable-page-protection', 'com.apple.security.cs.disable-library-validation', 'com.apple.security.get-task-allow', ]); // The official archive is validated verbatim above, but Apple rejects // distribution software that keeps get-task-allow. Re-sign only the Node // executable with the same runtime capabilities minus that development grant. export const DISTRIBUTION_NODE_RUNTIME_ENTITLEMENTS = Object.freeze( OFFICIAL_NODE_RUNTIME_ENTITLEMENTS.filter( (entitlement) => entitlement !== 'com.apple.security.get-task-allow', ), ); export function runCommand(command, args, options = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(command, args, { cwd: options.cwd ?? repoRoot, env: options.env ?? process.env, stdio: 'inherit', }); child.once('error', reject); child.once('exit', (code, signal) => { if (code === 0) { resolvePromise(); return; } reject( new Error( `${command} ${(options.displayArgs ?? args).join(' ')} failed with ${ signal ? `signal ${signal}` : `exit code ${code}` }`, ), ); }); }); } function inspectCommand(command, args, options = {}) { return execFileAsync(command, args, { cwd: options.cwd ?? repoRoot, env: options.env ?? process.env, maxBuffer: options.maxBuffer ?? 20 * 1024 * 1024, timeout: options.timeout ?? 30_000, }); } export function assertMacosArm64CliHost(platform = process.platform, arch = process.arch) { if (platform !== 'darwin' || arch !== 'arm64') { throw new Error('CLI release packaging requires an Apple Silicon macOS host.'); } } export function resolveMacosArm64CliArtifactPaths(version) { const archiveName = `Maka-${version}-cli-mac-arm64.zip`; return { archiveRootName: `Maka-${version}-cli-mac-arm64`, archivePath: join(releaseDirectory, archiveName), checksumPath: join(releaseDirectory, `${archiveName}.sha256`), }; } export function macosArm64CliWrapper() { return `#!/bin/sh set -eu launcher=$0 while [ -L "$launcher" ]; do link_dir=$(CDPATH= cd -P "$(dirname "$launcher")" && pwd) link_target=$(readlink "$launcher") case "$link_target" in /*) launcher=$link_target ;; *) launcher=$link_dir/$link_target ;; esac done bin_dir=$(CDPATH= cd -P "$(dirname "$launcher")" && pwd) libexec_dir=$(CDPATH= cd -P "$bin_dir/../libexec" && pwd) MAKA_EVAL_MAKA_BUNDLE_PATH=$libexec_dir export MAKA_EVAL_MAKA_BUNDLE_PATH exec "$libexec_dir/node/bin/node" "$libexec_dir/node_modules/maka-agent/dist/cli.js" "$@" `; } export function macosArm64CliInstallArgs() { return [ 'ci', '--omit=dev', '--workspace', cliPackageName, '--include-workspace-root=false', '--ignore-scripts', '--no-audit', '--no-fund', ]; } export function standaloneInstallEnvironment(environment) { return releaseNpmEnvironment(environment, join(repoRoot, '.npmrc')); } export async function resolveCliWorkspacePackages() { return resolveReleaseWorkspacePackages(repoRoot, cliPackageName); } export async function pruneThirdPartyDevelopmentArtifacts(directory, root = directory) { for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); const relativePath = relative(root, path); if (isThirdPartyDevelopmentArtifact(relativePath)) { await rm(path, { recursive: entry.isDirectory(), force: true }); } else if (entry.isDirectory()) { await pruneThirdPartyDevelopmentArtifacts(path, root); } } } export function standaloneInstallRootManifest(rootManifest, workspacePackages) { const { allowScripts: _allowScripts, ...staged } = rootManifest; return { ...staged, workspaces: workspacePackages.map(({ workspacePath }) => workspacePath), }; } async function sha256File(path) { const hash = createHash('sha256'); for await (const chunk of createReadStream(path)) hash.update(chunk); return hash.digest('hex'); } export async function stageWorkspacePackages(installRoot, workspacePackages) { const rootManifest = JSON.parse(await readFile(join(repoRoot, 'package.json'), 'utf8')); await Promise.all([ writeFile( join(installRoot, 'package.json'), `${JSON.stringify(standaloneInstallRootManifest(rootManifest, workspacePackages), null, 2)}\n`, 'utf8', ), copyFile(join(repoRoot, 'package-lock.json'), join(installRoot, 'package-lock.json')), ...workspacePackages.map(async ({ directory, manifest, workspacePath }) => { const targetDirectory = join(installRoot, workspacePath); await mkdir(targetDirectory, { recursive: true }); await writeFile( join(targetDirectory, 'package.json'), `${JSON.stringify(workspaceReleaseManifest(manifest), null, 2)}\n`, ); await Promise.all( resolveWorkspaceReleaseFiles(directory, manifest).map(async (releaseFile) => { const source = join(directory, ...releaseFile.split('/')); const target = join(targetDirectory, ...releaseFile.split('/')); await mkdir(dirname(target), { recursive: true }); await cp(source, target, { recursive: true, filter: (path) => !isMakaDevelopmentArtifact(relative(directory, path)), }); }), ); }), ]); } export async function listDependencyPatchNames() { let entries; try { entries = await readdir(dependencyPatchesDirectory, { withFileTypes: true }); } catch (error) { if (error?.code === 'ENOENT') return []; throw error; } return entries .filter((entry) => entry.isFile() && entry.name.endsWith('.patch')) .map((entry) => entry.name) .sort(); } export async function dependencyPatchPackageName(patchName) { const patch = await readFile(join(dependencyPatchesDirectory, patchName), 'utf8'); const target = patch.match(/^diff --git a\/node_modules\/((?:@[^/]+\/)?[^/]+)\//m)?.[1]; if (!target) throw new Error(`Cannot resolve the target package for patch ${patchName}.`); return target; } export async function listApplicableDependencyPatchNames(nodeModulesDirectory) { const applicable = []; for (const patchName of await listDependencyPatchNames()) { const packageName = await dependencyPatchPackageName(patchName); try { await access(packageModulePath(nodeModulesDirectory, packageName)); applicable.push(patchName); } catch (error) { if (error?.code !== 'ENOENT') throw error; } } return applicable; } export async function applyDependencyPatches( installRoot, { env = process.env, run = runCommand, patchPackageEntry } = {}, ) { const patchNames = await listApplicableDependencyPatchNames(join(installRoot, 'node_modules')); if (patchNames.length === 0) return patchNames; const stagedPatchesDirectory = join(installRoot, 'patches'); await mkdir(stagedPatchesDirectory, { recursive: true }); await Promise.all( patchNames.map((name) => copyFile(join(dependencyPatchesDirectory, name), join(stagedPatchesDirectory, name)), ), ); const entry = patchPackageEntry ?? requireFromHere.resolve('patch-package/index.js'); await run(process.execPath, [entry, '--error-on-fail'], { cwd: installRoot, env }); return patchNames; } function packageModulePath(nodeModulesDirectory, packageName) { return join(nodeModulesDirectory, ...packageName.split('/')); } export async function assertNoDanglingSymlinks(directory, rootDirectory = directory) { const resolvedRoot = await realpath(rootDirectory); for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isSymbolicLink()) { try { const target = await realpath(path); const targetFromRoot = relative(resolvedRoot, target); if ( targetFromRoot === '..' || targetFromRoot.startsWith(`..${sep}`) || isAbsolute(targetFromRoot) ) { throw new Error(`Symlink escapes the CLI artifact: ${path}`); } } catch (error) { if (error?.code === 'ENOENT') throw new Error(`Dangling symlink in CLI artifact: ${path}`); throw error; } } else if (entry.isDirectory()) { await assertNoDanglingSymlinks(path, rootDirectory); } } } async function assertWorkspaceLinks(archiveRoot, workspacePackages) { const nodeModulesDirectory = join(archiveRoot, 'libexec', 'node_modules'); for (const { name, workspacePath } of workspacePackages) { const linkTarget = await realpath(packageModulePath(nodeModulesDirectory, name)); const packageTarget = await realpath(join(archiveRoot, 'libexec', workspacePath)); if (linkTarget !== packageTarget) { throw new Error(`${name} does not resolve to its staged workspace package.`); } } await assertNoDanglingSymlinks(join(archiveRoot, 'libexec')); } function parseLinkedLibraries(output) { return output .split('\n') .slice(1) .map((line) => line.trim().split(/\s+/)[0]) .filter(Boolean); } export function assertOfficialNodeRuntime({ actualVersion, expectedVersion, architectures, signature, linkedLibraries, }) { if (actualVersion !== expectedVersion) { throw new Error(`CLI release requires Node ${expectedVersion}, found ${actualVersion}.`); } const architectureList = architectures.trim().split(/\s+/).filter(Boolean); if (architectureList.length !== 1 || architectureList[0] !== 'arm64') { throw new Error( `CLI Node runtime must contain only arm64, found ${architectureList.join(', ')}.`, ); } if (!signature.includes('Authority=Developer ID Application: Node.js Foundation (HX7739G8FX)')) { throw new Error('CLI release requires the official Node.js Foundation runtime.'); } if (!signature.includes('flags=0x10000(runtime)')) { throw new Error('CLI Node runtime must use the hardened runtime signature.'); } const nonSystemLibraries = linkedLibraries.filter( (path) => !path.startsWith('/usr/lib/') && !path.startsWith('/System/Library/'), ); if (nonSystemLibraries.length > 0) { throw new Error( `CLI Node runtime is not self-contained; non-system libraries: ${nonSystemLibraries.join(', ')}`, ); } } function extractNodeEntitlements(output, expectedEntitlements, description) { const start = output.indexOf(''); if (start < 0 || end < start) { throw new Error(`${description} has no readable entitlement plist.`); } const plist = output.slice(start, end + ''.length); const allKeys = [...plist.matchAll(/([^<]+)<\/key>/gu)].map((match) => match[1]).sort(); const keys = [...plist.matchAll(/([^<]+)<\/key>\s*/gu)] .map((match) => match[1]) .sort(); if ( JSON.stringify(allKeys) !== JSON.stringify(expectedEntitlements) || JSON.stringify(keys) !== JSON.stringify(expectedEntitlements) ) { throw new Error(`${description} entitlements do not match the reviewed contract.`); } return plist; } export function extractOfficialNodeEntitlements(output) { return extractNodeEntitlements( output, OFFICIAL_NODE_RUNTIME_ENTITLEMENTS, 'Official Node runtime', ); } export function extractDistributionNodeEntitlements(output) { return extractNodeEntitlements( output, DISTRIBUTION_NODE_RUNTIME_ENTITLEMENTS, 'Distribution Node runtime', ); } export function distributionNodeEntitlements(officialEntitlements) { const plist = extractOfficialNodeEntitlements(officialEntitlements); const distributionPlist = plist.replace( /\s*com\.apple\.security\.get-task-allow<\/key>\s*/u, '', ); return extractDistributionNodeEntitlements(distributionPlist); } async function inspectReleaseToolchain({ execPath, env, inspect, toolchain }) { const [nodeVersion, npmVersion, architectures, signature, entitlements, dependencies] = await Promise.all([ inspect(execPath, ['-p', 'process.versions.node'], { env }), inspect('npm', ['--version'], { env }), inspect('lipo', ['-archs', execPath], { env }), inspect('codesign', ['-d', '--verbose=4', execPath], { env }), inspect('codesign', ['-d', '--entitlements', ':-', execPath], { env }), inspect('otool', ['-L', execPath], { env }), ]); assertOfficialNodeRuntime({ actualVersion: nodeVersion.stdout.trim(), expectedVersion: toolchain.nodeVersion, architectures: architectures.stdout, signature: `${signature.stdout}\n${signature.stderr}`, linkedLibraries: parseLinkedLibraries(dependencies.stdout), }); if (npmVersion.stdout.trim() !== toolchain.npmVersion) { throw new Error( `CLI release requires npm ${toolchain.npmVersion}, found ${npmVersion.stdout.trim()}.`, ); } return extractOfficialNodeEntitlements(`${entitlements.stdout}\n${entitlements.stderr}`); } export async function assertOfficialNodeArchive( archivePath, toolchain, { hashFile = sha256File } = {}, ) { if (basename(archivePath) !== toolchain.nodeArchive) { throw new Error(`CLI Node archive must be named ${toolchain.nodeArchive}.`); } const actualSha256 = await hashFile(archivePath); if (actualSha256 !== toolchain.nodeArchiveSha256) { throw new Error( `CLI Node archive digest mismatch: expected ${toolchain.nodeArchiveSha256}, found ${actualSha256}.`, ); } return actualSha256; } async function extractOfficialNodeRuntime(stagingRoot, archivePath, toolchain, { env, run }) { await access(archivePath); await assertOfficialNodeArchive(archivePath, toolchain); const extractionRoot = join(stagingRoot, 'official-node'); await mkdir(extractionRoot, { recursive: true }); await run('tar', ['-xJf', archivePath, '-C', extractionRoot], { env }); const distributionRoot = join(extractionRoot, `node-v${toolchain.nodeVersion}-darwin-arm64`); const execPath = join(distributionRoot, 'bin', 'node'); const licensePath = join(distributionRoot, 'LICENSE'); await Promise.all([access(execPath), access(licensePath)]); return { execPath, licensePath }; } export async function collectPackagedProductionDependencies( nodeModulesDirectory, workspaceNames = new Set(), ) { const dependencies = new Set(); async function visitNodeModules(directory) { let entries; try { entries = await readdir(directory, { withFileTypes: true }); } catch (error) { if (error?.code === 'ENOENT') return; throw error; } const packageDirectories = []; for (const entry of entries) { if (entry.name === '.bin') continue; const path = join(directory, entry.name); if (entry.name.startsWith('@') && entry.isDirectory()) { for (const scopedEntry of await readdir(path, { withFileTypes: true })) { packageDirectories.push(join(path, scopedEntry.name)); } } else if (entry.isDirectory() || entry.isSymbolicLink()) { packageDirectories.push(path); } } for (const packageDirectory of packageDirectories) { let manifest; try { manifest = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8')); } catch (error) { if (error?.code === 'ENOENT') continue; throw error; } if ( !workspaceNames.has(manifest.name) && typeof manifest.name === 'string' && typeof manifest.version === 'string' ) { dependencies.add(`${manifest.name}@${manifest.version}`); } await visitNodeModules(join(packageDirectory, 'node_modules')); } } await visitNodeModules(nodeModulesDirectory); return [...dependencies].sort(); } async function findFiles(directory, predicate) { const matches = []; for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name); if (entry.isDirectory()) matches.push(...(await findFiles(path, predicate))); else if (entry.isFile() && predicate(path)) matches.push(path); } return matches; } export async function inspectNativeArtifacts( directory, { inspect = inspectCommand, concurrency = 16 } = {}, ) { const files = await findFiles(directory, () => true); const foreignBinaries = []; const machOBinaries = []; let nextIndex = 0; const workers = Array.from( { length: Math.min(Math.max(1, concurrency), files.length) }, async () => { while (nextIndex < files.length) { const path = files[nextIndex++]; const result = await inspect('file', ['-b', path]); if (/\bMach-O\b/.test(result.stdout)) machOBinaries.push(path); else if (/^(?:ELF\b|PE32\b|MS-DOS executable\b)/.test(result.stdout)) { foreignBinaries.push(path); } } }, ); await Promise.all(workers); return { foreignBinaries: foreignBinaries.sort(), machOBinaries: machOBinaries.sort() }; } export async function findMachOBinaries(directory, options = {}) { return (await inspectNativeArtifacts(directory, options)).machOBinaries; } export function isMacosArm64MachO(architectures, buildVersion) { return architectures.trim() === 'arm64' && /^\s*platform MACOS\s*$/m.test(buildVersion); } export function macosArm64MachOAction(architectures, buildVersion) { const architectureList = architectures.trim().split(/\s+/u).filter(Boolean); if (!architectureList.includes('arm64') || !/^\s*platform MACOS\s*$/m.test(buildVersion)) { return 'remove'; } return architectureList.length === 1 ? 'keep' : 'thin'; } async function pruneNonTargetNativeBinaries(nodeModulesDirectory, { inspect, run }) { const { foreignBinaries, machOBinaries } = await inspectNativeArtifacts(nodeModulesDirectory, { inspect, }); await Promise.all(foreignBinaries.map((path) => rm(path, { force: true }))); for (const binaryPath of machOBinaries) { const [architectures, buildVersion] = await Promise.all([ inspect('lipo', ['-archs', binaryPath]), inspect('xcrun', ['vtool', '-show-build', binaryPath]), ]); const action = macosArm64MachOAction(architectures.stdout, buildVersion.stdout); if (action === 'remove') { await rm(binaryPath, { force: true }); } else if (action === 'thin') { const thinnedPath = `${binaryPath}.arm64`; try { await run('lipo', [binaryPath, '-thin', 'arm64', '-output', thinnedPath]); const [thinnedArchitectures, thinnedBuildVersion] = await Promise.all([ inspect('lipo', ['-archs', thinnedPath]), inspect('xcrun', ['vtool', '-show-build', thinnedPath]), ]); if (!isMacosArm64MachO(thinnedArchitectures.stdout, thinnedBuildVersion.stdout)) { throw new Error(`Could not thin Mach-O file to macOS arm64: ${binaryPath}`); } await rename(thinnedPath, binaryPath); } finally { await rm(thinnedPath, { force: true }); } } } } export function assertReleaseSigningEnvironment(env) { for (const name of requiredSigningEnvironment) { if (!env[name]?.trim()) throw new Error(`CLI release signing requires ${name}.`); } } export function assertAcceptedNotarization(output) { let result; try { result = JSON.parse(output); } catch { throw new Error('notarytool did not return valid JSON.'); } if (result.status !== 'Accepted') { throw new Error(`CLI notarization failed with status ${result.status ?? 'unknown'}.`); } } export function decodeSigningCertificate(value) { const encoded = value?.replace(/[\t\n\r ]+/gu, ''); if (!encoded || encoded.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) { throw new Error('CSC_LINK must contain one base64-encoded PKCS12 certificate.'); } const bytes = Buffer.from(encoded, 'base64'); if (bytes.length === 0) { throw new Error('CSC_LINK must contain one base64-encoded PKCS12 certificate.'); } return { bytes }; } export function parseDeveloperIdApplicationIdentity(output) { const identities = [...output.matchAll(/^\s*\d+\)\s+([0-9A-Fa-f]{40})\s+"([^"]+)"\s*$/gmu)] .map((match) => ({ hash: match[1].toUpperCase(), name: match[2], teamIdentifier: / \(([A-Z0-9]{10})\)$/u.exec(match[2])?.[1], })) .filter(({ name }) => name.startsWith('Developer ID Application:')); if (identities.length !== 1) { throw new Error('CLI signing keychain must contain one Developer ID Application identity.'); } if (!identities[0].teamIdentifier) { throw new Error('Developer ID Application identity has no exact Apple Team ID.'); } return identities[0]; } export function assertExpectedAppleTeam(identity, expectedTeamIdentifier) { if (identity.teamIdentifier !== expectedTeamIdentifier) { throw new Error( `CLI signing identity belongs to Apple team ${identity.teamIdentifier}, expected ${expectedTeamIdentifier}.`, ); } return identity; } export async function createSigningKeychain({ env, expectedTeamIdentifier, run, inspect }) { const temporaryRoot = await mkdtemp(join(tmpdir(), 'maka-cli-signing-')); const certificatePath = join(temporaryRoot, 'identity.p12'); const pemPath = join(temporaryRoot, 'identity.pem'); const keychainFile = join(temporaryRoot, 'signing.keychain-db'); const keychainPassword = randomBytes(32).toString('hex'); let created = false; const cleanup = async () => { let keychainDeletionError; if (created) { try { await run('security', ['delete-keychain', keychainFile], { env }); } catch (error) { keychainDeletionError = error; } finally { created = false; } } try { await rm(temporaryRoot, { recursive: true, force: true }); } catch (error) { if (keychainDeletionError) { throw new AggregateError( [keychainDeletionError, error], 'Signing keychain and temporary credential cleanup both failed', ); } throw error; } if (keychainDeletionError) throw keychainDeletionError; }; try { await writeFile(certificatePath, decodeSigningCertificate(env.CSC_LINK).bytes, { mode: 0o600 }); await run('security', ['create-keychain', '-p', keychainPassword, keychainFile], { env, displayArgs: ['create-keychain', '-p', '', keychainFile], }); created = true; await run('security', ['unlock-keychain', '-p', keychainPassword, keychainFile], { env, displayArgs: ['unlock-keychain', '-p', '', keychainFile], }); await run('security', ['set-keychain-settings', '-lut', '21600', keychainFile], { env }); await run( 'openssl', [ 'pkcs12', '-in', certificatePath, '-nodes', '-passin', 'env:CSC_KEY_PASSWORD', '-out', pemPath, ], { env }, ); await chmod(pemPath, 0o600); await run('security', ['import', pemPath, '-k', keychainFile, '-T', '/usr/bin/codesign'], { env, }); await run( 'security', [ 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', keychainPassword, keychainFile, ], { env, displayArgs: [ 'set-key-partition-list', '-S', 'apple-tool:,apple:', '-s', '-k', '', keychainFile, ], }, ); const identityOutput = await inspect( 'security', ['find-identity', '-v', '-p', 'codesigning', keychainFile], { env }, ); const identity = assertExpectedAppleTeam( parseDeveloperIdApplicationIdentity(identityOutput.stdout), expectedTeamIdentifier, ); return { cleanup, directory: temporaryRoot, identity, keychainFile, }; } catch (error) { await cleanup(); throw error; } } async function signCliBinaries( machOBinaries, { env, expectedTeamIdentifier, run, inspect, nodeEntitlements, nodePath }, ) { assertReleaseSigningEnvironment(env); const signing = await createSigningKeychain({ env, expectedTeamIdentifier, run, inspect }); try { const nodeEntitlementsPath = join(signing.directory, 'node-entitlements.plist'); await writeFile(nodeEntitlementsPath, `${nodeEntitlements}\n`, { mode: 0o600 }); for (const binaryPath of machOBinaries) { const entitlements = resolve(binaryPath) === resolve(nodePath) ? ['--entitlements', nodeEntitlementsPath] : []; await run( 'codesign', [ '--force', '--options', 'runtime', '--timestamp', ...entitlements, '--sign', signing.identity.hash, '--keychain', signing.keychainFile, binaryPath, ], { env }, ); await run('codesign', ['--verify', '--strict', '--verbose=2', binaryPath], { env }); } const signedNodeEntitlements = await inspect( 'codesign', ['-d', '--entitlements', ':-', nodePath], { env }, ); extractDistributionNodeEntitlements( `${signedNodeEntitlements.stdout}\n${signedNodeEntitlements.stderr}`, ); return { identityName: signing.identity.name, machOBinaryCount: machOBinaries.length, teamIdentifier: signing.identity.teamIdentifier, }; } finally { await signing.cleanup(); } } async function createCliZip(archiveRoot, archivePath, { env, run }) { await rm(archivePath, { force: true }); await run( 'ditto', [ '-c', '-k', '--keepParent', '--norsrc', '--noextattr', '--noqtn', '--noacl', archiveRoot, archivePath, ], { env }, ); } async function notarizeCliZip(archivePath, { env, inspect }) { const result = await inspect( 'xcrun', [ 'notarytool', 'submit', archivePath, '--key', env.APPLE_API_KEY, '--key-id', env.APPLE_API_KEY_ID, '--issuer', env.APPLE_API_ISSUER, '--wait', '--output-format', 'json', ], { env, timeout: 20 * 60_000 }, ); assertAcceptedNotarization(result.stdout); } export async function packageMacosArm64Cli({ platform = process.platform, arch = process.arch, env = process.env, run = runCommand, inspect = inspectCommand, releaseSigning = env.MAKA_CLI_RELEASE_SIGNING === '1', nodeArchivePath = env.MAKA_CLI_NODE_ARCHIVE, } = {}) { assertMacosArm64CliHost(platform, arch); const [rootManifest, desktopManifest, cliManifest, workspacePackages, sourceCommitResult] = await Promise.all([ readFile(join(repoRoot, 'package.json'), 'utf8').then(JSON.parse), readFile(join(repoRoot, 'apps', 'desktop', 'package.json'), 'utf8').then(JSON.parse), readFile(join(repoRoot, 'packages', 'cli', 'package.json'), 'utf8').then(JSON.parse), resolveCliWorkspacePackages(), inspect('git', ['rev-parse', 'HEAD']), ]); const sourceCommit = sourceCommitResult.stdout.trim(); const identity = resolveProductReleaseIdentity({ rootManifest, desktopManifest, cliManifest, sha: sourceCommit, }); if (releaseSigning) assertReleaseSigningEnvironment(env); if (!nodeArchivePath) { throw new Error(`Set MAKA_CLI_NODE_ARCHIVE to the verified ${identity.nodeArchive} path.`); } const version = identity.version; await Promise.all([ access(join(repoRoot, 'DISCLAIMER-WIP')), access(join(repoRoot, 'LICENSE')), access(join(repoRoot, 'NOTICE')), ...workspacePackages.map(({ directory }) => access(join(directory, 'dist'))), ]); const { archiveRootName, archivePath, checksumPath } = resolveMacosArm64CliArtifactPaths(version); await mkdir(releaseDirectory, { recursive: true }); const stagingRoot = await mkdtemp(join(tmpdir(), 'maka-cli-')); let complete = false; try { const officialNode = await extractOfficialNodeRuntime( stagingRoot, resolve(nodeArchivePath), identity, { env, run }, ); const officialNodeEntitlements = await inspectReleaseToolchain({ execPath: officialNode.execPath, env, inspect, toolchain: identity, }); const nodeEntitlements = distributionNodeEntitlements(officialNodeEntitlements); const installRoot = join(stagingRoot, 'install'); await mkdir(installRoot, { recursive: true }); await stageWorkspacePackages(installRoot, workspacePackages); await run('npm', macosArm64CliInstallArgs(), { cwd: installRoot, env: standaloneInstallEnvironment(env), }); const dependencyPatches = await applyDependencyPatches(installRoot, { env, run }); const productionDependencies = await collectPackagedProductionDependencies( join(installRoot, 'node_modules'), new Set(workspacePackages.map(({ name }) => name)), ); const nodeModulesDirectory = join(installRoot, 'node_modules'); await pruneNonTargetNativeBinaries(nodeModulesDirectory, { inspect, run }); await pruneThirdPartyDevelopmentArtifacts(nodeModulesDirectory); const archiveRoot = join(stagingRoot, archiveRootName); const binDirectory = join(archiveRoot, 'bin'); const embeddedNodeDirectory = join(archiveRoot, 'libexec', 'node'); await Promise.all([ mkdir(binDirectory, { recursive: true }), mkdir(join(embeddedNodeDirectory, 'bin'), { recursive: true }), ]); await rename(nodeModulesDirectory, join(archiveRoot, 'libexec', 'node_modules')); for (const { workspacePath } of workspacePackages) { const source = join(installRoot, workspacePath); const target = join(archiveRoot, 'libexec', workspacePath); await mkdir(dirname(target), { recursive: true }); await rename(source, target); } await Promise.all([ copyFile(officialNode.execPath, join(embeddedNodeDirectory, 'bin', 'node')), copyFile(officialNode.licensePath, join(embeddedNodeDirectory, 'LICENSE')), copyFile(join(repoRoot, 'DISCLAIMER-WIP'), join(archiveRoot, 'DISCLAIMER-WIP')), copyFile(join(repoRoot, 'LICENSE'), join(archiveRoot, 'LICENSE')), copyFile(join(repoRoot, 'NOTICE'), join(archiveRoot, 'NOTICE')), writeFile(join(binDirectory, 'maka'), macosArm64CliWrapper(), 'utf8'), writeFile( join(archiveRoot, 'README.txt'), [ `Maka CLI/TUI ${version} for Apple Silicon macOS`, '', "Add this directory's bin folder to PATH, then run:", ' maka --help', '', 'The archive includes its own Node.js runtime and does not require the Maka desktop app.', '', ].join('\n'), 'utf8', ), ]); await Promise.all([ chmod(join(embeddedNodeDirectory, 'bin', 'node'), 0o755), chmod(join(binDirectory, 'maka'), 0o755), ]); await assertWorkspaceLinks(archiveRoot, workspacePackages); const thirdPartyNoticesPath = join(archiveRoot, 'THIRD_PARTY_NOTICES.txt'); await copyFile( join(repoRoot, 'packages', 'cli', 'THIRD_PARTY_NOTICES.txt'), thirdPartyNoticesPath, ); const thirdPartyNoticesSha256 = await sha256File(thirdPartyNoticesPath); const machOBinaries = await findMachOBinaries(archiveRoot, { inspect }); if (machOBinaries.length === 0) throw new Error('CLI artifact contains no Mach-O binaries.'); const releaseMetadata = { schemaVersion: 1, product: 'Maka', version, sourceCommit, platform: 'macos', architecture: 'arm64', publicCommands: identity.publicCommands, node: { version: identity.nodeVersion, sourceUrl: identity.nodeSourceUrl, archive: identity.nodeArchive, archiveSha256: identity.nodeArchiveSha256, entitlements: releaseSigning ? DISTRIBUTION_NODE_RUNTIME_ENTITLEMENTS : OFFICIAL_NODE_RUNTIME_ENTITLEMENTS, }, npmVersion: identity.npmVersion, dependencyPatches, productionDependencies, thirdPartyNoticesSha256, workspacePackages: workspacePackages.map(({ name }) => name).sort(), machOBinaries: machOBinaries.map((path) => relative(archiveRoot, path)).sort(), signing: releaseSigning ? 'developer-id-notarized' : 'development', signingTeamIdentifier: releaseSigning ? identity.appleTeamIdentifier : null, }; await writeFile( join(archiveRoot, 'RELEASE.json'), `${JSON.stringify(releaseMetadata, null, 2)}\n`, 'utf8', ); let signing; if (releaseSigning) { signing = await signCliBinaries(machOBinaries, { env, expectedTeamIdentifier: identity.appleTeamIdentifier, run, inspect, nodeEntitlements, nodePath: join(embeddedNodeDirectory, 'bin', 'node'), }); } await createCliZip(archiveRoot, archivePath, { env, run }); if (releaseSigning) await notarizeCliZip(archivePath, { env, inspect }); const sha256 = await sha256File(archivePath); await writeFile(checksumPath, `${sha256} ${basename(archivePath)}\n`, 'utf8'); complete = true; return { archivePath, checksumPath, dependencyPatches, sha256, signing, version }; } finally { await rm(stagingRoot, { recursive: true, force: true }); if (!complete) { const { archivePath, checksumPath } = resolveMacosArm64CliArtifactPaths(version); await Promise.all([rm(archivePath, { force: true }), rm(checksumPath, { force: true })]); } } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const result = await packageMacosArm64Cli(); console.log(`Created ${result.archivePath}`); console.log(`SHA-256 ${result.sha256}`); }