From ac76fcca86bb7e171044a7f40bc1b9d5e335cd22 Mon Sep 17 00:00:00 2001 From: PJ Date: Tue, 9 Jun 2026 14:46:54 +0530 Subject: [PATCH] fix(ioscompanion): key the device build cache on signing identity The cache marker hashed only sources, so switching signing team or key reused a runner signed with the stale identity, which the device rejects at install (0xe8008018). Fold team + key id into the cache key so a signing change forces a rebuild. --- internal/driver/ioscompanion/devicerunner.go | 20 +++++++++--- .../driver/ioscompanion/devicerunner_test.go | 31 +++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/driver/ioscompanion/devicerunner.go b/internal/driver/ioscompanion/devicerunner.go index b7beb56..cd92c2b 100644 --- a/internal/driver/ioscompanion/devicerunner.go +++ b/internal/driver/ioscompanion/devicerunner.go @@ -162,16 +162,16 @@ func (d *Driver) realSpawnDeviceRunner(ctx context.Context, address string) (*ex } // buildDeviceRunnerIfNeeded regenerates the project and runs build-for-testing, -// skipping the build when a marker recording the current source hash already +// skipping the build when a marker recording the current build key already // matches. The device signature is per-account/per-device, so the build cannot // be embedded; the stable derivedDataPath makes the build incremental. func (d *Driver) buildDeviceRunnerIfNeeded(ctx context.Context, companionDir, derivedDataPath string, creds signingCredentials) error { - sources, err := sourceHash(companionDir) + key, err := buildCacheKey(companionDir, creds) if err != nil { return err } marker := filepath.Join(derivedDataPath, "device-runner.sha256") - if existing, readErr := os.ReadFile(marker); readErr == nil && string(existing) == sources { + if existing, readErr := os.ReadFile(marker); readErr == nil && string(existing) == key { fmt.Fprintln(d.output, "device runner build is up to date; skipping build") return nil } @@ -186,12 +186,24 @@ func (d *Driver) buildDeviceRunnerIfNeeded(ctx context.Context, companionDir, de if out, buildErr := runQuiet(ctx, companionDir, append([]string{"xcrun"}, args...)...); buildErr != nil { return fmt.Errorf("build-for-testing: %w: %s", buildErr, tailLines(string(out), 20)) } - if err := os.WriteFile(marker, []byte(sources), 0o644); err != nil { + if err := os.WriteFile(marker, []byte(key), 0o644); err != nil { return err } return nil } +// buildCacheKey combines the source hash with the signing identity so a changed +// team or key invalidates the cached build. A runner signed with a stale +// identity would otherwise be reused and rejected at install (0xe8008018). +func buildCacheKey(companionDir string, creds signingCredentials) (string, error) { + sources, err := sourceHash(companionDir) + if err != nil { + return "", err + } + sum := sha256.Sum256([]byte(sources + "\x00" + creds.team + "\x00" + creds.authKeyID)) + return hex.EncodeToString(sum[:]), nil +} + // xcodegenArgs regenerates the runner project from its spec. func xcodegenArgs(specPath string) []string { return []string{"xcodegen", "--spec", specPath} diff --git a/internal/driver/ioscompanion/devicerunner_test.go b/internal/driver/ioscompanion/devicerunner_test.go index 976698a..7688c31 100644 --- a/internal/driver/ioscompanion/devicerunner_test.go +++ b/internal/driver/ioscompanion/devicerunner_test.go @@ -168,6 +168,37 @@ func TestReadSigningCredentialsResolvesRelativeKeyPath(t *testing.T) { } } +func TestBuildCacheKeyChangesWithSigningIdentity(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "Sources"), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "Sources", "Server.swift"), []byte("v1"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "project.yml"), []byte("name: x"), 0o644); err != nil { + t.Fatal(err) + } + base, err := buildCacheKey(dir, signingCredentials{team: "TEAM1", authKeyID: "KID1"}) + if err != nil { + t.Fatal(err) + } + otherTeam, err := buildCacheKey(dir, signingCredentials{team: "TEAM2", authKeyID: "KID1"}) + if err != nil { + t.Fatal(err) + } + otherKey, err := buildCacheKey(dir, signingCredentials{team: "TEAM1", authKeyID: "KID2"}) + if err != nil { + t.Fatal(err) + } + if base == otherTeam { + t.Fatal("a changed team must invalidate the cached build") + } + if base == otherKey { + t.Fatal("a changed signing key must invalidate the cached build") + } +} + func TestSourceHashChangesWithSources(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "Sources"), 0o755); err != nil {