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
21 changes: 15 additions & 6 deletions crates/buildbridge-machines/src/android_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1423,13 +1423,16 @@ fi
let scope = OperationScope::new();
let cancellation = Arc::clone(&scope);
let _entered = enter_operation(scope);
let calls = self.root.join("adb.args");
// The Stop stands in for one from the desktop, and lands once the run has read
// both lines the fake logcat prints. Stopping as soon as logcat was merely
// called let a slow runner end the run before the lines had arrived.
let streamed = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let seen = Arc::clone(&streamed);
let cancel = thread::spawn(move || {
let deadline = std::time::Instant::now() + Duration::from_secs(30);
while std::time::Instant::now() < deadline {
if fs::read_to_string(&calls).is_ok_and(|logged| logged.contains("logcat")) {
break;
}
while std::time::Instant::now() < deadline
&& seen.load(std::sync::atomic::Ordering::SeqCst) < 2
{
thread::sleep(Duration::from_millis(10));
}
cancellation.cancel();
Expand All @@ -1442,7 +1445,13 @@ fi
&self.apk,
SHA256,
live_reload_url,
|progress| events.push(progress),
|progress| {
streamed.fetch_add(
progress.log_lines.len(),
std::sync::atomic::Ordering::SeqCst,
);
events.push(progress);
},
);
cancel.join().unwrap();
(result, events)
Expand Down
13 changes: 10 additions & 3 deletions crates/buildbridge-machines/src/recipes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ fi
/// The framework's own step between the dependencies and Xcode: `cap sync`, `cordova
/// prepare`, `expo prebuild` when the native project is not committed, Flutter's project
/// configuration. Nothing for React Native, which bundles inside Xcode, and for a native app.
///
/// `cap sync ios` rewrites the Podfile's plugin block and then runs `pod install` itself, with
/// no switch to leave that out; only the `pod` it runs can be chosen. It is pointed at
/// `/usr/bin/true` so CocoaPods runs once, in the next phase, where a committed lock a plugin
/// upgrade outgrew is updated in the guest rather than failing the build.
fn ios_framework_script(
layout: &ProjectLayout,
workspace: &str,
Expand All @@ -297,7 +302,7 @@ if /bin/test ! -x {root}/node_modules/.bin/cap; then
/usr/bin/printf '%s\n' 'The project has no Capacitor command line: add @capacitor/cli to its devDependencies.' >&2
exit 1
fi
{root}/node_modules/.bin/cap sync ios
CAPACITOR_COCOAPODS_PATH=/usr/bin/true {root}/node_modules/.bin/cap sync ios
"#
),
ProjectKind::Cordova => format!(
Expand Down Expand Up @@ -902,8 +907,10 @@ mod tests {
assert!(script.contains("js_run build"), "{script}");
assert!(script.contains("phase syncing_ios"), "{script}");
assert!(
script.contains("node_modules/.bin/cap sync ios"),
"{script}"
script.contains(
"CAPACITOR_COCOAPODS_PATH=/usr/bin/true '/Users/b/BuildBridge/workspaces/active'/node_modules/.bin/cap sync ios"
),
"Capacitor must not run CocoaPods itself: {script}"
);
assert!(script.contains("phase resolving_pods"), "{script}");
assert!(
Expand Down
4 changes: 3 additions & 1 deletion docs/PRODUCT_AND_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ The first real project fixture is the Ionic/Capacitor application at the Ionic/C
The desktop presents the project workflow as the first two steps of a machine's build phase (see [Desktop application](#desktop-application)):

1. **Set up the project** — paste or drop the local project folder. Approval is read-only and does not copy source. The step also holds the snapshot: a bounded, checksummed archive streamed through the authenticated, host-key-pinned SSH bridge, made by every build of the latest source and, on demand, by the step's own synchronize. The progress display reports source inspection, bytes transferred, and guest extraction.
2. **Run the unsigned test build** — prepare pinned guest tools, install the project's dependencies with the package manager it locks with, build the web assets and run the framework's own preparation for the kinds that have them (`cap sync`, `cordova prepare`, `expo prebuild`, Flutter's project configuration), resolve pods where the Podfile is against a committed lock, and compile the approved scheme with signing disabled. The target is a choice: the **device SDK** by default, which ships inside Xcode and is what the signed archive and the phone build compile against, so nothing is downloaded; or the **iOS Simulator**, the only target that can run on screen inside the guest, for which Apple's iOS Simulator platform is installed first when the selected Xcode has no compatible runtime. The guest diagnostics report which runtime is installed, so the choice is labelled with its cost before it is made. Live phase and bounded log output remain visible in the desktop UI. The multi-gigabyte Apple platform download is a one-time persistent guest operation with measured byte/percentage progress and a distinct install/register state. Remote builds queued from the control plane always use the device SDK. The guest runs one durable smoke-build job at a time, so repeating the build action after a desktop restart or development hot reload reattaches to its log instead of starting a duplicate build. If resolving the native dependencies requires CocoaPods to refresh a committed `Podfile.lock`, that change remains guest-only and is reported clearly rather than silently modifying the host project. `pod install` never moves a version the lock pins, so when a plugin upgrade on the host asks for a pod the committed lock holds at an older version, it stops and names that pod rather than refreshing anything; the guest then does what CocoaPods advises, `pod update` for exactly the pods named, in the guest only, and the lock change is reported the same way. Because a host without a Mac cannot regenerate the lock itself, the step offers **Adopt the guest's Podfile.lock**: it copies the lock CocoaPods wrote in the guest into the approved project (the previous copy is kept beside the machine's records), lists each repinned pod, and lifts the archive's drift block, since the test build that just passed compiled with exactly that lock. Committing it in the project stays the user's.
2. **Run the unsigned test build** — prepare pinned guest tools, install the project's dependencies with the package manager it locks with, build the web assets and run the framework's own preparation for the kinds that have them (`cap sync`, `cordova prepare`, `expo prebuild`, Flutter's project configuration), resolve pods where the Podfile is against a committed lock, and compile the approved scheme with signing disabled. The target is a choice: the **device SDK** by default, which ships inside Xcode and is what the signed archive and the phone build compile against, so nothing is downloaded; or the **iOS Simulator**, the only target that can run on screen inside the guest, for which Apple's iOS Simulator platform is installed first when the selected Xcode has no compatible runtime. The guest diagnostics report which runtime is installed, so the choice is labelled with its cost before it is made. Live phase and bounded log output remain visible in the desktop UI. The multi-gigabyte Apple platform download is a one-time persistent guest operation with measured byte/percentage progress and a distinct install/register state. Remote builds queued from the control plane always use the device SDK. The guest runs one durable smoke-build job at a time, so repeating the build action after a desktop restart or development hot reload reattaches to its log instead of starting a duplicate build. If resolving the native dependencies requires CocoaPods to refresh a committed `Podfile.lock`, that change remains guest-only and is reported clearly rather than silently modifying the host project. `pod install` never moves a version the lock pins, so when a plugin upgrade on the host asks for a pod the committed lock holds at an older version, it stops and names that pod rather than refreshing anything; the guest then does what CocoaPods advises, `pod update` for exactly the pods named, in the guest only, and the lock change is reported the same way. Capacitor's `cap sync ios` runs `pod install` itself after rewriting the Podfile's plugin block, with no switch to leave it out, so it is pointed at `/usr/bin/true` and CocoaPods runs once, in buildbridge's own phase. Because a host without a Mac cannot regenerate the lock itself, the step offers **Adopt the guest's Podfile.lock**: it copies the lock CocoaPods wrote in the guest into the approved project (the previous copy is kept beside the machine's records), lists each repinned pod, and lifts the archive's drift block, since the test build that just passed compiled with exactly that lock. Committing it in the project stays the user's.

The first implementation caps an approved snapshot at 50,000 files and 2 GiB before compression. It excludes Git metadata, `node_modules`, generated native/web output, dependency stores, common IDE/cache directories, every `.env` variant, package-manager authentication files, SSH material, private keys, certificates, provisioning profiles, and App Store Connect keys. It rejects included symlinks and non-regular files. The archive checksum and source counts are retained in the local workspace record; removing approval does not modify the host project.

Expand Down Expand Up @@ -797,6 +797,8 @@ Accepted live through `crates/buildbridge-engine/tests/android_release.rs` again
| On a host that had run an earlier release, the iPhone step warned that the udev rule "differs from the expected content", all six checks showed as failed, and the **Reinstall the host USB rule** button was disabled | Two faults. The rule check compared the whole file byte for byte, and the lowercase-brand commit had changed the comment header from "Written by BuildBridge" to "Written by buildbridge"; the rule line itself was identical, so passthrough worked while the step said it did not. And the step disabled its primary action whenever the step was pending, whichever rung it stood on, so a host waiting on its test build could not reinstall a udev rule that no build depends on | The check compares the lines udev acts on and ignores comments and blank lines, so a header change never asks a working host to reinstall, while a changed or added rule line still does. The pending gate now withholds only the signing and run rungs, which the engine refuses without the test build anyway, and the withheld button names that reason; every host and phone rung acts at any time | Fixed 2026-09-17 |
| The unsigned test build failed in the guest at Resolving CocoaPods: "could not find compatible versions for pod IONCameraLib", with the lock pinning 1.0.4 and the project's `@capacitor/camera` 8.2.4 wanting 2.x | The host's `@capacitor/camera` had been upgraded, and its pod now depends on a newer IONCameraLib than the committed `ios/App/Podfile.lock` pins. `pod install` keeps every pinned version, so it cannot satisfy both and stops. The lock-drift flow the design describes, a refreshed guest lock that the step offers for adoption, was never reached, because it only fires when `pod install` itself rewrites the lock; a conflicting lock makes it fail instead. A Linux host cannot run CocoaPods, so `cap sync ios` there copies assets and updates the plugin list and can do nothing about the lock | The guest recipe runs `pod install`, and on failure reads the pods CocoaPods names in its own "could not find compatible versions" lines, limited to the characters a pod name can hold, and retries with `pod update` for exactly those, as CocoaPods advises. The lock then differs from the committed one, the existing marker reports it, and the test build step offers the refreshed lock for adoption. Any other failure still fails the build as before, and a project with no committed lock is left to plain `pod install`. Verified with a fixture `pod` on Linux and under bash 3.2. The native Mac path keeps its `--deployment` install: that host has CocoaPods, and the path refuses by design to alter the host project | Fixed 2026-09-17 |
| The macOS half of CI failed one `buildbridge-machines` test with `File exists` from the Android HTTP fixture's `create_dir`, on a change that had not touched it | Every test fixture named its temp directory by the process id and a nanosecond timestamp. A macOS clock ticks in microseconds, and a test binary creates fixtures on many threads at once, so two fixtures born in the same instant took the same name and the second died. Linux clocks are fine-grained enough that it had never shown there | `test_scripts::fixture_dir` names a directory by the process id and a process-wide counter and creates it; every test fixture in the crate uses it, and none names a directory by a clock any more. Production temp names in `archive`, `android_signing` and `android_device` still use a timestamp, but each is created once per operation rather than in parallel by a test runner | Fixed 2026-09-17 |
| With the CocoaPods retry shipped and installed, the same IONCameraLib failure came back unchanged | The retry never ran: the failure was earlier. Capacitor's `cap sync ios` rewrites the Podfile's plugin block and then runs `pod install` itself, in the syncing phase before buildbridge's Resolving CocoaPods phase, and it has no switch to leave that out, only `CAPACITOR_COCOAPODS_PATH` to choose which `pod` it runs. Found by reproducing in the guest over buildbridge's own SSH identity: `pod update IONCameraLib` there resolved 2.0.0 at once, so the retry was sound and simply unreached | The sync runs with `CAPACITOR_COCOAPODS_PATH=/usr/bin/true`, so Capacitor rewrites the Podfile and cleans the project but leaves CocoaPods to the next phase, where the retry lives. Verified on the guest against the project with its stale lock restored: sync passes, install refuses the pod, the update resolves it, and the lock-changed marker fires | Fixed 2026-09-17 |
| The macOS half of CI failed `android_device`'s install-and-launch test with no console lines streamed, on a change to the iOS recipe | The test's stand-in for a Stop from the desktop fired as soon as the fake `adb` had been called with `logcat`, not once the run had read the two lines the fake prints, so on a slow runner the run ended before they arrived. The 2026-09-08 fix to this test had moved the Stop from a fixed delay to that call, which was later but still not late enough | The Stop waits until the run's progress has carried both lines, with the same 30-second ceiling, so it lands where a person's would: after the console is visibly streaming | Fixed 2026-09-17 |

### Automation and UI boundary

Expand Down
Loading