Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/actions/prepare-windows-cli-launcher/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Prepare the orcad Windows CLI launcher
description: Compiles native/windows-cli-launcher for each requested arch into .build/windows-cli-launcher/<arch>/orca.exe.

inputs:
arches:
description: Space-separated Windows architectures (x64, arm64).
required: true

runs:
using: composite
steps:
- name: Cache the Windows CLI launcher builds
if: runner.os == 'Windows'
uses: actions/cache@v5
with:
path: |
.build/windows-cli-launcher
native/windows-cli-launcher/target
~/.cargo/registry
key: orcad-windows-cli-launcher-${{ runner.arch }}-${{ inputs.arches }}-${{ hashFiles('native/windows-cli-launcher/**', 'config/scripts/build-windows-cli-launcher.mjs', 'resources/build/icon.ico', 'package.json') }}

# Why explicit: a runner image without cargo would otherwise fail inside the orcad build.
- name: Build the launcher for each architecture
if: runner.os == 'Windows'
shell: pwsh
env:
ARCHES: ${{ inputs.arches }}
run: |
if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
rustup toolchain install stable --profile minimal
rustup default stable
}
$triples = @{ x64 = 'x86_64-pc-windows-msvc'; arm64 = 'aarch64-pc-windows-msvc' }
foreach ($arch in ($env:ARCHES -split ' ' | Where-Object { $_ })) {
rustup target add $triples[$arch]
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
node config/scripts/build-windows-cli-launcher.mjs --arch $arch --output ".build/windows-cli-launcher/$arch/orca.exe"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,9 @@ jobs:
with:
resolve-windows-cache: 'true'
restore-windows-cache: 'true'
- uses: ./.github/actions/prepare-windows-cli-launcher
with:
arches: x64
- name: Build the e2e app, CLI, orcad slot and template
shell: bash
run: |
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/node-server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ jobs:
(github.event_name == 'pull_request' ||
(github.ref == 'refs/heads/main' && contains(fromJSON('["push","schedule","workflow_dispatch"]'), github.event_name))) }}
restore-windows-cache: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
# The template lane cross-compiles arm64 too, as it does for the process-table addon.
- uses: ./.github/actions/prepare-windows-cli-launcher
if: runner.os == 'Windows'
with:
arches: ${{ (inputs.build_template && matrix.os == 'windows-2022') && 'x64 arm64' || (runner.arch == 'ARM64' && 'arm64' || 'x64') }}
- run: pnpm build:orcad
- wait: bun-orcad
- run: pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}
Expand All @@ -216,6 +221,15 @@ jobs:
if-no-files-found: error
retention-days: 7
overwrite: true
- name: Keep the Windows CLI launchers for the desktop template
if: inputs.build_template && matrix.os == 'windows-2022'
uses: actions/upload-artifact@v7
with:
name: orcad-windows-cli-launcher
path: .build/windows-cli-launcher/
if-no-files-found: error
retention-days: 7
overwrite: true
- uses: actions/setup-node@v6
if: runner.arch == 'X64'
with:
Expand Down Expand Up @@ -458,6 +472,11 @@ jobs:
with:
name: orcad-windows-process-tree
path: .build/windows-process-tree
- name: Collect the Windows CLI launchers
uses: actions/download-artifact@v8
with:
name: orcad-windows-cli-launcher
path: .build/windows-cli-launcher
- name: Merge the lanes and gate the full slot matrix
shell: bash
run: |
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@ jobs:
config/scripts/ensure-native-runtime-job-ownership.test.mjs
config/scripts/verify-packaged-node-pty-job-ownership.test.mjs
config/scripts/windows-pe-machine.test.mjs
config/scripts/build-windows-cli-launcher.test.mjs
config/scripts/script-module-dependencies.test.mjs
src/main/windows-registry-addon.test.ts
config/scripts/windows-process-tree-gyp-path.test.mjs
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/ssh-windows-hosts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ on:
- 'config/scripts/build-orcad-*.mjs'
- 'config/scripts/orcad-prebuild-*.mjs'
- 'config/scripts/orcad-windows-process-tree.mjs'
- 'config/scripts/orcad-windows-cli-launcher.mjs'
- 'config/scripts/build-windows-cli-launcher.mjs'
- 'native/windows-cli-launcher/**'
- 'src/main/orcad/orcad-cli-launcher.ts'
- '.github/actions/prepare-windows-cli-launcher/**'
- 'config/scripts/build-relay.mjs'
- 'config/patches/node-pty*'
- 'config/patches/@vscode__windows-process-tree*'
Expand Down Expand Up @@ -150,6 +155,9 @@ jobs:
with:
resolve-windows-cache: ${{ github.event_name == 'pull_request' }}
restore-windows-cache: ${{ github.event_name == 'pull_request' }}
- uses: ./.github/actions/prepare-windows-cli-launcher
with:
arches: ${{ matrix.arch }}
- name: Build the win32 template and the relay
shell: bash
env:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/win-orcad-serve-switch-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ jobs:
with:
resolve-windows-cache: 'true'
restore-windows-cache: 'true'
- uses: ./.github/actions/prepare-windows-cli-launcher
with:
arches: x64
- name: Build the win32-x64 orcad template
shell: bash
run: |
Expand Down
2 changes: 2 additions & 0 deletions config/scripts/build-orcad.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { smokeForeignSqliteReaderWorker } from './foreign-sqlite-reader-worker-s
import { smokeSessionScannerService } from './session-scanner-service-smoke.mjs'
import { materializeWatcherPackage } from './orcad-watcher-package.mjs'
import { stageOrcadWindowsProcessTree } from './orcad-windows-process-tree.mjs'
import { stageOrcadWindowsCliLauncher } from './orcad-windows-cli-launcher.mjs'
import {
ORCAD_EMOJI_SHORTCODE_DATASET,
ORCAD_CLI_ENTRY_FILENAME,
Expand Down Expand Up @@ -157,6 +158,7 @@ writeFileSync(
stageNodePty(BUILD_TARGET)
await stageParcelWatcher(BUILD_TARGET)
stageOrcadWindowsProcessTree(ROOT, OUT_DIR, BUILD_TARGET)
stageOrcadWindowsCliLauncher(ROOT, OUT_DIR, BUILD_TARGET)
const emojiDatasetOutput = join(OUT_DIR, ORCAD_EMOJI_SHORTCODE_DATASET)
mkdirSync(dirname(emojiDatasetOutput), { recursive: true })
copyFileSync(
Expand Down
27 changes: 24 additions & 3 deletions config/scripts/build-windows-cli-launcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,20 @@ function defaultOutputPath(projectRoot) {
return join(projectRoot, 'native', 'windows-cli-launcher', '.build', 'orca.exe')
}

// Why explicit triples: the orcad template ships both, cross-compiled from one x64 runner.
const RUST_TARGETS = { x64: 'x86_64-pc-windows-msvc', arm64: 'aarch64-pc-windows-msvc' }

export function windowsCliLauncherRustTarget(arch) {
if (arch === undefined) {
return null
}
const target = RUST_TARGETS[arch]
if (!target) {
throw new Error(`Unsupported Windows CLI launcher architecture: ${arch}`)
}
return target
}

function readArg(name) {
const index = process.argv.indexOf(name)
return index !== -1 ? process.argv[index + 1] : undefined
Expand All @@ -61,6 +75,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
const { version } = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'))
// Throws on a version the PE cannot represent, before anything is compiled.
windowsCliLauncherFileVersion(version)
const rustTarget = windowsCliLauncherRustTarget(readArg('--arch'))
const fingerprint = windowsCliLauncherFingerprint(
[
join(crateRoot, 'src', 'main.rs'),
Expand All @@ -71,7 +86,7 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
iconPath,
join(repoRoot, 'config/scripts/build-windows-cli-launcher.mjs')
],
version
rustTarget ? `${version}\0${rustTarget}` : version
)
const outputPath = readArg('--output') ?? defaultOutputPath(repoRoot)

Expand All @@ -92,7 +107,8 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
'--manifest-path',
manifestPath,
'--target-dir',
targetDirectory
targetDirectory,
...(rustTarget ? ['--target', rustTarget] : [])
],
{
cwd: crateRoot,
Expand All @@ -119,7 +135,12 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
process.exit(result.status ?? 1)
}

const builtPath = join(targetDirectory, 'release', 'orca.exe')
const builtPath = join(
targetDirectory,
...(rustTarget ? [rustTarget] : []),
'release',
'orca.exe'
)
const vcRuntimeImports = findDynamicVcRuntimeImports(
readPeImportedDllNames(readFileSync(builtPath))
)
Expand Down
76 changes: 76 additions & 0 deletions config/scripts/build-windows-cli-launcher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { describe, expect, it } from 'vitest'
import {
shouldReuseCompiledWindowsCliLauncher,
windowsCliLauncherFileVersion,
windowsCliLauncherRustTarget,
windowsCliLauncherFingerprint
} from './build-windows-cli-launcher.mjs'
import { findDynamicVcRuntimeImports, readPeImportedDllNames } from './windows-pe-imports.mjs'
Expand Down Expand Up @@ -245,6 +246,81 @@ describe('Windows CLI launcher', () => {
}
})

it('cross-compiles only the architectures the orcad template ships', () => {
expect(windowsCliLauncherRustTarget(undefined)).toBeNull()
expect(windowsCliLauncherRustTarget('x64')).toBe('x86_64-pc-windows-msvc')
expect(windowsCliLauncherRustTarget('arm64')).toBe('aarch64-pc-windows-msvc')
expect(() => windowsCliLauncherRustTarget('ia32')).toThrow('Unsupported')
})

itWindows('runs a server profile CLI on pinned Node with argv and profile intact', () => {
const root = mkdtempSync(join(tmpdir(), 'orca server launcher '))
try {
const profile = join(root, 'server profile')
const launcherPath = join(profile, 'cli', 'bin', 'orca.exe')
const cliPath = join(root, 'server slot', 'out', 'cli', 'index.js')
mkdirSync(dirname(cliPath), { recursive: true })
writeFileSync(
cliPath,
`process.stdout.write(JSON.stringify({
argv: process.argv.slice(2),
profile: process.env.ORCA_USER_DATA_PATH,
electronRunAsNode: process.env.ELECTRON_RUN_AS_NODE ?? null,
nodeOptions: process.env.NODE_OPTIONS ?? null
}))\n`,
'utf8'
)
const build = spawnSync(
process.execPath,
['config/scripts/build-windows-cli-launcher.mjs', '--output', launcherPath],
{ cwd: projectRoot, encoding: 'utf8' }
)
expect(build.status, `${build.stdout}\n${build.stderr}`).toBe(0)
writeFileSync(
join(dirname(launcherPath), 'orca-launcher.cfg'),
`node=${process.execPath}\ncli=${cliPath}\nuser-data=${profile}\n`
)

const inherited = {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
NODE_OPTIONS: '--require C:\\missing\\preload.js',
ORCA_USER_DATA_PATH: 'C:\\another\\owner',
ORCA_TEST_LAUNCHER: launcherPath
}
const expected = (body) => ({
argv: ['orchestration', 'send', '--body', body, '--json'],
profile,
electronRunAsNode: null,
nodeOptions: null
})
// Windows PowerShell 5.1 itself drops embedded double quotes, so its body has none.
const shellBody = 'line one & %PATH% ^caret\n\nline three'
const powershell = spawnSync(
'powershell.exe',
[
'-NoProfile',
'-NonInteractive',
'-Command',
'& $env:ORCA_TEST_LAUNCHER orchestration send --body $env:ORCA_TEST_BODY --json'
],
{ encoding: 'utf8', env: { ...inherited, ORCA_TEST_BODY: shellBody } }
)
expect(powershell.status, powershell.stderr).toBe(0)
expect(JSON.parse(powershell.stdout)).toEqual(expected(shellBody))

const body = 'line one "quoted" \\"escaped\\" & %PATH% ^caret\\\n\nline three\\'
const direct = spawnSync(launcherPath, expected(body).argv, {
encoding: 'utf8',
env: inherited
})
expect(direct.status, direct.stderr).toBe(0)
expect(JSON.parse(direct.stdout)).toEqual(expected(body))
} finally {
removeFixtureTree(root)
}
})

itWindows('survives an inherited environment block containing PATH and Path', () => {
const appRoot = mkdtempSync(join(tmpdir(), 'orca duplicate path launcher '))
try {
Expand Down
1 change: 1 addition & 0 deletions config/scripts/node-server-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const BUILD_SCRIPTS = [
'config/scripts/orcad-windows-prebuild-cache.mjs',
'config/scripts/orcad-prebuild-smoke-child.cjs',
'config/scripts/build-windows-process-tree-relay-addon.mjs',
'config/scripts/build-windows-cli-launcher.mjs',
'config/scripts/run-node-server-tests.mjs',
'config/vitest.config.ts',
'config/scripts/happy-dom-offscreen-canvas.ts',
Expand Down
63 changes: 63 additions & 0 deletions config/scripts/orcad-windows-cli-launcher.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { copyFileSync, existsSync, mkdirSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { runProcessSync } from '@orca/process-host'
import { ORCAD_WINDOWS_CLI_LAUNCHER_FILENAME } from '../../src/shared/orcad-artifacts.ts'

const { PE_MACHINE, describePeMachine, readPeMachine } = createRequire(import.meta.url)(
'./windows-pe-machine.cjs'
)

export function orcadWindowsCliLauncherSource(root, arch) {
return join(root, '.build', 'windows-cli-launcher', arch, 'orca.exe')
}

/** A Windows slot's `orca` command; a Windows build host compiles a missing one. */
export function stageOrcadWindowsCliLauncher(
root,
outputDir,
target,
host = { platform: process.platform },
compile = compileOrcadWindowsCliLauncher
) {
if (!target.startsWith('win32-')) {
return
}
const arch = target.slice('win32-'.length)
const source = orcadWindowsCliLauncherSource(root, arch)
if (!existsSync(source)) {
if (host.platform !== 'win32') {
throw new Error(
`Orcad ${target} requires the native CLI launcher. On Windows, run: ` +
`node config/scripts/build-windows-cli-launcher.mjs --arch ${arch} --output ${source}`
)
}
compile(root, arch, source)
}
const machine = readPeMachine(source)
if (machine !== PE_MACHINE[arch]) {
throw new Error(`Orcad ${target} CLI launcher has ${describePeMachine(machine)}`)
}
const destination = join(outputDir, ...ORCAD_WINDOWS_CLI_LAUNCHER_FILENAME.split('/'))
mkdirSync(dirname(destination), { recursive: true })
copyFileSync(source, destination)
}

function compileOrcadWindowsCliLauncher(root, arch, output) {
const result = runProcessSync({
program: process.execPath,
args: [
join(root, 'config/scripts/build-windows-cli-launcher.mjs'),
'--arch',
arch,
'--output',
output
],
cwd: root,
stdio: 'inherit',
timeoutMs: null
})
if (result.code !== 0) {
throw new Error(`Windows CLI launcher build for ${arch} failed with exit ${result.code}`)
}
}
Loading
Loading