blob: 6d5d914ef8339ed65a39d8cdf95d4a27cc9a51c2 [file]
/*
* 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}`);
}