| /* | |
| * 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 { mergeDesktopUpdateFeeds } from './desktop-update-contract.mjs'; | |
| import { resolveDesktopReleaseTarget } from './desktop-nightly.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. | |
| 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 linuxPackageArchitectures = Object.freeze(['x64', 'arm64']); | |
| /** electron-builder names the unpacked staging directory after the target. */ | |
| function linuxUnpackedDirectoryName(arch) { | |
| return arch === 'x64' ? 'linux-unpacked' : `linux-${arch}-unpacked`; | |
| } | |
| 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}` | |
| }`, | |
| ), | |
| ); | |
| }); | |
| }); | |
| } | |
| export async function packageLinux({ | |
| platform = process.platform, | |
| arch = process.arch, | |
| env = process.env, | |
| run = runCommand, | |
| remove = rm, | |
| move = rename, | |
| mergeFeeds = mergeDesktopUpdateFeeds, | |
| assertFile = access, | |
| } = {}) { | |
| // The native Runtime Host peer is built for the host, so each architecture | |
| // ships from a runner of its own rather than cross-building both from one. | |
| if (platform !== 'linux' || !linuxPackageArchitectures.includes(arch)) { | |
| throw new Error( | |
| `Release packaging requires a Linux ${linuxPackageArchitectures.join(' or ')} host.`, | |
| ); | |
| } | |
| const target = await resolveDesktopReleaseTarget(`linux-${arch}`, { environment: env }); | |
| const appImagePath = target.payloadPath('.AppImage'); | |
| const debPath = target.payloadPath('.deb'); | |
| const updateMetadataPath = join(target.releaseDirectory, target.feed); | |
| // electron-builder rewrites the feed on every run rather than adding to it, so | |
| // the AppImage's copy is moved aside and merged back once the deb has run. | |
| const appImageMetadataPath = `${updateMetadataPath}.appimage`; | |
| 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 }); | |
| // Both distributables come out of one unpacked tree, and the deb target writes | |
| // a `package-type` marker into it that makes electron-updater drive an install | |
| // through DebUpdater. Building the AppImage in a run of its own is what keeps | |
| // that marker out of the AppImage, which has to update itself in place. | |
| await run('npm', ['--workspace', '@maka/desktop', 'run', `package:linux-appimage-${arch}`]); | |
| await assertFile(appImagePath); | |
| await assertFile(updateMetadataPath); | |
| await move(updateMetadataPath, appImageMetadataPath); | |
| await run('npm', ['--workspace', '@maka/desktop', 'run', `package:linux-deb-${arch}`]); | |
| await assertFile(debPath); | |
| await assertFile(updateMetadataPath); | |
| await mergeFeeds({ | |
| sourcePaths: [appImageMetadataPath, updateMetadataPath], | |
| outputPath: updateMetadataPath, | |
| }); | |
| await remove(appImageMetadataPath, { force: true }); | |
| await remove(join(target.releaseDirectory, linuxUnpackedDirectoryName(arch)), { | |
| recursive: true, | |
| force: true, | |
| }); | |
| return { appImagePath, debPath, updateMetadataPath }; | |
| } | |
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { | |
| const { appImagePath, debPath } = await packageLinux(); | |
| console.log(`Created ${appImagePath}`); | |
| console.log(`Created ${debPath}`); | |
| } |