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
61 changes: 37 additions & 24 deletions scripts/build-capture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
// package publishes dist/ and Apache-2.0 §4(a) asks that whoever gets a binary gets its license too.
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { chmodSync, copyFileSync, cpSync, mkdirSync, readFileSync } from "node:fs";
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

Expand Down Expand Up @@ -41,35 +41,45 @@ function copyScrcpyServer() {
copyNotices("scrcpy");
}

// why: with more than one --arch, SwiftPM builds through Xcode's build system, and where that
// writes the binary differs between Xcode versions (26: apple/Products/Release, 27:
// out/Products/Release), so the folder comes from SwiftPM itself rather than a guess. Answers
// undefined when SwiftPM names no folder holding the program.
function builtProductPath(args, product) {
const binPath = spawnSync("swift", [...args, "--show-bin-path"], { encoding: "utf8" });
if (binPath.status !== 0) return undefined;
const built = join(binPath.stdout.trim(), product);
return existsSync(built) ? built : undefined;
}

// why: one product per call, so a build that fails on one program is not what skips the other;
// the caller decides what the pair of outcomes means.
function buildAndCopySwiftProduct(product, what, packagePath, scratch) {
const result = spawnSync(
"swift",
[
"build",
"-c",
"release",
"--product",
product,
"--package-path",
packagePath,
"--scratch-path",
scratch,
],
{ stdio: "inherit" },
);
function buildAndCopySwiftProduct(product, what, packagePath, scratch, archs) {
const args = [
"build",
"-c",
"release",
"--product",
product,
"--package-path",
packagePath,
"--scratch-path",
scratch,
...archs.flatMap((arch) => ["--arch", arch]),
];
const result = spawnSync("swift", args, { stdio: "inherit" });
// why: a failed spawn (no `swift` on the PATH) leaves `status` null, which is not 0 either, so
// this one test covers both the tool that is missing and the build that failed.
if (result.status !== 0) {
const built = result.status === 0 ? builtProductPath(args, product) : undefined;
if (built === undefined) {
console.warn(
`build-capture: could not build ${product} (is Xcode installed and selected?); ${what} will be unavailable`,
);
return false;
}
const target = join(dist, "bin", product);
mkdirSync(dirname(target), { recursive: true });
copyFileSync(join(scratch, "release", product), target);
copyFileSync(built, target);
chmodSync(target, 0o755);
return true;
}
Expand All @@ -82,13 +92,16 @@ function buildIosCapture() {
const packagePath = join(root, "vendor/screen-capture");
const scratch = join(packagePath, ".build/out");
// why: each program builds on its own -- the device program links a private framework the
// simulator program never needs, so one failing must not take the other's streams with it.
// simulator program never needs, so one failing must not take the other's streams with it. The
// simulator program runs on the worker's own CPU, and an Intel Mac or an x86_64 macOS VM is a
// worker too, so it ships universal; the device program needs Xcode 27, which runs on Apple
// silicon only, so it builds for the host alone.
const products = [
["simlock-screen-capture", "iOS screen streams"],
["simlock-device-capture", "physical iPhone streams"],
["simlock-screen-capture", "iOS screen streams", ["arm64", "x86_64"]],
["simlock-device-capture", "physical iPhone streams", []],
];
const copied = products.filter(([product, what]) =>
buildAndCopySwiftProduct(product, what, packagePath, scratch),
const copied = products.filter(([product, what, archs]) =>
buildAndCopySwiftProduct(product, what, packagePath, scratch, archs),
);
if (copied.length > 0) copyNotices("screen-capture");
}
Expand Down
24 changes: 18 additions & 6 deletions src/build-capture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ function scratchRepo(server: string, digest: string): string {
return root;
}

// why: a `swift` that writes the program where `swift build` would, so the claim about what ships
// why: a `swift` that writes the program where `swift build` would, and names that folder for
// `--show-bin-path`, so the claim about what ships
// beside the iOS programs is checked without a Swift toolchain. It honours `--product` like the
// real build does, and fails the products named in SIMLOCK_FAKE_SWIFT_FAIL so a test can have one
// program build and the other not.
Expand All @@ -59,14 +60,25 @@ function fakeSwift(repo: string): string {
"#!/bin/sh",
"product=''",
'scratch=""',
'archs=""',
"count=0",
"show=0",
'while [ "$#" -gt 0 ]; do',
' if [ "$1" = "--product" ]; then shift; product="$1"; fi',
' if [ "$1" = "--scratch-path" ]; then shift; scratch="$1"; fi',
' if [ "$1" = "--arch" ]; then shift; archs="$archs $1"; count=$((count + 1)); fi',
' if [ "$1" = "--show-bin-path" ]; then show=1; fi',
" shift",
"done",
// why: a folder no fixed path in the script would name, as Xcode 27 writes a universal build
// under out/Products/Release and Xcode 26 under apple/Products/Release; only
// --show-bin-path knows.
'out="$scratch/release"',
'if [ "$count" -gt 1 ]; then out="$scratch/xcode-$count/Products/Release"; fi',
'if [ "$show" = 1 ]; then echo "$out"; exit 0; fi',
'case ",$SIMLOCK_FAKE_SWIFT_FAIL," in *",$product,"*) exit 1 ;; esac',
'/bin/mkdir -p "$scratch/release"',
'echo program > "$scratch/release/$product"',
'/bin/mkdir -p "$out"',
'echo "program for:$archs" > "$out/$product"',
].join("\n"),
);
chmodSync(join(bin, "swift"), 0o755);
Expand Down Expand Up @@ -114,7 +126,7 @@ describe("scripts/build-capture.mjs", () => {
});

it.skipIf(process.platform !== "darwin")(
"builds each iOS capture program on its own, ships each one that built with serve-sim's NOTICE.md and license, and leaves out what did not build",
"builds each iOS capture program on its own, the simulator one universal (arm64 and x86_64) and the device one for the host alone, ships each one that built with serve-sim's NOTICE.md and license, and leaves out what did not build",
() => {
const repo = scratchRepo("the real server", sha256("the real server"));
const run = (path: string, fail = "") =>
Expand All @@ -132,8 +144,8 @@ describe("scripts/build-capture.mjs", () => {
expect(existsSync(shipped)).toBe(false);

run(fakeSwift(repo));
expect(existsSync(simulatorProgram)).toBe(true);
expect(existsSync(deviceProgram)).toBe(true);
expect(readFileSync(simulatorProgram, "utf8")).toBe("program for: arm64 x86_64\n");
expect(readFileSync(deviceProgram, "utf8")).toBe("program for:\n");
expect(readFileSync(join(shipped, "NOTICE.md"), "utf8")).toBe("notice of screen-capture");
expect(readFileSync(join(shipped, "LICENSES", "serve-sim.APACHE-2.0.txt"), "utf8")).toBe(
"license of screen-capture",
Expand Down
Loading