Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .agents/skills/running-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ tests, and architecture lint.
CI test jobs use `--skip-architecture` because the dedicated Bumper job owns
that sequence. Do not use this flag for normal local validation.

`./test --porthole-host` runs the native runtime/client package and the separate
certificate package. A failure in either package fails the tier.

Affected and unit-capable scopes run the backup-upgrader regression. A scope
that contains only image bundles skips that host-side unit regression.

Expand Down Expand Up @@ -77,6 +80,10 @@ server. That makes captures slower and flaky. See

## Iterate faster

Use `--build-jobs 2` to reduce concurrent Xcode build tasks when memory is limited.
Keep the default when the machine has enough memory. This option does not change test parallelism.
Set `TEST_WORKDIR` to an ignored repository directory when logs must survive a reboot.

After a green build:

```bash
Expand Down
3 changes: 2 additions & 1 deletion .bumper/RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ tests and generated files are outside the architecture graph.
| --- | --- | --- |
| `RegionKit` | none | Foundation |
| `WhereCore` | `RegionKit` | Foundation, persistence |
| `WhereUI` | `RegionKit`, `WhereCore` | Foundation, SwiftUI, UIKit |
| `WhereAssets` | none | Foundation |
| `WhereUI` | `RegionKit`, `WhereCore`, `WhereAssets` | Foundation, SwiftUI, UIKit |
| `WhereIntents` | `RegionKit`, `WhereCore`, `WhereUI` | Foundation, SwiftUI, UIKit |
| `Where` app | `RegionKit`, `WhereCore`, `WhereUI`, `WhereIntents` | Foundation, SwiftUI, UIKit |
| `WhereWidgets` | `RegionKit`, `WhereCore`, `WhereUI` | Foundation, SwiftUI, UIKit |
Expand Down
19 changes: 18 additions & 1 deletion .bumper/Tests/WhereArchitectureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func `Where architecture accepts downward dependencies`() throws {
SourceInput(
path: "Where/WhereUI/Sources/Screen.swift",
component: ComponentID(WhereComponent.whereUI.rawValue),
source: "import WhereCore\nimport SwiftUI\nstruct Screen {}",
source: "import WhereCore\nimport WhereAssets\nimport SwiftUI\nstruct Screen {}",
),
],
),
Expand All @@ -24,6 +24,23 @@ func `Where architecture accepts downward dependencies`() throws {
#expect(report.violations.isEmpty)
}

@Test
func `WhereAssets cannot depend on the UI layer`() throws {
let report = try bumper.evaluate(
RepositoryInput(
architecture: bumper.architecture,
files: [SourceInput(
path: "Where/WhereAssets/Sources/WhereAssets.swift",
component: ComponentID(WhereComponent.whereAssets.rawValue),
source: "import WhereUI\nstruct Assets {}",
)],
),
)
let violation = try #require(report.violations.first)
#expect(report.violations.count == 1)
#expect(violation.rule.id == .componentBoundary)
}

@Test
func `RegionKit cannot depend upward on WhereCore`() throws {
let report = try bumper.evaluate(
Expand Down
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ commands:
name: Generate project and resolve Swift packages
command: |
# Populate the immutable download caches before either test job saves them.
mise exec -- python3 Tools/porthole_export.py
started="$(python3 -c 'import time; print(time.time_ns())')"
if mise exec -- tuist generate --no-open >"$TEST_WORKDIR/generate.log" 2>&1; then
python3 -c 'import json,sys,time; print("CI_TIMING " + json.dumps({"phase":"generate","seconds":round((time.time_ns()-int(sys.argv[1]))/1e9,3),"status":0}, separators=(",",":")))' "$started"
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,72 @@ jobs:
path: .build
key: ${{ steps.bumper-cache.outputs.cache-primary-key }}

porthole-compiler:
if: ${{ github.event_name != 'pull_request' || !contains(github.event.pull_request.title, 'NO-CI') }}
name: Porthole compiler contracts
runs-on: xcode-27
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v3
- name: Check pinned Xcode
run: test "$(xcodebuild -version | awk '/Build version/{print $3}')" = "$(cat .xcode-build-version)"
- name: Qualify private bindings and actor isolation
run: ./test --porthole-generator --skip-architecture
- name: Qualify native Porthole runtime and clients
run: ./test --porthole-host --skip-architecture
- name: Compile the Porthole Mac Catalyst client
run: |
mkdir -p "$RUNNER_TEMP/porthole-catalyst"
./ide --no-open > "$RUNNER_TEMP/porthole-catalyst/generate.log" 2>&1
mise exec -- xcodebuild build \
-workspace Stuff.xcworkspace -scheme Porthole -configuration Release \
-destination 'generic/platform=macOS,variant=Mac Catalyst' \
-derivedDataPath "$RUNNER_TEMP/porthole-catalyst/products" \
-jobs 2 ARCHS=arm64 ONLY_ACTIVE_ARCH=YES CODE_SIGNING_ALLOWED=NO \
> "$RUNNER_TEMP/porthole-catalyst/build.log" 2>&1
- name: Upload Mac Catalyst build logs
if: always()
uses: actions/upload-artifact@v4
with:
name: porthole-catalyst-build
path: ${{ runner.temp }}/porthole-catalyst/*.log
if-no-files-found: ignore
retention-days: 7

porthole-app-compiler:
if: ${{ github.event_name != 'pull_request' || !contains(github.event.pull_request.title, 'NO-CI') }}
name: Porthole compiler pair (${{ matrix.configuration }}, ${{ matrix.sdk }})
runs-on: xcode-27
timeout-minutes: 180
strategy:
fail-fast: false
max-parallel: 1
matrix:
configuration: [Debug, Beta, Release]
sdk: [iphonesimulator, iphoneos]
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v3
- name: Build original and instrumented source with matching settings
run: >-
python3 Tools/porthole_compiler_contract.py
--configuration '${{ matrix.configuration }}'
--sdk '${{ matrix.sdk }}'
--jobs 2
--output "$RUNNER_TEMP/porthole-compiler-pair"
- name: Upload compiler evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: porthole-compiler-${{ matrix.configuration }}-${{ matrix.sdk }}
path: |
${{ runner.temp }}/porthole-compiler-pair/result.json
${{ runner.temp }}/porthole-compiler-pair/*-packaging.json
${{ runner.temp }}/porthole-compiler-pair/*.log
if-no-files-found: warn
retention-days: 7

test-macos:
if: ${{ github.event_name != 'pull_request' || !contains(github.event.pull_request.title, 'NO-CI') }}
name: Build & Test (macOS)
Expand All @@ -96,6 +162,8 @@ jobs:
# no single xcodebuild destination can build both — so the macOS scheme
# runs here rather than alongside the iOS bundles. The iOS `test-ios` and
# `snapshot` jobs live in .circleci/config.yml (PR #237).
- name: Generate Porthole application bindings
run: mise exec -- python3 Tools/porthole_export.py
- name: Build & Test (macOS)
run: mise exec -- tuist test Ledger-macOS-Tests --no-selective-testing -- -destination 'platform=macOS'
# Tests can crash the host process on CI without leaving any error in
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Tuist managed
Derived/
.generated/

## Local mise overrides (e.g. TUIST_DEVELOPMENT_TEAM for on-device signing)
mise.local.toml
Expand Down
58 changes: 57 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,61 @@ package). Apps, app extensions, and test bundles are Tuist targets in
references the package via `Package.local(path: .relativeToRoot("."))`. The
two manifests are the authoritative target catalog. This file does not duplicate that catalog.

Porthole has two local package boundaries under [`Shared/Porthole`](Shared/Porthole/README.md).
Its runtime package reuses source and tests for native qualification and the CLI.
Its dynamic certificate package owns X509 linkage in the application graph.
Keep its pins aligned with the root resolution. Do not flatten this boundary;
see [`PortholeCertificates`](Shared/Porthole/PortholeCertificates/README.md).

### Shared Where linkage

Link the Where app, widgets, and share extension through the explicit dynamic `WhereApplicationSupport` product.
Keep their shared package dependencies behind that product. Do not add separate products from its dependency closure to these hosts.
Embed the product only from the Where app with `.runtimeEmbedded`. Keep both extensions on the default `.runtime` linkage.
Set `LM_SKIP_METADATA_EXTRACTION=YES` only on the widget and share targets. Keep App Intents metadata extraction enabled for the app and package targets.
Verify app-only intent routes and absent extension metadata after changing the pinned Xcode toolchain.
Link the app-hosted `WhereTests` bundle to that same product, without separate application-module products.
Keep source imports and architecture rules on the existing Swift modules. The aggregate product adds no module.
Keep debugger, store, and reporting activation in the existing composition roots. Linking the shared image must not activate them.
After a linkage change, verify one metadata definition per process, extension resource loading, and App Intents metadata.
Run the full iOS unit scheme and the matched optimized compiler pair before accepting the new linkage.

### Porthole compilation

Treat `PortholeRuntime` as a generated-adapter dependency for adopting modules.
Module import restrictions continue to govern handwritten source.
Permit generated adapters to import `PortholeRuntime` and use its scope-checked execution API.
Keep this exception separate from dependencies on Porthole UI, credentials, remote transport, and agents.
For modules without a handwritten Porthole integration, use this dependency only in generated adapters.
Keep debugger activation and resource installation in the application composition root.

After each original-source/instrumented compiler pair, run the retained packaging checker on both completed apps.
Keep its expected app routes, resource rules, and synthetic regression fixtures with intentional module or packaging changes.
Treat static packaging checks as separate from runtime extension and physical-device acceptance.

Where's first-party module export is selected from the root package dependency
graph. Generated adapters require `-disable-access-control` and
`-enable-private-imports` for cross-file private symbol linkage.
Keep these flags on adopting targets, outside reusable runtime targets. Run
`./test --porthole-generator --skip-architecture` after exporter changes and
`./test --porthole-host --skip-architecture` for native runtime/client checks.
The compiler-contract CI job also builds original source without either flag
or private binding bodies. Keep that guard and the instrumented build on the
same SDK, optimization, compilation mode, and compiler conditions.
Use `Tools/porthole_compiler_contract.py` for paired app builds in Debug, Beta,
and Release, with both simulator and iPhoneOS SDKs. Preserve its per-module
compiler evidence and bounded build concurrency.

`WhereAssets` owns icon-preview compilation separately from WhereUI's string
catalogs. Keep both Xcode-generated resource helpers in separate modules while
private access is enabled. Preserve the catalog path owned by `./icons`;
see [`WhereAssets`](Where/WhereAssets/README.md).

`Tools/porthole_export.py` generates app bindings before project generation and
again before app compilation. Keep its output in `.generated/Porthole`, outside
Tuist's `Derived` directory. Use `./ide --no-open` to regenerate. The build plugin
owns package-target adapters. Never edit either generated output by hand.

`./ide` regenerates the Xcode project and does the surrounding setup. That setup includes external agent skills and `core.hooksPath`. Use `./ide` to regenerate. Do not use `tuist generate` alone. Agents must always pass `--no-open` (see [Generating the
Xcode project](#generating-the-xcode-project)). On a fresh machine, run `./ide
--bootstrap` first. That command installs `mise` and the pinned tools before
Expand Down Expand Up @@ -93,7 +148,8 @@ How the app was built is stamped by a post-build script
([`Where/Where/Scripts/stamp-build-info.sh`](Where/Where/Scripts/stamp-build-info.sh)).
The script writes the commit into `WhereGitSHA` / `WhereGitStatus`. It writes how the Swift compiler
was invoked into `WhereConfiguration` / `WhereSwiftOptimizationLevel` /
`WhereSwiftCompilationMode`. All of it is read back by `WhereCore.BuildInfo`.
`WhereSwiftCompilationMode`. These are read back by `WhereCore.BuildInfo`.
`WhereSwiftCompilerVersion` records the compiler identity for Porthole's build evidence.
Settings > About uses it. Every Periscope logging session uses it for attributes.
The optimization level tells you if a recorded span duration means
anything. Only the app is stamped. Tripwires: it must stay a **post** script
Expand Down
10 changes: 9 additions & 1 deletion BumperBowling.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import BumperBowlingCore
enum WhereComponent: String, ComponentKey {
case regionKit
case whereCore
case whereAssets
case whereUI
case whereIntents
case app
Expand All @@ -15,6 +16,7 @@ let bumper = BumperProject {
Included {
"Where/RegionKit/Sources"
"Where/WhereCore/Sources"
"Where/WhereAssets/Sources"
"Where/WhereUI/Sources"
"Where/WhereIntents/Sources"
"Where/Where/Sources"
Expand Down Expand Up @@ -47,10 +49,16 @@ let bumper = BumperProject {
Component(.whereUI) {
Owns("Where/WhereUI/Sources")
Modules("WhereUI")
MayDependOn(.regionKit, .whereCore)
MayDependOn(.regionKit, .whereCore, .whereAssets)
Applies(.wherePresentationLayer)
}

Component(.whereAssets) {
Owns("Where/WhereAssets/Sources")
Modules("WhereAssets")
Applies(.whereFoundationLayer)
}

Component(.whereIntents) {
Owns("Where/WhereIntents/Sources")
Modules("WhereIntents")
Expand Down
2 changes: 2 additions & 0 deletions Ledger/install
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ installed_pids() {
installer validate-destination --destination "$DEST"

if [ "$DRY_RUN" = true ]; then
echo "==> Would generate Porthole application bindings"
echo "==> Would generate the Xcode project"
echo "==> Would build $APP_NAME (Release)"
pids="$(installed_pids)"
Expand All @@ -71,6 +72,7 @@ DERIVED="$(mktemp -d)"
trap 'rm -rf "$DERIVED"' EXIT

echo "==> Generating the Xcode project"
mise exec -- python3 Tools/porthole_export.py
mise exec -- tuist generate --no-open >/dev/null

echo "==> Building ${APP_NAME} (Release)"
Expand Down
38 changes: 37 additions & 1 deletion Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading