From 6499fb26cf501ae0bd82b5b7750ec4b72dbb1731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 6 Jul 2026 11:14:23 +0200 Subject: [PATCH 1/4] build: strip runner unit-test blocks from package --- package.json | 10 +- scripts/package-apple-runner-source.mjs | 166 ++++++++++++++++++ scripts/size-report.mjs | 20 ++- .../apple-runner-package-source.test.ts | 89 ++++++++++ .../core/__tests__/runner-source.test.ts | 41 +++++ .../apple/core/runner/runner-artifact.ts | 8 +- .../core/runner/runner-cache-metadata.ts | 3 +- .../apple/core/runner/runner-source.ts | 17 ++ 8 files changed, 336 insertions(+), 18 deletions(-) create mode 100644 scripts/package-apple-runner-source.mjs create mode 100644 src/__tests__/apple-runner-package-source.test.ts create mode 100644 src/platforms/apple/core/__tests__/runner-source.test.ts create mode 100644 src/platforms/apple/core/runner/runner-source.ts diff --git a/package.json b/package.json index b4cd3c0bf..12fdf9740 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "build:android-snapshot-helper": "sh ./scripts/build-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper": "sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") .tmp/android-snapshot-helper", "package:android-snapshot-helper:npm": "rm -rf android-snapshot-helper/dist && sh ./scripts/package-android-snapshot-helper.sh $(node -p \"require('./package.json').version\") v$(node -p \"require('./package.json').version\") android-snapshot-helper/dist", + "package:apple-runner:npm": "node scripts/package-apple-runner-source.mjs", "build:android-multitouch-helper": "sh ./scripts/build-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper", "package:android-multitouch-helper": "sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") .tmp/android-multitouch-helper", "package:android-multitouch-helper:npm": "rm -rf android-multitouch-helper/dist && sh ./scripts/package-android-multitouch-helper.sh $(node -p \"require('./package.json').version\") android-multitouch-helper/dist", @@ -116,7 +117,7 @@ "check:tooling": "pnpm lint && pnpm typecheck && pnpm check:layering && pnpm check:mcp-metadata && pnpm build", "check:unit": "pnpm test:unit && pnpm test:smoke", "check": "pnpm check:tooling && pnpm check:fallow && pnpm check:unit", - "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm", + "prepack": "pnpm check:mcp-metadata && pnpm build:all && pnpm package:apple-runner:npm && pnpm package:android-snapshot-helper:npm && pnpm package:android-multitouch-helper:npm", "typecheck": "tsgo -p tsconfig.json", "test-app:install": "pnpm install --dir examples/test-app --ignore-workspace", "test-app:start": "pnpm --dir examples/test-app start", @@ -149,13 +150,6 @@ "files": [ "bin", "dist", - "apple-runner", - "!apple-runner/**/.build", - "!apple-runner/**/.swiftpm", - "!apple-runner/**/xcuserdata", - "!apple-runner/**/*.xcuserstate", - "!apple-runner/README.md", - "!apple-runner/RUNNER_PROTOCOL.md", "macos-helper", "!macos-helper/**/.build", "android-snapshot-helper/dist", diff --git a/scripts/package-apple-runner-source.mjs b/scripts/package-apple-runner-source.mjs new file mode 100644 index 000000000..2313c95f6 --- /dev/null +++ b/scripts/package-apple-runner-source.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const UNIT_TEST_CONDITION = 'AGENT_DEVICE_RUNNER_UNIT_TESTS'; +const SOURCE_DIR = path.join('apple-runner'); +const OUTPUT_DIR = path.join('dist', 'apple-runner'); +const SKIPPED_DIR_NAMES = new Set(['.build', '.swiftpm', 'xcuserdata']); +const SKIPPED_ROOT_FILES = new Set(['README.md', 'RUNNER_PROTOCOL.md']); + +export function packageAppleRunnerSource(options = {}) { + const root = path.resolve(options.root ?? process.cwd()); + const sourceRoot = path.join(root, SOURCE_DIR); + const outputRoot = path.join(root, OUTPUT_DIR); + if (!fs.existsSync(sourceRoot)) { + throw new Error(`Apple runner source not found at ${sourceRoot}`); + } + + fs.rmSync(outputRoot, { recursive: true, force: true }); + const summary = { + outputRoot, + copiedFiles: 0, + strippedFiles: 0, + strippedBlocks: 0, + }; + + copyDirectory(sourceRoot, outputRoot, '', summary); + return summary; +} + +export function stripRunnerUnitTestBlocks(source, filePath = '') { + const lines = source.match(/[^\n]*\n|[^\n]+/g) ?? []; + const output = []; + let strippedBlocks = 0; + let skippedDepth = 0; + + for (const line of lines) { + if (skippedDepth === 0) { + if (isRunnerUnitTestBlockStart(line)) { + skippedDepth = 1; + strippedBlocks += 1; + continue; + } + output.push(line); + continue; + } + + if (isConditionalStart(line)) { + skippedDepth += 1; + } + if (isConditionalEnd(line)) { + skippedDepth -= 1; + } + } + + if (skippedDepth !== 0) { + throw new Error(`Unterminated ${UNIT_TEST_CONDITION} block in ${filePath}`); + } + + return { + contents: output.join(''), + strippedBlocks, + }; +} + +function copyDirectory(sourceDir, outputDir, relativeDir, summary) { + fs.mkdirSync(outputDir, { recursive: true }); + const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); + + for (const entry of entries) { + const relativePath = path.join(relativeDir, entry.name); + if (shouldSkipEntry(entry, relativePath)) { + continue; + } + + const sourcePath = path.join(sourceDir, entry.name); + const outputPath = path.join(outputDir, entry.name); + if (entry.isDirectory()) { + copyDirectory(sourcePath, outputPath, relativePath, summary); + continue; + } + if (!entry.isFile()) { + continue; + } + + copyFile(sourcePath, outputPath, summary); + } +} + +function copyFile(sourcePath, outputPath, summary) { + if (path.extname(sourcePath) !== '.swift') { + fs.copyFileSync(sourcePath, outputPath); + summary.copiedFiles += 1; + return; + } + + const source = fs.readFileSync(sourcePath, 'utf8'); + const stripped = stripRunnerUnitTestBlocks(source, sourcePath); + fs.writeFileSync(outputPath, stripped.contents); + summary.copiedFiles += 1; + if (stripped.strippedBlocks > 0) { + summary.strippedFiles += 1; + summary.strippedBlocks += stripped.strippedBlocks; + } +} + +function shouldSkipEntry(entry, relativePath) { + if (entry.isDirectory() && SKIPPED_DIR_NAMES.has(entry.name)) { + return true; + } + if (entry.isFile() && entry.name.endsWith('.xcuserstate')) { + return true; + } + return entry.isFile() && !relativePath.includes(path.sep) && SKIPPED_ROOT_FILES.has(entry.name); +} + +function isRunnerUnitTestBlockStart(line) { + return new RegExp(`^\\s*#if\\s+${UNIT_TEST_CONDITION}(?:\\b|$)`).test(line); +} + +function isConditionalStart(line) { + return /^\s*#if\b/.test(line); +} + +function isConditionalEnd(line) { + return /^\s*#endif\b/.test(line); +} + +function parseArgs(argv) { + const parsed = { root: process.cwd(), quiet: false }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === '--quiet') { + parsed.quiet = true; + continue; + } + if (arg === '--root') { + const value = argv[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error('--root requires a path'); + } + parsed.root = value; + index += 1; + continue; + } + throw new Error(`Unknown argument: ${arg}`); + } + return parsed; +} + +function isMainModule() { + return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +} + +if (isMainModule()) { + const options = parseArgs(process.argv.slice(2)); + const summary = packageAppleRunnerSource(options); + if (!options.quiet) { + const relativeOutput = path.relative(path.resolve(options.root), summary.outputRoot); + console.log( + `Packaged Apple runner source at ${relativeOutput} ` + + `(${summary.copiedFiles} files, stripped ${summary.strippedBlocks} unit-test blocks).`, + ); + } +} diff --git a/scripts/size-report.mjs b/scripts/size-report.mjs index a027307ba..0b39cc6dd 100644 --- a/scripts/size-report.mjs +++ b/scripts/size-report.mjs @@ -105,6 +105,7 @@ function collectReport(root, options) { if (jsFiles.length === 0) { throw new Error('No dist/src JavaScript files found. Run `pnpm build` before measuring size.'); } + prepareGeneratedPackageAssets(root); const chunks = jsFiles .map((file) => { @@ -132,11 +133,20 @@ function collectReport(root, options) { generatedAt: new Date().toISOString(), js, npmPack: collectNpmPack(root), - ...(options.startupRuns > 0 ? { startup: collectStartupBenchmarks(root, options.startupRuns) } : {}), + ...(options.startupRuns > 0 + ? { startup: collectStartupBenchmarks(root, options.startupRuns) } + : {}), chunks: chunks.slice(0, 20), }; } +function prepareGeneratedPackageAssets(root) { + execFileSync(process.execPath, ['scripts/package-apple-runner-source.mjs', '--quiet'], { + cwd: root, + stdio: ['ignore', 'ignore', 'inherit'], + }); +} + function collectStartupBenchmarks(root, runs) { return { runs, @@ -297,7 +307,9 @@ function formatDiff(base, current) { function formatStartupBenchmarks(startup, baseStartup) { if (!startup) return ''; - const baseByName = new Map((baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark])); + const baseByName = new Map( + (baseStartup?.benchmarks ?? []).map((benchmark) => [benchmark.name, benchmark]), + ); const rows = startup.benchmarks.map((benchmark) => { const base = baseByName.get(benchmark.name); return `| ${benchmark.name} | ${formatMaybeMs(base?.medianMs)} | ${formatMs(benchmark.medianMs)} | ${formatMsDiff(base?.medianMs, benchmark.medianMs)} |`; @@ -369,7 +381,9 @@ function readGitHubCommentConfig(explicitPrNumber) { function assertGitHubCommentConfig(token, repository, prNumber) { for (const value of [token, repository, prNumber]) { if (!value) { - throw new Error('GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.'); + throw new Error( + 'GITHUB_TOKEN, GITHUB_REPOSITORY, and PR number are required to post a comment.', + ); } } } diff --git a/src/__tests__/apple-runner-package-source.test.ts b/src/__tests__/apple-runner-package-source.test.ts new file mode 100644 index 000000000..a86c44237 --- /dev/null +++ b/src/__tests__/apple-runner-package-source.test.ts @@ -0,0 +1,89 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { onTestFinished, test } from 'vitest'; +import { runCmd } from '../utils/exec.ts'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const packageScript = path.join(repoRoot, 'scripts', 'package-apple-runner-source.mjs'); + +test('package apple runner source strips unit-test blocks without mutating checkout source', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-package-')); + onTestFinished(() => fs.rmSync(root, { recursive: true, force: true })); + + writeFixtureFile(root, 'apple-runner/README.md', 'developer docs\n'); + writeFixtureFile(root, 'apple-runner/.build/cache.txt', 'cache\n'); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj', + '', + ); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate', + 'state\n', + ); + writeFixtureFile( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + [ + 'extension RunnerTests {', + ' func runtimeHelper() {}', + '#if AGENT_DEVICE_RUNNER_UNIT_TESTS', + ' func unitOnlyHelper() {', + ' #if os(iOS)', + ' print("nested platform guard should disappear with the unit-test block")', + ' #endif', + ' }', + '#endif', + ' #if os(macOS)', + ' func macOnlyRuntimeHelper() {}', + ' #endif', + '}', + '', + ].join('\n'), + ); + + await runCmd(process.execPath, [packageScript, '--root', root, '--quiet']); + + const sourceSwiftPath = path.join( + root, + 'apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + ); + const packagedSwiftPath = path.join( + root, + 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Feature.swift', + ); + const sourceSwift = fs.readFileSync(sourceSwiftPath, 'utf8'); + const packagedSwift = fs.readFileSync(packagedSwiftPath, 'utf8'); + + assert.match(sourceSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/); + assert.doesNotMatch(packagedSwift, /AGENT_DEVICE_RUNNER_UNIT_TESTS/); + assert.doesNotMatch(packagedSwift, /unitOnlyHelper/); + assert.match(packagedSwift, /runtimeHelper/); + assert.match(packagedSwift, /#if os\(macOS\)/); + assert.ok( + fs.existsSync( + path.join(root, 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj'), + ), + ); + assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/README.md')), false); + assert.equal(fs.existsSync(path.join(root, 'dist/apple-runner/.build/cache.txt')), false); + assert.equal( + fs.existsSync( + path.join( + root, + 'dist/apple-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcuserdata/user.xcuserstate', + ), + ), + false, + ); +}); + +function writeFixtureFile(root: string, relativePath: string, contents: string): void { + const filePath = path.join(root, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents); +} diff --git a/src/platforms/apple/core/__tests__/runner-source.test.ts b/src/platforms/apple/core/__tests__/runner-source.test.ts new file mode 100644 index 000000000..5aac0244e --- /dev/null +++ b/src/platforms/apple/core/__tests__/runner-source.test.ts @@ -0,0 +1,41 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { onTestFinished, test } from 'vitest'; +import { + resolveAppleRunnerProjectPath, + resolveAppleRunnerSourceRoot, +} from '../runner/runner-source.ts'; + +test('resolveAppleRunnerSourceRoot prefers checkout source over packaged source', () => { + const root = makeTempRoot(); + const checkoutSource = path.join(root, 'apple-runner', 'AgentDeviceRunner'); + const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner'); + fs.mkdirSync(path.join(checkoutSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + + assert.equal(resolveAppleRunnerSourceRoot(root), checkoutSource); + assert.equal( + resolveAppleRunnerProjectPath(root), + path.join(checkoutSource, 'AgentDeviceRunner.xcodeproj'), + ); +}); + +test('resolveAppleRunnerSourceRoot falls back to packaged source', () => { + const root = makeTempRoot(); + const packagedSource = path.join(root, 'dist', 'apple-runner', 'AgentDeviceRunner'); + fs.mkdirSync(path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), { recursive: true }); + + assert.equal(resolveAppleRunnerSourceRoot(root), packagedSource); + assert.equal( + resolveAppleRunnerProjectPath(root), + path.join(packagedSource, 'AgentDeviceRunner.xcodeproj'), + ); +}); + +function makeTempRoot(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-runner-source-')); + onTestFinished(() => fs.rmSync(root, { recursive: true, force: true })); + return root; +} diff --git a/src/platforms/apple/core/runner/runner-artifact.ts b/src/platforms/apple/core/runner/runner-artifact.ts index 1a75c9290..316d2e50d 100644 --- a/src/platforms/apple/core/runner/runner-artifact.ts +++ b/src/platforms/apple/core/runner/runner-artifact.ts @@ -40,6 +40,7 @@ import { resolveRunnerBuildDestination, resolveRunnerXctestrunHints, } from '../apple-runner-platform.ts'; +import { resolveAppleRunnerProjectPath } from './runner-source.ts'; export { prepareXctestrunWithEnv } from './runner-artifact-env.ts'; const runnerXctestrunBuildLocks = new Map>(); @@ -234,12 +235,7 @@ async function buildXctestrunArtifact(params: { reason: ExistingXctestrunState['reason']; }): Promise { const { device, options, projectRoot, expectedCacheMetadata, derived, cache, reason } = params; - const projectPath = path.join( - projectRoot, - 'apple-runner', - 'AgentDeviceRunner', - 'AgentDeviceRunner.xcodeproj', - ); + const projectPath = resolveAppleRunnerProjectPath(projectRoot); if (!fs.existsSync(projectPath)) { throw new AppError('COMMAND_FAILED', 'iOS runner project not found', { projectPath }); diff --git a/src/platforms/apple/core/runner/runner-cache-metadata.ts b/src/platforms/apple/core/runner/runner-cache-metadata.ts index 4177c6289..6151beaa8 100644 --- a/src/platforms/apple/core/runner/runner-cache-metadata.ts +++ b/src/platforms/apple/core/runner/runner-cache-metadata.ts @@ -13,6 +13,7 @@ import { resolveRunnerPlatformName, resolveRunnerSdkName, } from '../apple-runner-platform.ts'; +import { resolveAppleRunnerSourceRoot } from './runner-source.ts'; const DEFAULT_IOS_RUNNER_APP_BUNDLE_ID = 'com.callstack.agentdevice.runner'; const RUNNER_DERIVED_ROOT = path.join(os.homedir(), '.agent-device', 'apple-runner'); @@ -232,7 +233,7 @@ type RunnerSourceFingerprintCacheEntry = { const runnerSourceFingerprintCache = new Map(); function computeRunnerSourceFingerprint(projectRoot: string): string { - const runnerRoot = path.join(projectRoot, 'apple-runner', 'AgentDeviceRunner'); + const runnerRoot = resolveAppleRunnerSourceRoot(projectRoot); const files = collectRunnerSourceFiles(runnerRoot); const fileStatsFingerprint = computeRunnerSourceFileStatsFingerprint(runnerRoot, files); const cached = runnerSourceFingerprintCache.get(runnerRoot); diff --git a/src/platforms/apple/core/runner/runner-source.ts b/src/platforms/apple/core/runner/runner-source.ts new file mode 100644 index 000000000..ff8db8e96 --- /dev/null +++ b/src/platforms/apple/core/runner/runner-source.ts @@ -0,0 +1,17 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const APPLE_RUNNER_SOURCE_ROOT = path.join('apple-runner', 'AgentDeviceRunner'); +const PACKAGED_APPLE_RUNNER_SOURCE_ROOT = path.join('dist', 'apple-runner', 'AgentDeviceRunner'); + +export function resolveAppleRunnerSourceRoot(projectRoot: string): string { + const checkoutSourceRoot = path.join(projectRoot, APPLE_RUNNER_SOURCE_ROOT); + if (fs.existsSync(checkoutSourceRoot)) { + return checkoutSourceRoot; + } + return path.join(projectRoot, PACKAGED_APPLE_RUNNER_SOURCE_ROOT); +} + +export function resolveAppleRunnerProjectPath(projectRoot: string): string { + return path.join(resolveAppleRunnerSourceRoot(projectRoot), 'AgentDeviceRunner.xcodeproj'); +} From 4342b5ea941cb5a1f4583b6a8e64b48d1e0f2276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 6 Jul 2026 11:27:35 +0200 Subject: [PATCH 2/4] ci: fix package size and fallow checks --- scripts/package-apple-runner-source.mjs | 151 ++++++++++++++---------- scripts/size-report.mjs | 6 +- 2 files changed, 95 insertions(+), 62 deletions(-) diff --git a/scripts/package-apple-runner-source.mjs b/scripts/package-apple-runner-source.mjs index 2313c95f6..bfc4b25a7 100644 --- a/scripts/package-apple-runner-source.mjs +++ b/scripts/package-apple-runner-source.mjs @@ -31,59 +31,70 @@ export function packageAppleRunnerSource(options = {}) { export function stripRunnerUnitTestBlocks(source, filePath = '') { const lines = source.match(/[^\n]*\n|[^\n]+/g) ?? []; - const output = []; - let strippedBlocks = 0; - let skippedDepth = 0; + const state = { + output: [], + strippedBlocks: 0, + skippedDepth: 0, + }; for (const line of lines) { - if (skippedDepth === 0) { - if (isRunnerUnitTestBlockStart(line)) { - skippedDepth = 1; - strippedBlocks += 1; - continue; - } - output.push(line); - continue; - } - - if (isConditionalStart(line)) { - skippedDepth += 1; - } - if (isConditionalEnd(line)) { - skippedDepth -= 1; - } - } - - if (skippedDepth !== 0) { + consumeSwiftLine(state, line); + } + + if (state.skippedDepth !== 0) { throw new Error(`Unterminated ${UNIT_TEST_CONDITION} block in ${filePath}`); } return { - contents: output.join(''), - strippedBlocks, + contents: state.output.join(''), + strippedBlocks: state.strippedBlocks, }; } +function consumeSwiftLine(state, line) { + if (state.skippedDepth > 0) { + consumeSkippedConditionalLine(state, line); + return; + } + if (isRunnerUnitTestBlockStart(line)) { + state.skippedDepth = 1; + state.strippedBlocks += 1; + return; + } + state.output.push(line); +} + +function consumeSkippedConditionalLine(state, line) { + if (isConditionalStart(line)) { + state.skippedDepth += 1; + } + if (isConditionalEnd(line)) { + state.skippedDepth -= 1; + } +} + function copyDirectory(sourceDir, outputDir, relativeDir, summary) { fs.mkdirSync(outputDir, { recursive: true }); const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); for (const entry of entries) { - const relativePath = path.join(relativeDir, entry.name); - if (shouldSkipEntry(entry, relativePath)) { - continue; - } - - const sourcePath = path.join(sourceDir, entry.name); - const outputPath = path.join(outputDir, entry.name); - if (entry.isDirectory()) { - copyDirectory(sourcePath, outputPath, relativePath, summary); - continue; - } - if (!entry.isFile()) { - continue; - } + copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary); + } +} + +function copyDirectoryEntry(entry, sourceDir, outputDir, relativeDir, summary) { + const relativePath = path.join(relativeDir, entry.name); + if (shouldSkipEntry(entry, relativePath)) { + return; + } + const sourcePath = path.join(sourceDir, entry.name); + const outputPath = path.join(outputDir, entry.name); + if (entry.isDirectory()) { + copyDirectory(sourcePath, outputPath, relativePath, summary); + return; + } + if (entry.isFile()) { copyFile(sourcePath, outputPath, summary); } } @@ -106,13 +117,23 @@ function copyFile(sourcePath, outputPath, summary) { } function shouldSkipEntry(entry, relativePath) { - if (entry.isDirectory() && SKIPPED_DIR_NAMES.has(entry.name)) { - return true; - } - if (entry.isFile() && entry.name.endsWith('.xcuserstate')) { - return true; - } - return entry.isFile() && !relativePath.includes(path.sep) && SKIPPED_ROOT_FILES.has(entry.name); + return shouldSkipDirectory(entry) || shouldSkipFile(entry, relativePath); +} + +function shouldSkipDirectory(entry) { + return entry.isDirectory() && SKIPPED_DIR_NAMES.has(entry.name); +} + +function shouldSkipFile(entry, relativePath) { + return entry.isFile() && (isXcodeUserStateFile(entry) || isSkippedRootFile(entry, relativePath)); +} + +function isXcodeUserStateFile(entry) { + return entry.name.endsWith('.xcuserstate'); +} + +function isSkippedRootFile(entry, relativePath) { + return !relativePath.includes(path.sep) && SKIPPED_ROOT_FILES.has(entry.name); } function isRunnerUnitTestBlockStart(line) { @@ -129,26 +150,34 @@ function isConditionalEnd(line) { function parseArgs(argv) { const parsed = { root: process.cwd(), quiet: false }; - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index]; - if (arg === '--quiet') { - parsed.quiet = true; - continue; - } - if (arg === '--root') { - const value = argv[index + 1]; - if (!value || value.startsWith('--')) { - throw new Error('--root requires a path'); - } - parsed.root = value; - index += 1; - continue; - } - throw new Error(`Unknown argument: ${arg}`); + let index = 0; + while (index < argv.length) { + index = parseArg(argv, index, parsed); } return parsed; } +function parseArg(argv, index, parsed) { + const arg = argv[index]; + if (arg === '--quiet') { + parsed.quiet = true; + return index + 1; + } + if (arg === '--root') { + return parseRootArg(argv, index, parsed); + } + throw new Error(`Unknown argument: ${arg}`); +} + +function parseRootArg(argv, index, parsed) { + const value = argv[index + 1]; + if (!value || value.startsWith('--')) { + throw new Error('--root requires a path'); + } + parsed.root = value; + return index + 2; +} + function isMainModule() { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); } diff --git a/scripts/size-report.mjs b/scripts/size-report.mjs index 0b39cc6dd..17a574d57 100644 --- a/scripts/size-report.mjs +++ b/scripts/size-report.mjs @@ -141,7 +141,11 @@ function collectReport(root, options) { } function prepareGeneratedPackageAssets(root) { - execFileSync(process.execPath, ['scripts/package-apple-runner-source.mjs', '--quiet'], { + const packageAppleRunnerScript = path.join(root, 'scripts', 'package-apple-runner-source.mjs'); + if (!fs.existsSync(packageAppleRunnerScript)) { + return; + } + execFileSync(process.execPath, [packageAppleRunnerScript, '--quiet'], { cwd: root, stdio: ['ignore', 'ignore', 'inherit'], }); From fc9b77f4a1d0b1db533d130f552d437f5506cb2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 6 Jul 2026 11:39:07 +0200 Subject: [PATCH 3/4] fix: resolve packaged recording scripts --- src/recording/__tests__/overlay.test.ts | 26 +++++++++++++- src/recording/overlay.ts | 45 +++++++++++++++++-------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/recording/__tests__/overlay.test.ts b/src/recording/__tests__/overlay.test.ts index 70b5d2945..e24ca6707 100644 --- a/src/recording/__tests__/overlay.test.ts +++ b/src/recording/__tests__/overlay.test.ts @@ -27,7 +27,11 @@ vi.mock('../../utils/video.ts', () => ({ waitForPlayableVideo: vi.fn(async () => {}), })); -import { overlayRecordingTouches, resizeRecording } from '../overlay.ts'; +import { + buildRecordingScriptPathCandidates, + overlayRecordingTouches, + resizeRecording, +} from '../overlay.ts'; import { AppError } from '../../kernel/errors.ts'; import { runCmd } from '../../utils/exec.ts'; @@ -151,3 +155,23 @@ test('resize forwards the requested high export preset', async () => { expect.arrayContaining(['--max-size', '720', '--quality', 'high']), ); }); + +test('recording script candidates include packaged dist apple-runner source', () => { + const packageRoot = path.join(tmpDir, 'package'); + const scriptPath = path.join( + packageRoot, + 'dist/apple-runner/AgentDeviceRunner/RecordingScripts/recording-overlay.swift', + ); + fs.mkdirSync(path.dirname(scriptPath), { recursive: true }); + fs.writeFileSync(scriptPath, 'print("overlay")\n'); + + const candidates = buildRecordingScriptPathCandidates( + 'recording-overlay.swift', + path.join(packageRoot, 'dist/src'), + packageRoot, + tmpDir, + ); + const firstExisting = candidates.find((candidate) => fs.existsSync(candidate)); + + expect(firstExisting).toBe(scriptPath); +}); diff --git a/src/recording/overlay.ts b/src/recording/overlay.ts index 8d9932051..ec6fa7a8f 100644 --- a/src/recording/overlay.ts +++ b/src/recording/overlay.ts @@ -4,23 +4,40 @@ import { fileURLToPath } from 'node:url'; import { runCmd } from '../utils/exec.ts'; import { AppError } from '../kernel/errors.ts'; import { buildSwiftToolEnv, compileSwiftSourceFile } from '../utils/swift-cache.ts'; +import { findProjectRoot } from '../utils/version.ts'; import { waitForPlayableVideo, waitForStableFile } from '../utils/video.ts'; import { DEFAULT_RECORDING_EXPORT_QUALITY, type RecordingExportQuality, } from '../core/recording-export-quality.ts'; -function resolveScriptPath(scriptName: string): string { - const moduleDir = path.dirname(fileURLToPath(import.meta.url)); - const scriptCandidates = [ - fileURLToPath(new URL(`./${scriptName}`, import.meta.url)), - path.resolve(moduleDir, `../../apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`), - path.resolve( - moduleDir, - `../../../apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`, - ), - path.resolve(process.cwd(), `apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`), +export function buildRecordingScriptPathCandidates( + scriptName: string, + moduleDir: string, + projectRoot: string, + cwd: string, +): string[] { + const sourceScriptPath = `apple-runner/AgentDeviceRunner/RecordingScripts/${scriptName}`; + const packagedScriptPath = `dist/${sourceScriptPath}`; + return [ + path.resolve(moduleDir, scriptName), + path.resolve(projectRoot, sourceScriptPath), + path.resolve(moduleDir, `../${sourceScriptPath}`), + path.resolve(moduleDir, `../../${sourceScriptPath}`), + path.resolve(moduleDir, `../../../${sourceScriptPath}`), + path.resolve(projectRoot, packagedScriptPath), + path.resolve(cwd, sourceScriptPath), ]; +} + +export function resolveRecordingScriptPath(scriptName: string): string { + const moduleDir = path.dirname(fileURLToPath(import.meta.url)); + const scriptCandidates = buildRecordingScriptPathCandidates( + scriptName, + moduleDir, + findProjectRoot(), + process.cwd(), + ); for (const candidate of scriptCandidates) { if (fs.existsSync(candidate)) { @@ -29,7 +46,7 @@ function resolveScriptPath(scriptName: string): string { } throw new AppError('COMMAND_FAILED', `Missing recording helper script: ${scriptName}`, { - hint: 'Ensure apple-runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled with the package.', + hint: 'Ensure apple-runner/AgentDeviceRunner/RecordingScripts is present in this checkout or bundled under dist/apple-runner in the package.', scriptName, searchedPaths: scriptCandidates, }); @@ -49,17 +66,17 @@ export function getRecordingOverlaySupportWarning( } function getOverlayScriptPath(): string { - overlayScriptPath ??= resolveScriptPath('recording-overlay.swift'); + overlayScriptPath ??= resolveRecordingScriptPath('recording-overlay.swift'); return overlayScriptPath; } function getTrimScriptPath(): string { - trimScriptPath ??= resolveScriptPath('recording-trim.swift'); + trimScriptPath ??= resolveRecordingScriptPath('recording-trim.swift'); return trimScriptPath; } function getResizeScriptPath(): string { - resizeScriptPath ??= resolveScriptPath('recording-resize.swift'); + resizeScriptPath ??= resolveRecordingScriptPath('recording-resize.swift'); return resizeScriptPath; } From f3281914b87c9438e22814c58024a4d7fd78e784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 6 Jul 2026 11:41:55 +0200 Subject: [PATCH 4/4] fix: keep recording script resolver internal --- src/recording/overlay.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/recording/overlay.ts b/src/recording/overlay.ts index ec6fa7a8f..581d1ece7 100644 --- a/src/recording/overlay.ts +++ b/src/recording/overlay.ts @@ -30,7 +30,7 @@ export function buildRecordingScriptPathCandidates( ]; } -export function resolveRecordingScriptPath(scriptName: string): string { +function resolveRecordingScriptPath(scriptName: string): string { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const scriptCandidates = buildRecordingScriptPathCandidates( scriptName,