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
10 changes: 2 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
195 changes: 195 additions & 0 deletions scripts/package-apple-runner-source.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#!/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 = '<swift source>') {
const lines = source.match(/[^\n]*\n|[^\n]+/g) ?? [];
const state = {
output: [],
strippedBlocks: 0,
skippedDepth: 0,
};

for (const line of lines) {
consumeSwiftLine(state, line);
}

if (state.skippedDepth !== 0) {
throw new Error(`Unterminated ${UNIT_TEST_CONDITION} block in ${filePath}`);
}

return {
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) {
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);
}
}

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) {
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) {
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 };
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);
}

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).`,
);
}
}
24 changes: 21 additions & 3 deletions scripts/size-report.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -132,11 +133,24 @@ 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) {
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'],
});
}

function collectStartupBenchmarks(root, runs) {
return {
runs,
Expand Down Expand Up @@ -297,7 +311,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)} |`;
Expand Down Expand Up @@ -369,7 +385,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.',
);
}
}
}
Expand Down
89 changes: 89 additions & 0 deletions src/__tests__/apple-runner-package-source.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading