mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
build(linux): carry the pinned Node inside the AppImage so serve's slot works offline (#28204)
* build(linux): carry the pinned Node inside the AppImage so orca serve's slot works offline Linux packaging places the pinned glibc Node at resources/node-runtime/bin/node before the glibc-floor gate, proves it serves one PTY with the packaged daemon entry, and deb/rpm exclude it through fpm. orca serve's orcad slot copies that Node (hash-checked) instead of downloading. * test(serve): type the bundled Node fixture --------- Co-authored-by: m4air <m4air@Mac.localdomain>
This commit is contained in:
@@ -1173,6 +1173,19 @@ jobs:
|
||||
rpm_marker="$(rpm2cpio "$rpm" | cpio --quiet --extract --to-stdout ./opt/Orca/resources/package-type)"
|
||||
[[ "$deb_marker" == deb ]] || { echo "Expected deb marker, got: $deb_marker"; exit 1; }
|
||||
[[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; }
|
||||
# Only the AppImage carries the pinned Node (config/scripts/appimage-node-runtime.cjs).
|
||||
if dpkg-deb --fsys-tarfile "$deb" | tar -t | grep '/resources/node-runtime' >/dev/null; then
|
||||
echo "The deb must not carry the AppImage's Node"; exit 1
|
||||
fi
|
||||
if rpm2cpio "$rpm" | cpio --quiet -t | grep '/resources/node-runtime' >/dev/null; then
|
||||
echo "The rpm must not carry the AppImage's Node"; exit 1
|
||||
fi
|
||||
appimage_root="$(mktemp -d)"
|
||||
(cd "$appimage_root" && "$GITHUB_WORKSPACE/dist/orca-linux.AppImage" --appimage-extract 'resources/node-runtime/bin/node' >/dev/null)
|
||||
test -x "$appimage_root/squashfs-root/resources/node-runtime/bin/node"
|
||||
"$appimage_root/squashfs-root/resources/node-runtime/bin/node" --version
|
||||
rm -rf "$appimage_root"
|
||||
ls -l dist/orca-linux.AppImage "$deb" "$rpm"
|
||||
|
||||
- wait: shutdown-fixture-cache
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ const {
|
||||
} = require('./scripts/packaged-orcad-template.cjs')
|
||||
const { verifySkillsCliRuntime } = require('./scripts/verify-skills-cli-runtime.cjs')
|
||||
const { verifyStaticAppImagePackage } = require('./scripts/static-appimage-package-contract.cjs')
|
||||
const {
|
||||
appImageNodeRuntimeFpmArgs,
|
||||
placeAppImageNodeRuntime,
|
||||
verifyAppImageNodeRuntimeServesPty
|
||||
} = require('./scripts/appimage-node-runtime.cjs')
|
||||
const { signWindowsUninstallerViaSignPath } = require('./scripts/windows-uninstaller-signing.cjs')
|
||||
|
||||
// Why: dev-channel builds must carry the *release* identity — same bundle id,
|
||||
@@ -440,13 +445,15 @@ module.exports = {
|
||||
// Why after the prune: `pnpm install:release` widens the CPU set for cross-builds,
|
||||
// so an arm64 slice can still carry the x64 @parcel/watcher until
|
||||
// prunePackagedRuntimeNodeModules drops it.
|
||||
let appImageNodeRuntime = null
|
||||
if (context.electronPlatformName === 'linux') {
|
||||
const targetArch = { 1: 'x64', 3: 'arm64' }[context.arch]
|
||||
// Before the floor gate, so the bundled Node is held to the same glibc floor.
|
||||
appImageNodeRuntime = await placeAppImageNodeRuntime(resourcesDir, targetArch)
|
||||
// Why the arch is passed: symbol-version checks pass happily on a wrong-architecture binary,
|
||||
// so a cross-built slice could ship the host's pty.node and only fail at runtime.
|
||||
finalizePackagedWaylandClipboard(resourcesDir)
|
||||
verifyLinuxGlibcFloor(context.appOutDir, {
|
||||
targetArch: { 1: 'x64', 3: 'arm64' }[context.arch]
|
||||
})
|
||||
verifyLinuxGlibcFloor(context.appOutDir, { targetArch })
|
||||
}
|
||||
verifyPackagedMainRuntimeDeps(resourcesDir)
|
||||
// Why: boot the packaged daemon-entry under plain Node, but only for the
|
||||
@@ -471,6 +478,9 @@ module.exports = {
|
||||
}
|
||||
if (canExecuteTargetArch) {
|
||||
verifyPackagedDaemonEntryBoots(resourcesDir)
|
||||
if (appImageNodeRuntime) {
|
||||
await verifyAppImageNodeRuntimeServesPty(resourcesDir, appImageNodeRuntime)
|
||||
}
|
||||
} else {
|
||||
// Why: a cross-arch slice can't be booted by the host Node, but the
|
||||
// unpacked entry must still exist — its absence is a layout regression
|
||||
@@ -740,7 +750,9 @@ module.exports = {
|
||||
// GUI-triggered and can never run on a server, so without this the CLI is
|
||||
// unreachable from the shell on exactly the hosts that need it.
|
||||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||||
afterRemove: 'resources/linux/packaging/after-remove.sh'
|
||||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||||
fpm: appImageNodeRuntimeFpmArgs
|
||||
},
|
||||
rpm: {
|
||||
packageName: 'orca-ide',
|
||||
@@ -757,7 +769,9 @@ module.exports = {
|
||||
],
|
||||
// Why: same headless CLI-on-PATH registration as deb; rpm runs these via fpm.
|
||||
afterInstall: 'resources/linux/packaging/after-install.sh',
|
||||
afterRemove: 'resources/linux/packaging/after-remove.sh'
|
||||
afterRemove: 'resources/linux/packaging/after-remove.sh',
|
||||
// Why: only an AppImage's files can vanish under a running daemon (see appimage-node-runtime.cjs).
|
||||
fpm: appImageNodeRuntimeFpmArgs
|
||||
},
|
||||
beforeBuild: electronBuilderNativeRebuild,
|
||||
// Why: must be true so that electron-builder rebuilds native modules
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Linux builds place the pinned Node at resources/node-runtime/bin/node in afterPack, before the
|
||||
* glibc-floor gate scans the tree. deb and rpm drop it again through fpm, so only the AppImage
|
||||
* ships it: an AppImage's files vanish with its mount, an installed package's do not.
|
||||
*/
|
||||
const { chmodSync, copyFileSync, mkdirSync } = require('node:fs')
|
||||
const { join, resolve } = require('node:path')
|
||||
const { pathToFileURL } = require('node:url')
|
||||
const {
|
||||
APPIMAGE_NODE_RUNTIME_FPM_EXCLUDE,
|
||||
APPIMAGE_NODE_RUNTIME_SEGMENTS,
|
||||
appImageNodeRuntimeTarget
|
||||
} = require('../../src/shared/appimage-node-runtime.ts')
|
||||
const { runTerminalHostRoundTrip } = require('./verify-packaged-daemon-entry.cjs')
|
||||
|
||||
const appImageNodeRuntimeFpmArgs = ['--exclude', APPIMAGE_NODE_RUNTIME_FPM_EXCLUDE]
|
||||
const PTY_ROUND_TRIP_TIMEOUT_MS = 20_000
|
||||
|
||||
async function ensurePinnedNode(target) {
|
||||
const { ensurePinnedNodeExecutable } = await import(
|
||||
pathToFileURL(resolve(__dirname, 'pinned-node-downloads.mjs')).href
|
||||
)
|
||||
return ensurePinnedNodeExecutable({ target })
|
||||
}
|
||||
|
||||
/** `arch` is a Node arch name; the copy is hash-checked by the pinned download it comes from. */
|
||||
async function placeAppImageNodeRuntime(resourcesDir, arch, ensureNode = ensurePinnedNode) {
|
||||
const target = appImageNodeRuntimeTarget(arch)
|
||||
if (!target) {
|
||||
throw new Error(`[appimage-node-runtime] the Node pin has no glibc build for Linux ${arch}`)
|
||||
}
|
||||
const destination = join(resourcesDir, ...APPIMAGE_NODE_RUNTIME_SEGMENTS)
|
||||
mkdirSync(join(destination, '..'), { recursive: true })
|
||||
copyFileSync(await ensureNode(target), destination)
|
||||
chmodSync(destination, 0o755)
|
||||
return destination
|
||||
}
|
||||
|
||||
/** The bundled Node must load the packaged node-pty and serve one PTY, as the daemon will. */
|
||||
async function verifyAppImageNodeRuntimeServesPty(resourcesDir, runtimePath) {
|
||||
const stderr = []
|
||||
try {
|
||||
await runTerminalHostRoundTrip({
|
||||
command: [runtimePath],
|
||||
entry: join(resourcesDir, 'app.asar.unpacked', 'out', 'main', 'daemon-entry.js'),
|
||||
deadline: Date.now() + PTY_ROUND_TRIP_TIMEOUT_MS,
|
||||
stderr
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`[appimage-node-runtime] the bundled Node did not serve a PTY: ${error.message}\n${stderr.join('').slice(-2000)}`
|
||||
)
|
||||
}
|
||||
console.log('[appimage-node-runtime] OK — the bundled Node served a PTY')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appImageNodeRuntimeFpmArgs,
|
||||
placeAppImageNodeRuntime,
|
||||
verifyAppImageNodeRuntimeServesPty
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const {
|
||||
appImageNodeRuntimeFpmArgs,
|
||||
placeAppImageNodeRuntime
|
||||
} = require('./appimage-node-runtime.cjs')
|
||||
const releaseConfig = require('../electron-builder.config.cjs')
|
||||
const prLinuxConfig = require('../electron-builder-pr-linux.config.cjs')
|
||||
|
||||
let root = ''
|
||||
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'orca-appimage-node-pack-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
/** Ruby's File.fnmatch without flags, which is how fpm applies --exclude: `*` crosses `/`. */
|
||||
function fpmExcludes(pattern, stagedPath) {
|
||||
const source = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*')
|
||||
return new RegExp(`^${source}$`).test(stagedPath)
|
||||
}
|
||||
|
||||
describe('the pinned Node in Linux packages', () => {
|
||||
it('is placed under resources as an executable bin/node for the slice arch', async () => {
|
||||
const pinned = join(root, 'pinned-node')
|
||||
writeFileSync(pinned, 'node bytes')
|
||||
const requested = []
|
||||
const destination = await placeAppImageNodeRuntime(join(root, 'resources'), 'arm64', (t) => {
|
||||
requested.push(t)
|
||||
return pinned
|
||||
})
|
||||
expect(requested).toEqual(['linux-arm64-glibc'])
|
||||
expect(destination).toBe(join(root, 'resources', 'node-runtime', 'bin', 'node'))
|
||||
expect(readFileSync(destination, 'utf8')).toBe('node bytes')
|
||||
expect(statSync(destination).mode & 0o777).toBe(0o755)
|
||||
})
|
||||
|
||||
it('fails packaging for a slice the pin has no glibc build for', async () => {
|
||||
await expect(
|
||||
placeAppImageNodeRuntime(join(root, 'resources'), 'ia32', () => '')
|
||||
).rejects.toThrow('no glibc build for Linux ia32')
|
||||
})
|
||||
|
||||
it('ships only in the AppImage: deb and rpm exclude it, release and PR builds alike', () => {
|
||||
for (const config of [releaseConfig, prLinuxConfig]) {
|
||||
for (const target of ['deb', 'rpm']) {
|
||||
const fpm = config[target].fpm.join('\0')
|
||||
expect(fpm).toContain(appImageNodeRuntimeFpmArgs.join('\0'))
|
||||
}
|
||||
expect(config.appImage.fpm).toBeUndefined()
|
||||
}
|
||||
const [, pattern] = appImageNodeRuntimeFpmArgs
|
||||
expect(fpmExcludes(pattern, 'opt/Orca/resources/node-runtime')).toBe(true)
|
||||
expect(fpmExcludes(pattern, 'opt/Orca/resources/node_modules')).toBe(false)
|
||||
expect(fpmExcludes(pattern, 'opt/Orca/resources/app.asar.unpacked/out/node-runtime')).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { NODE_RUNTIME_ASSETS } from '../../shared/node-runtime-pin'
|
||||
import { findAppImageNodeRuntime } from './appimage-bundled-node-runtime'
|
||||
|
||||
let resources = ''
|
||||
|
||||
beforeEach(() => {
|
||||
resources = mkdtempSync(join(tmpdir(), 'orca-appimage-node-'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(resources, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function bundle(): string {
|
||||
const path = join(resources, 'node-runtime', 'bin', 'node')
|
||||
mkdirSync(join(path, '..'), { recursive: true })
|
||||
writeFileSync(path, '')
|
||||
return path
|
||||
}
|
||||
|
||||
describe('findAppImageNodeRuntime', () => {
|
||||
it('names the pinned glibc Node a Linux package carries for its CPU', () => {
|
||||
const path = bundle()
|
||||
expect(findAppImageNodeRuntime(resources, 'arm64', 'linux')).toEqual({
|
||||
path,
|
||||
target: 'linux-arm64-glibc',
|
||||
executableSha256: NODE_RUNTIME_ASSETS['linux-arm64-glibc'].executableSha256
|
||||
})
|
||||
})
|
||||
|
||||
it('finds nothing in a package without one, or off Linux', () => {
|
||||
expect(findAppImageNodeRuntime(resources, 'x64', 'linux')).toBeNull()
|
||||
bundle()
|
||||
expect(findAppImageNodeRuntime(resources, 'x64', 'darwin')).toBeNull()
|
||||
expect(findAppImageNodeRuntime(resources, 'ia32', 'linux')).toBeNull()
|
||||
expect(findAppImageNodeRuntime(undefined, 'x64', 'linux')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
/** The pinned Node inside a Linux AppImage's resources, used where Orca would otherwise download it. */
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { chmod, copyFile, mkdir, rename, rm } from 'node:fs/promises'
|
||||
import { dirname, join } from 'node:path'
|
||||
import {
|
||||
APPIMAGE_NODE_RUNTIME_SEGMENTS,
|
||||
appImageNodeRuntimeTarget
|
||||
} from '../../shared/appimage-node-runtime'
|
||||
import { NODE_RUNTIME_ASSETS, type ServerTarget } from '../../shared/node-runtime-pin'
|
||||
import { verifyFileSha256 } from './pinned-runtime-materializer'
|
||||
|
||||
export type AppImageNodeRuntime = {
|
||||
path: string
|
||||
target: ServerTarget
|
||||
executableSha256: string
|
||||
}
|
||||
|
||||
/** Null for every package that carries none: deb, rpm, macOS, Windows and dev builds. */
|
||||
export function findAppImageNodeRuntime(
|
||||
resourcesPath: string | undefined,
|
||||
arch: string = process.arch,
|
||||
platform: NodeJS.Platform = process.platform
|
||||
): AppImageNodeRuntime | null {
|
||||
const target = appImageNodeRuntimeTarget(arch)
|
||||
if (platform !== 'linux' || !resourcesPath || !target) {
|
||||
return null
|
||||
}
|
||||
const path = join(resourcesPath, ...APPIMAGE_NODE_RUNTIME_SEGMENTS)
|
||||
return existsSync(path)
|
||||
? { path, target, executableSha256: NODE_RUNTIME_ASSETS[target].executableSha256 }
|
||||
: null
|
||||
}
|
||||
|
||||
/** Copies the bundled Node to `destination`; only bytes that hash to the pin are published. */
|
||||
export async function copyVerifiedAppImageNodeRuntime(
|
||||
runtime: AppImageNodeRuntime,
|
||||
destination: string
|
||||
): Promise<void> {
|
||||
await mkdir(dirname(destination), { recursive: true })
|
||||
const staging = `${destination}.copy-${process.pid}-${randomUUID()}`
|
||||
try {
|
||||
await copyFile(runtime.path, staging)
|
||||
await verifyFileSha256(staging, runtime.executableSha256, 'bundled Node')
|
||||
await chmod(staging, 0o755)
|
||||
await rename(staging, destination)
|
||||
} finally {
|
||||
await rm(staging, { force: true })
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../../shared/orcad-local-serve-selection'
|
||||
import { selectServeRuntime } from './orcad-local-serve-selection'
|
||||
import { pruneDesktopOrcadArtifactCache } from './orcad-artifact-cache-retention'
|
||||
import { findAppImageNodeRuntime } from '../node-runtime/appimage-bundled-node-runtime'
|
||||
|
||||
function flagValue(argv: readonly string[], flag: string): string | null {
|
||||
const index = argv.indexOf(flag)
|
||||
@@ -30,7 +31,8 @@ async function run(argv: readonly string[]): Promise<void> {
|
||||
templateDirs: [
|
||||
...(process.resourcesPath ? [join(process.resourcesPath, 'orcad-template')] : []),
|
||||
join(appRoot, 'out', 'orcad-template')
|
||||
]
|
||||
],
|
||||
bundledNodeRuntime: findAppImageNodeRuntime(process.resourcesPath)
|
||||
})
|
||||
// orcad serve never starts the desktop's startup pass, so the slot cache is bounded here too.
|
||||
await pruneDesktopOrcadArtifactCache(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { hashProfileStatePayload } from '../persistence/profile-state/profile-state-document-validation'
|
||||
import { importProfileStateJson } from '../persistence/profile-state/profile-state-documents'
|
||||
import { selectServeRuntime, type ServeRuntimeSelectionInput } from './orcad-local-serve-selection'
|
||||
import type { AppImageNodeRuntime } from '../node-runtime/appimage-bundled-node-runtime'
|
||||
|
||||
const TARGET = 'linux-x64-glibc'
|
||||
const SSH_TARGET = { id: 'ssh-1', label: 'box', host: 'box.example', port: 22, username: 'me' }
|
||||
@@ -202,6 +204,57 @@ describe('orca serve runtime selection', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('an AppImage that carries the pinned Node', () => {
|
||||
const offline = (): ServeRuntimeSelectionInput['materializeRuntime'] =>
|
||||
vi.fn(async (): Promise<string> => {
|
||||
throw new Error('fetch failed')
|
||||
})
|
||||
|
||||
function bundledNode(bytes: string): AppImageNodeRuntime {
|
||||
const path = join(root, 'resources', 'node-runtime', 'bin', 'node')
|
||||
mkdirSync(join(path, '..'), { recursive: true })
|
||||
writeFileSync(path, bytes)
|
||||
return {
|
||||
path,
|
||||
target: TARGET,
|
||||
executableSha256: createHash('sha256').update('#!/bin/sh\n# pinned\n').digest('hex')
|
||||
}
|
||||
}
|
||||
|
||||
it('prepares the slot from it on an offline first run', async () => {
|
||||
const selection = await selectServeRuntime(
|
||||
input({
|
||||
materializeRuntime: offline(),
|
||||
bundledNodeRuntime: bundledNode('#!/bin/sh\n# pinned\n')
|
||||
})
|
||||
)
|
||||
const runtime = join(
|
||||
root,
|
||||
'orcad-artifacts',
|
||||
TARGET,
|
||||
'0.1.0+abc',
|
||||
...orcadNodeRuntimeRelativePath(TARGET, SHA)
|
||||
)
|
||||
expect(selection).toMatchObject({ kind: 'orcad', runtime })
|
||||
expect(readFileSync(runtime, 'utf8')).toBe('#!/bin/sh\n# pinned\n')
|
||||
})
|
||||
|
||||
it('downloads instead when the bundled bytes are not the pin', async () => {
|
||||
const options = input({ bundledNodeRuntime: bundledNode('#!/bin/sh\n# tampered\n') })
|
||||
expect(await selectServeRuntime(options)).toMatchObject({ kind: 'orcad' })
|
||||
expect(options.materializeRuntime).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('downloads instead when the bundled Node is for another host', async () => {
|
||||
const options = input({
|
||||
hostTarget: () => 'linux-x64-musl',
|
||||
bundledNodeRuntime: bundledNode('#!/bin/sh\n# pinned\n')
|
||||
})
|
||||
await selectServeRuntime(options)
|
||||
expect(options.materializeRuntime).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[
|
||||
'an unknown runtime name',
|
||||
|
||||
@@ -31,6 +31,10 @@ import { resolveBundledOrcadRuntime } from './orcad-bundled-runtime'
|
||||
import { detectNativeHostAbi, nativeSlotName } from './native-host-abi'
|
||||
import { materializeOrcadArtifact } from './orcad-artifact-materializer'
|
||||
import { materializeCachedNodeRuntime } from '../node-runtime/pinned-runtime-materializer'
|
||||
import {
|
||||
copyVerifiedAppImageNodeRuntime,
|
||||
type AppImageNodeRuntime
|
||||
} from '../node-runtime/appimage-bundled-node-runtime'
|
||||
import { serveProfileHasSshTargets } from './serve-profile-ssh-targets'
|
||||
import { errorMessage } from '../../shared/error-message'
|
||||
|
||||
@@ -44,6 +48,8 @@ export type ServeRuntimeSelectionInput = {
|
||||
hostTarget?: () => string
|
||||
materializeSlot?: typeof materializeOrcadArtifact
|
||||
materializeRuntime?: typeof materializeCachedNodeRuntime
|
||||
/** The pinned Node an AppImage carries; the slot uses it before trying a download. */
|
||||
bundledNodeRuntime?: AppImageNodeRuntime | null
|
||||
nativePreflight?: (runtime: string, entry: string) => Promise<string>
|
||||
profileHasSshTargets?: (userDataPath: string) => boolean
|
||||
}
|
||||
@@ -87,7 +93,7 @@ export async function selectServeRuntime(
|
||||
templateDir,
|
||||
cacheRoot
|
||||
})
|
||||
await placeSlotRuntime(slotDir, target, cacheRoot, input.materializeRuntime)
|
||||
await placeSlotRuntime(slotDir, target, cacheRoot, input)
|
||||
} catch (error) {
|
||||
return electron(`the orcad slot for ${target} could not be prepared: ${errorMessage(error)}`)
|
||||
}
|
||||
@@ -133,12 +139,15 @@ function profileSshReason(input: ServeRuntimeSelectionInput): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** The slot references its runtime beside it; the cached pinned Node is linked into place. */
|
||||
/** The slot references its runtime beside it: the AppImage's own copy, else the cached download. */
|
||||
async function placeSlotRuntime(
|
||||
slotDir: string,
|
||||
target: ServerTarget,
|
||||
cacheRoot: string,
|
||||
materializeRuntime: typeof materializeCachedNodeRuntime = materializeCachedNodeRuntime
|
||||
{
|
||||
materializeRuntime = materializeCachedNodeRuntime,
|
||||
bundledNodeRuntime
|
||||
}: Pick<ServeRuntimeSelectionInput, 'materializeRuntime' | 'bundledNodeRuntime'>
|
||||
): Promise<void> {
|
||||
const destination = join(
|
||||
slotDir,
|
||||
@@ -147,6 +156,14 @@ async function placeSlotRuntime(
|
||||
if (existsSync(destination)) {
|
||||
return
|
||||
}
|
||||
if (bundledNodeRuntime?.target === target) {
|
||||
try {
|
||||
await copyVerifiedAppImageNodeRuntime(bundledNodeRuntime, destination)
|
||||
return
|
||||
} catch (error) {
|
||||
console.error(`orca serve: not using the bundled Node (${errorMessage(error)})`)
|
||||
}
|
||||
}
|
||||
const cached = await materializeRuntime(target, cacheRoot, { fetcher: fetch })
|
||||
mkdirSync(dirname(destination), { recursive: true })
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* The pinned Node a Linux AppImage carries, so what runs outside Electron needs no download.
|
||||
* deb and rpm exclude it at packaging time: their installs never vanish under a running process.
|
||||
*
|
||||
* Keep this file erasable-only TypeScript with no imports — packaging scripts require it directly.
|
||||
*/
|
||||
|
||||
/** Segments under the app's resources dir, laid out like the upstream archive (`bin/node`). */
|
||||
export const APPIMAGE_NODE_RUNTIME_SEGMENTS = ['node-runtime', 'bin', 'node'] as const
|
||||
/** fpm `--exclude` pattern, matched against each staged path (`*` crosses `/` there). */
|
||||
export const APPIMAGE_NODE_RUNTIME_FPM_EXCLUDE = '*/resources/node-runtime'
|
||||
|
||||
/** AppImages are glibc builds, so only a glibc host of the same CPU can use the bundled Node. */
|
||||
export function appImageNodeRuntimeTarget(
|
||||
arch: string
|
||||
): 'linux-x64-glibc' | 'linux-arm64-glibc' | null {
|
||||
if (arch === 'x64') {
|
||||
return 'linux-x64-glibc'
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
return 'linux-arm64-glibc'
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user