| /* |
| * 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 { spawn } from 'node:child_process'; |
| import { access, rename, rm } from 'node:fs/promises'; |
| import { createRequire } from 'node:module'; |
| import { dirname, join } from 'node:path'; |
| import { fileURLToPath, pathToFileURL } from 'node:url'; |
| import { resolveDesktopReleaseTarget } from './desktop-nightly.mjs'; |
| import { desktopPublishedFeeds } from './desktop-release-targets.mjs'; |
| |
| const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); |
| const desktopRoot = join(repoRoot, 'apps', 'desktop'); |
| // electron is declared by apps/desktop, so resolve its install directory from |
| // there rather than assuming node_modules hoisted it to the repo root. This |
| // pre-flight guard exists to catch a missing electron dist before packaging; |
| // pinning it to the hoisted path would make it fail at the wrong location the |
| // moment an installer nests electron under apps/desktop. |
| const require = createRequire(join(desktopRoot, 'package.json')); |
| const electronDistributionDirectory = join( |
| dirname(require.resolve('electron/package.json')), |
| 'dist', |
| ); |
| const requiredElectronLicensePaths = [ |
| join(electronDistributionDirectory, 'LICENSE'), |
| join(electronDistributionDirectory, 'LICENSES.chromium.html'), |
| ]; |
| const requiredSigningEnvironment = [ |
| 'CSC_LINK', |
| 'CSC_KEY_PASSWORD', |
| 'APPLE_API_KEY', |
| 'APPLE_API_KEY_ID', |
| 'APPLE_API_ISSUER', |
| ]; |
| |
| function runCommand(command, args) { |
| return new Promise((resolve, reject) => { |
| const child = spawn(command, args, { |
| cwd: repoRoot, |
| env: process.env, |
| stdio: 'inherit', |
| }); |
| child.once('error', reject); |
| child.once('exit', (code, signal) => { |
| if (code === 0) { |
| resolve(); |
| return; |
| } |
| reject( |
| new Error( |
| `${command} ${args.join(' ')} failed with ${ |
| signal ? `signal ${signal}` : `exit code ${code}` |
| }`, |
| ), |
| ); |
| }); |
| }); |
| } |
| |
| const macosPackageArchitectures = Object.freeze(['arm64', 'x64']); |
| |
| export async function packageMacos({ |
| targetArch = process.arch, |
| platform = process.platform, |
| arch = process.arch, |
| env = process.env, |
| run = runCommand, |
| remove = rm, |
| move = rename, |
| assertFile = access, |
| } = {}) { |
| if (!macosPackageArchitectures.includes(targetArch)) { |
| throw new Error( |
| `Release packaging supports ${macosPackageArchitectures.join(' and ')}, not ${targetArch}.`, |
| ); |
| } |
| // The native Runtime Host peer and the packaged smoke probes are built and |
| // run for the host, so each architecture ships from a runner of its own |
| // rather than cross-building both from one. |
| if (platform !== 'darwin' || arch !== targetArch) { |
| throw new Error(`Release packaging of ${targetArch} requires a ${targetArch} macOS host.`); |
| } |
| |
| for (const name of requiredSigningEnvironment) { |
| if (!env[name]?.trim()) { |
| throw new Error(`Release packaging requires ${name}.`); |
| } |
| } |
| |
| const target = await resolveDesktopReleaseTarget(`macos-${targetArch}`, { environment: env }); |
| const dmgPath = target.payloadPath('.dmg'); |
| const zipPath = target.payloadPath('.zip'); |
| // Both architectures write the one feed clients read, and both uploads land |
| // in one directory before publication. Naming the feed after its architecture |
| // here is what keeps the two from overwriting each other; they are merged |
| // back into the single feed at publication time. |
| const updateMetadataPath = join( |
| target.releaseDirectory, |
| desktopPublishedFeeds(target.version, { nightly: target.nightly }).find((feed) => |
| feed.mergedFrom?.includes(target.feed), |
| ).name, |
| ); |
| const architectureMetadataPath = join(target.releaseDirectory, target.feed); |
| |
| for (const path of requiredElectronLicensePaths) { |
| await assertFile(path); |
| } |
| |
| await run('npm', ['run', 'clean']); |
| await run('npm', ['run', 'build']); |
| await run('npm', ['run', 'build:runtime-host-peer']); |
| await run('npm', ['run', 'check:runtime-host-peer-notices']); |
| await run('npm', ['run', 'check:release']); |
| await remove(target.releaseDirectory, { recursive: true, force: true }); |
| await run('npm', ['--workspace', '@maka/desktop', 'run', `package:macos-${targetArch}`]); |
| await assertFile(dmgPath); |
| await assertFile(zipPath); |
| await assertFile(updateMetadataPath); |
| await move(updateMetadataPath, architectureMetadataPath); |
| // electron-builder names the unpacked staging directory after the target: |
| // `mac` for x64, `mac-<arch>` for everything else. |
| await remove(join(target.releaseDirectory, targetArch === 'x64' ? 'mac' : `mac-${targetArch}`), { |
| recursive: true, |
| force: true, |
| }); |
| |
| return dmgPath; |
| } |
| |
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| const dmgPath = await packageMacos({ targetArch: process.argv[2] || process.arch }); |
| console.log(`Created ${dmgPath}`); |
| } |