| /* | |
| * 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}`); | |
| } |