Skip to content

Add a hermetic Bazel build alongside SwiftPM - #246

Merged
obj-p merged 26 commits into
mainfrom
bazel-migration
Jun 24, 2026
Merged

Add a hermetic Bazel build alongside SwiftPM#246
obj-p merged 26 commits into
mainfrom
bazel-migration

Conversation

@obj-p

@obj-p obj-p commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Builds previewsmcp, its full module graph, and all test suites with Bazel, so the product builds, runs, and renders without the SwiftPM glue scripts. SwiftPM is not retired: Package.swift stays as the published manifest for consumers and feeds the Bazel dependency sync, so the two build systems coexist.

Build system

  • Hermetic LLVM from the Swift fork via rules_foreign_cc cmake() (libLLVM.dylib in ~3.4 min), pinned + sparse clone in bazel/llvm.bzl. orc runtime, host llvm-tblgen, and the iossim LLVM TargetProcess + orc cross-builds go through an apple_support platform transition (bazel/transitions.bzl).
  • Swift graph via rules_swift + rules_swift_package_manager (SPM deps read from Package.resolved). The 4 SPM build-tool plugins become Bazel rules (version stamping, host-app source embedding incl. the #241 Shell app, resource bundling).
  • PreviewAgent (macOS JIT executor) as a cc_binary on libLLVM. server.o (iOS in-sim executor object) via a custom cc_object rule (bazel/object.bzl) that compiles for iossim while reusing the cached macOS libLLVM headers. LLVM cmake modules referenced by label (//:llvm_cmake_modules), no bzlmod-canonical path hardcoded.

Runtime + tests

  • JIT runtime resources resolve from the binary's runfiles (env override for tests, runfiles search for bazel run), so the SwiftPM resource bundle is not needed. macOS and iOS previews both render end-to-end, in tests and via bazel run.
  • All test suites run as swift_test (tags local/exclusive). A tools/bazel wrapper exports the workspace root so integration tests find the checked-in examples/. The iOS end-to-end suite is renamed IOSPreviewE2ETests (per Rename IOSSimSpikeTests + fix test isolation (no longer a spike) #244) and @Suite(.serialized).
  • bazel test //Tests/...all 9 targets green (macOS + iOS render, daemon integration, e2e).

Docs

AGENTS.md, the bootstrap skill, and Brewfile lead with Bazel and keep SwiftPM for consumers.

Follow-ups (optional)

🤖 Generated with Claude Code

Comment thread patches/BUILD.bazel
Comment thread tools/bazel

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep tools in bazel/tools? What is the idiomatic choice. My thinking is keeping Bazel related things in bazel.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is a hard constraint, not a preference: tools/bazel is the bazelisk wrapper, and bazelisk specifically looks for tools/bazel at the workspace root and execs it (ours also sets PREVIEWSMCP_REPO_ROOT). Moving it under bazel/ would make bazelisk no longer find it, silently disabling the wrapper. So it stays at the root tools/ path.

obj-p and others added 7 commits June 22, 2026 13:09
Build previewsmcp, its full module graph, and all test suites with Bazel, so the
product builds, runs, and renders without the SwiftPM glue scripts. SwiftPM is
not yet retired; Package.swift still works.

Build system:
- Hermetic LLVM from the Swift fork via rules_foreign_cc cmake() (libLLVM.dylib
  in ~3.4min), pinned + sparse clone in bazel/llvm.bzl. orc runtime, host
  llvm-tblgen, and the iossim LLVM TargetProcess + orc cross-builds via an
  apple_support platform transition (bazel/transitions.bzl).
- Swift graph via rules_swift + rules_swift_package_manager (SPM deps from
  Package.resolved). The 4 SPM build-tool plugins become Bazel rules
  (version stamping, host-app source embedding, resource bundling).
- PreviewAgent (the macOS JIT executor) as a cc_binary on libLLVM. server.o (the
  iOS in-sim executor object) via a custom cc_object rule (bazel/object.bzl) that
  compiles for iossim while reusing the cached macOS libLLVM headers.

JIT runtime resources are resolved from the binary's runfiles (env override for
tests, runfiles search for bazel run), retiring the need for a SwiftPM resource
bundle. macOS and iOS previews both render end-to-end.

Tests: all 8 suites run as swift_test (tags local/exclusive). A tools/bazel
wrapper exports the workspace root so integration tests find the checked-in
examples; the Bazel-built previewsmcp binary is supplied via runfiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
examples/bazel is a nested Bazel workspace and the SwiftPM .build dirs carry
foreign BUILD files; both broke the //... wildcard CI and bootstrap rely on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
One-file SwiftUI library product (the preview setup protocol), no deps. Last
SPM target not yet in the Bazel graph.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
Bazel is now the build for development, tests, and CI. SwiftPM is kept as the
published manifest so other projects can depend on the library products
(notably PreviewsSetupKit) and to feed the Bazel external-dependency sync.

- Brewfile: add bazelisk.
- AGENTS.md: Setup and Build & Test lead with bazel build/test/run; add a
  "Consuming via SwiftPM" section; the PR-verification step runs bazel test
  (swift test kept as a fallback). Note that the iOS JIT resources and LLVM are
  built by Bazel automatically, so the build-jit-llvm scripts are not needed
  under Bazel.
- bootstrap skill: build with `bazel build //...`; --jit is now only for the
  SwiftPM JIT path; verify bazel availability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
…epo path

orc_rt and orc_rt_iossim hardcoded the bzlmod-canonical external path
($$EXT_BUILD_ROOT$$/external/+llvm_repository+llvm_src/llvm/cmake/modules) for
LLVM_CMAKE_DIR, which breaks if the module-extension or repo name changes.

Add a small stage_dir rule that filters the llvm/cmake/modules files out of
@llvm_src into a declared directory (//:llvm_cmake_modules), and pass it to both
cmake targets via build_data + $(execpath), the same pattern //:iossim_orc
already uses for llvm-tblgen. No canonical repo name is hardcoded.

Both orc archives rebuild; PreviewsJITLinkTests passes on the rebuilt runtime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
new ios-host/app/Shell app (ShellMain.m, Info.plist, entitlements, icon),
emitting an IOSShellAppSource enum that PreviewsIOS now references.

Extend the Bazel embed_host_app_source rule + header fragment to match: four
new shell inputs, four _shell*Base64 lets, and the IOSShellAppSource enum. Wire
the Shell files through ios-host/app exports. PreviewsIOS compiles and all 8
suites pass, iOS render included.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
…244)

Earlier I deleted IOSSimSpikeTests as obsolete scaffolding. Per #244 it is not a
spike: it is the real end-to-end iOS suite (host-app build, sim boot, flash-free
respawn, relaunch, JIT-over-EPC render, agent->shell redirect). Restore it from
main and address #244 part 1:

- Rename IOSSimSpikeTests -> IOSPreviewE2ETests (file, suite, SimSpikeSupport ->
  IOSPreviewE2ESupport); refresh the stale "Phase 0 spike" doc.
- Give it its own swift_test target (excluded from PreviewsJITLinkTests) wired
  with the iOS JIT resources; locate packageRoot + fixtures via
  PREVIEWSMCP_REPO_ROOT.
- Mark the suite @suite(.serialized): all 18 tests share one simulator, one
  host-app bundle ID, and one IOSHostBuilder workDir, so concurrent runs clobber
  the host-app source mid-build ("modified during the build"). Serial is correct
  and also fixes the same spurious failures under `swift test`.

18/18 pass (the standalone iossim-executor tests self-skip when that artifact is
absent). The worktree-level isolation #244 mentions is a separate concern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PVPJon8ZA1CLZcPhKTUng9
@obj-p
obj-p force-pushed the bazel-migration branch from b656a33 to d4f6165 Compare June 22, 2026 17:47
obj-p and others added 6 commits June 22, 2026 13:48
Adopt rules_xcodeproj 4.1.0 (dev_dependency) so Xcode builds-with-Bazel
across the LLVM/JIT/iOS graph. A swift_binary's data/runfiles are not staged
by BwB (rules_xcodeproj#828), so previewsmcp's iOS JIT resources are absent
when Run from Xcode. The xcschemes Run pre-action builds the runfiles tree in
the workspace output base and points RUNFILES_DIR at it, giving Xcode Run the
same resource fidelity as `bazel run //Sources/previewsmcp`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…reviewsSetupKit

Reshape the repo around the Bazel-primary model. All first-party Bazel code
moves from Sources/ into a previewsmcp/ umbrella (modules as previewsmcp/<X>,
the binary at previewsmcp/cli with target name previewsmcp). Tests/ and
ios-host/ (agent/shell/executor) move under previewsmcp/ too. bazel/ and the
root LLVM cmake targets stay at root. examples/ is untouched.

Sources/ now holds only PreviewsSetupKit, the one SwiftPM-exposed library.
Package.swift is slimmed to expose just PreviewsSetupKit, keeping the three
external .package() declarations so rules_swift_package_manager's from_package
still syncs them. The SwiftPM-only GenerateVersionTool / EmbedHostAppSourceTool
/ Plugins are deleted (Bazel's version.bzl / embed.bzl replace them).

.mcp.json launches via scripts/previewsmcp, which builds //previewsmcp/cli and
execs the Bazel binary (build output to stderr to keep MCP stdio clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…ecutor

The Bazel build no longer needs the SwiftPM LLVM build or the spike standalone
executor. Delete scripts/build-jit-llvm*.sh, ios-host/executor/{build.sh,
main.cpp,bundle.sh}, and the standalone-executor tests in IOSPreviewE2ETests
(the withRemoteSession/spawnExecutor path that linked objects via a
simctl-spawned iossim-executor against third_party/.build-iossim artifacts).
Production keeps server.cpp/server.h (compiled to server.o via cc_object and
embedded in the agent app) and the IOSAgentBuilder-based e2e tests.

Move the LLVM fork patches to a top-level patches/ (they patch a fetched
dependency, not build logic) and update MODULE.bazel's llvm_repository. Strip
the build-jit-llvm and third_party references from CI, AGENTS.md, and the
bootstrap skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…project

Providing custom xcschemes had disabled rules_xcodeproj scheme autogeneration,
leaving only the previewsmcp scheme. Set scheme_autogeneration_mode = "all",
add the swift_test targets to top_level_targets so they get test schemes (which
required making them publicly visible to the generator), and exclude the
external-dependency schemes (NIO, ArgumentParser, SwiftSyntax, etc.) via
scheme_autogeneration_config. The project now shows the previewsmcp run scheme,
the nine first-party libraries, and the nine test suites.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…esolution)

Two pre-existing issues surfaced only when the full-suite gate is run as the
`//...` wildcard (the handoff ran the 9 explicit test labels instead). Both
reproduce on the base bazel-migration, so neither is a restructure regression.

1. `//...` builds the iOS-only foreign_cc cmake targets `iossim_orc` and
   `orc_rt_iossim` in the host (macOS) config as top-level targets. They set
   `CMAKE_SYSTEM_NAME=iOS` but get the macOS toolchain/SDK, so CMake's
   CheckAtomic try-compile fails and aborts configure with a misleading
   "Host compiler appears to require libatomic". These targets are only meant
   to be consumed via the `ios_sim_build` transition. Mark them
   `target_compatible_with = ["@platforms//os:ios"]` so `//...` skips them as
   incompatible in the host config while the transition still builds them.

2. `IOSPreviewE2ESupport.compileForIOSSim` resolved the Fixtures dir via
   `#filePath` only, which `bazel test` file-prefix-maps into runfiles where
   `Fixtures/` is not staged, failing the two `answer.c` tests. Resolve it via
   `PREVIEWSMCP_REPO_ROOT` first (mirroring the canonical
   `FixtureSupport.fixturesDirectory`), keeping `#filePath` as the swift-test
   fallback.

After both fixes the 6 non-sim suites + CLI/MCP integration are green and
IOSPreviewE2ETests is back to the documented 11/12 (one rotating sim-health
flake on the JIT-render-over-sim tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…move

`IOSPreviewE2ETests.packageRoot` and `ExamplesSplitE2ETests.repoRoot` derive the
repo root from `#filePath` when `PREVIEWSMCP_REPO_ROOT` is unset. The restructure
moved these files one level deeper (under `previewsmcp/`), but the fallback still
stripped only three path components, landing at `<repo>/previewsmcp/` instead of
`<repo>/`. Add the fourth `deletingLastPathComponent()` so the examples/spm paths
resolve when the suite is run without the `tools/bazel` env passthrough.

Masked today because `.bazelrc` forwards `PREVIEWSMCP_REPO_ROOT` and `tools/bazel`
sets it, so the env branch normally wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
obj-p and others added 2 commits June 22, 2026 21:59
The Bazel build compiles LLVM + the orc runtime hermetically via foreign_cc, so
nothing on this branch produces third_party/ anymore (the SwiftPM-JIT teardown
removed the last reference). Drop the now-dead ignore line. The physical
third_party/ directory still belongs to the SwiftPM-JIT path on main and is
local-only; reclaim it with `rm -rf third_party` after this lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…site)

CI's stale `Sources/ Tests/` swiftlint paths were hiding these in the moved
code. Clearing them so the upcoming hermetic-lint Bazel gate can be a hard
`bazel test` failure rather than advisory (#245).

- PreviewSession.storedWindowFrame: return a named `WindowFrame` struct instead
  of a 4-member tuple (large_tuple). All four callers use member access, so the
  change is a drop-in.
- LogRotationTests: make `tempDir()` and the four @test funcs `throws`, replacing
  eight `try!` with `try` (force_try).
- BuildSystemTests: move the trailing XcodeBuildSystem/manifest test block into a
  same-file `extension` so the struct body drops under the type_body_length error
  threshold (no test moved out of the suite, no content change).

Verified: swiftlint error-severity count 0; swift-format --strict clean;
PreviewsCoreTests + PreviewsJITLinkTests pass; PreviewsMacOS builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
@obj-p
obj-p marked this pull request as draft June 23, 2026 02:31
obj-p and others added 6 commits June 23, 2026 08:10
…ls/lint

Pin the tools as Bazel downloads (not host PATH) and drive them with a single
run-based entry point, so CI / the local merge queue (#230) gates lint without
host installs. This is the run-based model that aspect_rules_lint itself uses
for format checks; an aspect was evaluated and rejected (no ruleset supports
Apple swift-format or swiftlint, caching is irrelevant at ~10 modules, and the
sandbox would break .swiftlint.yml's path-based excludes).

- MODULE.bazel: http_file buildifier v8.5.1 + clang-format 19 (muttleyxd static),
  http_archive swiftlint 0.63.3; add rules_shell as a direct dep.
- tools/lint/lint.sh + BUILD.bazel: `bazel run //tools/lint:format` (fix) and
  `:check` (verify). Each tool resolved via runfiles rlocation, run over
  $BUILD_WORKSPACE_DIRECTORY so existing .swift-format/.swiftlint.yml/.clang-format
  configs and excludes apply unchanged. swiftlint is check-only (non-strict, so
  warnings stay advisory like the prior CI). swift-format is wired but inert until
  its from-source binary lands (next commit).
- Apply the one-time buildifier (BUILD/.bzl/MODULE) and clang-format (C/C++)
  formatting. Verified: `bazel run //tools/lint:check` exits 0, //previewsmcp/...
  builds clean after the C/C++ reformat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
Apple swift-format ships no prebuilt binary and pulls a multi-package source
graph (swift-syntax et al), so the only hermetic options were a repository_rule
that recompiles it on every fresh/ephemeral runner (not remote-cacheable) or
hosting our own built binary. nicklockwood SwiftFormat ships a single official
universal macOS binary, so it drops in as an http_archive exactly like the other
three tools: one cached, sha-pinned download everywhere, remote-cache friendly,
zero consumer pollution. Decision made after an investigation that found the
caching profile is identical to prebuild-and-publish, so the tiebreaker was not
hosting an artifact.

- MODULE.bazel: pin SwiftFormat 0.61.1 via http_archive; drop the swift-format
  from-source repo rule (bazel/swift_format.bzl).
- .swiftformat: maxwidth 120, exclude the generated embed sources, and DISABLE
  hoistTry, hoistAwait, preferKeyPath. The first two are required: hoistAwait
  rewrote `let x = (try? await f())` into the invalid `let await x = (try? f())`.
  preferKeyPath rewrote `map { $0.schema }` into `map(\.schema)`, which crashed
  swiftc SILGen in MCPServer.mcpToolSchemas().
- Reformat the Swift tree (170/192 files). SwiftFormat is more vertical than
  swift-format, so bump the swiftlint error ceilings (type_body_length 1000->1100,
  file_length 1500->1600); warnings unchanged.
- Re-pin IOSAgentBuilderHashTests: reformatting AgentApp.swift changed the
  embedded agent-app source hash.
- lint.sh/BUILD: swap swift-format for swiftformat (lint via `<paths> --lint`).

Verified: `bazel run //tools/lint:check` exits 0 (all four tools), SwiftFormat
idempotent (0/192), zero swiftlint errors, all six non-sim test suites pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
Make the Bazel-pinned tools the single source of truth so a local commit and the
merge gate can never disagree on tool versions.

- lint.sh: unify file selection on git (git ls-files for the whole tree,
  git diff --cached for staged) instead of find, and add a `staged` mode that
  lints only the staged files. Swift tools skip bazel/ tooling fragments
  (header.swift is generated DO-NOT-EDIT); clang-format skips test Fixtures.
- tools/lint: add the //tools/lint:staged target.
- .githooks/pre-commit: replace host swift-format/swiftlint/clang-format with
  `bazel run //tools/lint:staged`.
- Brewfile: drop swift-format, swiftlint, clang-format (now hermetic); keep only
  bazelisk.
- AGENTS.md + bootstrap skill: document `bazel run //tools/lint:{format,check,staged}`
  as the lint entry points and that the merge gate runs `:check`.

Verified: `bazel run //tools/lint:check` exits 0; staged mode flags only staged
violations and exits non-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXqo6GCpm1xTPc2YMicCna
…LVM rebuild

`bazel clean` (or a fresh output base) otherwise rebuilds the foreign_cc LLVM
graph cold (~5.5 min). Point `--disk_cache` at ~/.cache/bazel-disk (Bazel
expands ~, so the checked-in path stays username-free) to back the normal
output base with a content-addressed store.

Verified: warm build 333s populates a 447MB cache; after `bazel clean` the
rebuild is 3.5s with 132 disk cache hits and 0 actions executed (LLVM served
from cache, never rebuilt). Part of #272; the shared remote cache is a
separate, runner-model-gated step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The integration suite spawns a `previewsmcp serve` daemon that persists past
the run by design (cleanup is the next suite's cleanSlate, not teardown). The
shared CLIRunner falls back to the real ~/.previewsmcp when PREVIEWSMCP_SOCKET_DIR
is unset, and this Bazel target never set it, so `bazel test` left a daemon in
the user's real daemon dir and its cleanSlate killed any live session daemon
there. CI already isolates this to /tmp (ci.yml PREVIEWSMCP_SOCKET_DIR); mirror
that for the Bazel target.

Verified: the suite's serve.log/sessions land in /tmp/previewsmcp-bazel-cli,
~/.previewsmcp is untouched, and a concurrently-running session daemon survives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `try-import %workspace%/.bazelrc.user` so each machine supplies its own
`--remote_cache` (and upload gating) out of a gitignored `.bazelrc.user`,
keeping the private endpoint out of the repo. A missing file is a silent no-op,
so an external clone builds locally with no warnings. The merge-queue VM drops
its own `.bazelrc.user` during provisioning.

Backed by a self-hosted bazel-remote on the Mac mini over Tailscale. Verified:
a fresh output base with no disk cache and the remote supplied only through the
try-import'd file is a full remote cache hit (132 hits, 0 actions executed, no
foreign_cc LLVM rebuild) -- a 6.5min cold build collapses to ~90s. Completes
the remote half of #272.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
obj-p and others added 2 commits June 23, 2026 17:36
The //:xcodeproj `previewsmcp` Run scheme pre-action built the binary with
`>/dev/null 2>&1 || bazel ...`, so a broken build produced no diagnostic and Run
launched against stale-or-absent runfiles. Pick bazelisk-or-bazel by presence and
run the build unsilenced, so a failure surfaces in the pre-action log and aborts
Run instead of yielding a confusing runtime error.

The two-output-base split (binary from the rxp base, RUNFILES_DIR from the normal
base) is left as-is: it is forced by rules_xcodeproj#828 (BwB does not stage a
swift_binary's runfiles), not cleanly removable until upstream lands. Part of #270.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion fallback

Code-review follow-ups on this branch's caching/cleanup work:

- MCPIntegrationTests now pins PREVIEWSMCP_SOCKET_DIR=/tmp/previewsmcp-bazel-mcp
  like CLIIntegrationTests, so neither daemon-touching suite touches the real
  ~/.previewsmcp (MCPTestServer spawned `serve` on the inherited env and only
  DaemonLifecycleTests overrode the dir per-spawn, leaving the suite half-isolated).
- Restore the `bazelisk || bazel` fallback in the //:xcodeproj runfiles pre-action
  (the `command -v` rewrite dropped it); still unsilenced, so it fails loudly when
  both fail.
- Register both bazel socket dirs in AGENTS.md so the per-suite convention has one
  source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
obj-p and others added 3 commits June 23, 2026 22:46
… the disk

`--disk_cache` has no GC by default, so ~/.cache/bazel-disk grows without bound
(the LLVM artifacts are large) until the disk fills. Set
`--experimental_disk_cache_gc_max_size=50G`; the Bazel server's idle GC then
evicts LRU entries to keep the cache under the cap. Surfaced by code review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	Package.swift
#	previewsmcp/PreviewsIOS/AVCCVideoStream.swift
#	previewsmcp/PreviewsIOS/FrameSource.swift
#	previewsmcp/PreviewsIOS/H264Encoder.swift
#	previewsmcp/PreviewsIOS/InputSink.swift
#	previewsmcp/PreviewsIOS/PreviewAppServer.swift
#	previewsmcp/PreviewsIOS/Resources/client.html
#	previewsmcp/PreviewsIOS/SimulatorManager.swift
#	previewsmcp/Tests/MCPIntegrationTests/IOSAppServerTests.swift
#	previewsmcp/Tests/MCPIntegrationTests/IOSHIDInputTests.swift
#	previewsmcp/Tests/PreviewsIOSTests/H264EncoderTests.swift
#	previewsmcp/Tests/PreviewsIOSTests/SimulatorManagerTests.swift
The JIT render fixtures call NSBitmapImageRep with only `import SwiftUI`. That
compiled while SwiftUI transitively re-exported AppKit, but Xcode 26.2's SwiftUI
no longer does, so the fixtures fail with "cannot find 'NSBitmapImageRep' in
scope". Add `import AppKit` to the affected fixture templates.

Pre-existing environment drift (reproduces before this branch's merge of main;
hits the SwiftPM path on main too), surfaced by the pre-merge full-suite gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@obj-p
obj-p marked this pull request as ready for review June 24, 2026 13:45
@obj-p
obj-p merged commit 9894d19 into main Jun 24, 2026
@obj-p
obj-p deleted the bazel-migration branch June 24, 2026 13:45
obj-p added a commit that referenced this pull request Jun 24, 2026
The hermetic Bazel migration (#246) accidentally tracked a worktree-local
`third_party -> ../../third_party` symlink (staged by a `git add -A` during the
merge resolution). Bazel builds LLVM from source via foreign_cc and the slim
Package.swift no longer references the prebuilt artifacts, so the symlink is
unused and dangles in any fresh clone. Remove it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
obj-p added a commit that referenced this pull request Jul 11, 2026
…e-gate trial (#361)

* CI on Bazel: rewrite workflows for the post-#246 tree, start the merge-gate trial

The old workflows ran swift build / swift test against the slim Package.swift
stub, so CI would green-light without testing anything real, and release.yml
would package the stub binary. Both now run Bazel: ci.yml is one macos-15 job
(Xcode 26.2 to match the .bazelrc pin; the image bundles the iOS 26.2 sim
runtime and iPhone 17 devicetype SimulatorTestDevices needs) doing
bazel test //... plus the hermetic lint, with the Bazel disk cache under
actions/cache. release.yml builds -c opt //previewsmcp/cli:previewsmcp.
cache-warm.yml existed to keep SPM caches alive and goes away with them.

CI runs as a non-required signal while we collect flake and wall-clock data;
the ruleset making it the merge gate comes after a green track record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Pin host_macos_minimum_os so exec tools run on runners older than the SDK

First trial run failed in 3 minutes: rules_swift's test_discoverer is built in
the exec configuration, which defaults its minimum OS to the SDK version
(26.2), and dyld on the macos-15 runner refuses to load it. Hosts running
macOS 26 never see this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Explicit timeouts for the sim-heavy test targets

Trial run 2: all four sim/integration targets died at Bazel's default 300s cap
on the GHA runner (slower cores + per-run first-boot device creation), while
passing locally in 20-180s. The suites bound their own tests with .timeLimit
traits, so eternal delegates the per-test caps to them; PreviewsJITLinkTests
ran 189s on the runner, close enough to 300 to bump to long.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* CI moves to the self-hosted Mac mini runner

Hosted macos-15 measured 50-70 min warm against ~5-7 min for the same suite
locally, so the trial pivots to a self-hosted Apple Silicon runner. The
persistent host replaces three steps: DEVELOPER_DIR pins Xcode per-job
instead of sudo xcode-select, ~/.cache/bazel-disk persists natively so
actions/cache goes away, and Spotlight is disabled once at host setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Lint before test: seconds of lint fail fast ahead of minutes of tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* iOS preview_start test calls get a 600s bound instead of the 60s default

First-ever run on a fresh machine (the mini's first CI job) pays device
first-boot plus a cold SwiftPM build of the example inside preview_start,
minutes by design; the generic 60s callTool bound failed exactly the two iOS
suites that used it while IOSAppServerTests survived only because
callToolResult carries no timeout at all. 600 seconds is a bound, not a wait:
warm runs stay ~17s, and the suite .timeLimit traits still cap the tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Event-pump probe gets a 10s dispatch budget

The mini's first CI run failed the 2s budget under full build+test load while
the agent kept answering runOnMain polls — the main thread was alive, the
posted event's run-loop dequeue was just late. The probe passes over SSH on
the same host in 1.3s, so dispatch itself works there; the budget was sized
for an idle laptop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* checkout@v7/cache@v6 (Node 20 deprecation) + weekly scheduled CI heartbeat

The schedule replaces what cache-warm.yml did for the hosted design: with the
disk cache living on the runner there is nothing to keep alive, but a weekly
run on a quiet repo still catches host/toolchain drift before a PR does, and
is a standing flake probe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Docs catch up with the CI rewrite

AGENTS.md still said CI was disabled and described the retired two-job hosted
topology (build-and-test/ios-tests socket-dir split, cross-job daemon
contention); setup-cache.md still documented the SPM actions/cache keys. Both
now describe the single-job self-hosted reality. The parity spec's historical
baseline reference stays as-is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove inert skip-on-CI guards; bound IOSAppServerTests preview_start at 600s

The three .disabled(if: CI != nil) traits stopped meaning anything the moment
tests moved under Bazel: test envs are hermetic, nothing forwards CI, so the
sim suites ran on the mini only by accident of env-scrubbing — and anyone
adding --test_env=CI later would silently skip all three and revert the
migration with no failing signal. The guards' original referent (GHA macos-15
under combined load wedging CoreSimulator, PR #141) is retired hardware for
this suite, and an unprovisioned host still skips via the nil-device guard.

callToolResult gains an optional bounded path reusing withTimeout + the
stderr dump, and appServerEndToEnd's preview_start uses it at 600s — the last
of the three iOS starts that could hang silently to its .timeLimit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Failure-diagnostics step: capture sim/daemon state while it is still true

Code review flagged that bazel test --test_output=errors dropped the old
workflow's on-failure daemon/sim dumps. On the persistent runner that state
survives the job but not the next run; printing it into the failing run's log
keeps the weekly flake probe actionable (#350 specimen collection included).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant