Skip to content

Xcode Build/Test/Run Configs — Design Spec (#TBD)

Milestone: xcode-build-configs Manual: docs/contributor_manual/guide/10-setup-and-day-to-day-development.md

Design-led (Testing Constitution). Status: APPROVED — 2026-09-09 (invariants verified from the project this session). A guardrail pins them so config drift is a red test, not a debugging session. Tags: [OK] built · [MISSING] not built.

Why this spec exists

The UI-test harness bug (ui-test-harness.md) was, at root, a config that had drifted from its own comments: a source comment asserted “Dev Local is sandboxed” while the actual build setting says the opposite — and that stale belief sent the test socket into the real app container. Config truth currently lives only in prose comments that nothing checks. This spec turns the build/test/run matrix into executable invariants.

Intent (the design)

There is ONE authoritative statement of what each build config is for and what security/arch/ deployment shape it carries, and a guardrail (scripts/check_xcode_config_invariants.py) that asserts it against fichero.xcodeproj/project.pbxproj, the *.entitlements files, the shared .xcschemes, and the Tests/plans/*.xctestplans. A drifted comment or a flipped setting fails the gate.

The matrix (verified 2026-09-09)

Scheme Build config Sandbox Entitlements Purpose
Dev Local Debug OFF (ENABLE_APP_SANDBOX = NO) Fichero.entitlements day-to-day dev + the UI-test host; unsandboxed so tests reach temp-dir fixtures
Dev Embedded Dev Embedded ON Fichero.entitlements embedded-engine dev parity
Release Embedded / App Store Release ON FicheroRelease / FicheroAppStore DMG + MAS

Cross-cutting invariants (all configs): - arm64-onlyEXCLUDED_ARCHS = x86_64 on the project configs (Golden Gate, Apple silicon). - Deployment floor macOS 26 — support 26 + 27, require 26. - FicheroTests carries SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor (else ~575 SIGTRAPs — see [[test-target-needs-mainactor-default-isolation]]). - The embedded test plan is attached to the Release Embedded scheme; dead plans stay deleted.

Behaviors

  • config.dev-local-unsandboxed [OK] — Fichero (Dev Local)DebugENABLE_APP_SANDBOX=NO. Pinned: test_check_xcode_config_invariants.py::test_real_project_is_clean, ::test_debug_sandbox_flip_is_caught.
  • config.release-sandboxed [OK] — Release + App Store + Dev Embedded → sandbox ON. Pinned: test_check_xcode_config_invariants.py::test_real_project_is_clean, ::test_debug_sandbox_flip_is_caught.
  • config.arm64-only [OK] — every project config excludes x86_64. Pinned: test_check_xcode_config_invariants.py::test_missing_excluded_archs_is_caught.
  • config.deployment-floor-26 [OK] — MACOSX_DEPLOYMENT_TARGET floor is 26. Pinned: test_check_xcode_config_invariants.py::test_wrong_deployment_floor_is_caught.
  • config.tests-mainactor-isolation [OK] — FicheroTests configs set the MainActor default. Pinned: test_check_xcode_config_invariants.py::test_missing_mainactor_isolation_is_caught, ::test_mainactor_dropped_from_ficherotests_only_is_caught.
  • config.embedded-testplan-attached[PARTIAL] (#4807) the embedded plan is referenced by its scheme, but re-checked 2026-09-18: check_xcode_config_invariants.py never actually asserts this — neither the guardrail’s source nor its test file mentions “testplan” or “embedded” plan attachment at all. The other five invariants in this spec really are checked and tested; this one just isn’t yet.
  • config.no-stale-sandbox-comments [MISSING] (#4778) — a source comment asserting a config’s sandbox state must match the actual setting (the drift that caused the harness bug).

Test matrix

Leg This surface? Pins File
Config guardrail (py) y every invariant above, parsed from project files scripts/check_xcode_config_invariants.py
Gate wiring y the guardrail runs in verify_all.sh scripts/verify_all.sh

Hard-gate: all of config.*. These are cheap (file parse, no build) and catch the exact drift class that stranded the harness.

Open questions

  • config.no-stale-sandbox-comments: worth the parsing complexity, or is asserting the settings enough and we just delete the stale comments? (Lean: assert settings; delete stale comments as part of the harness fix; add comment-checking only if drift recurs.)