Skip to content

Commit 98937cc

Browse files
Badlazzorclaude
andauthored
feat: ACI-5273 doctor probes xcodebuild/xcrun resolve to the xcelerate wrapper (#453)
Warns when `activate xcode` has installed the wrapper but the current shell still resolves `xcodebuild` or `xcrun` to /usr/bin — the case where the user runs a build in the same shell they ran activate in and gets no cache. Gated on Xcelerate being activated; skipped otherwise. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 9679138 commit 98937cc

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package doctor
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/paths"
10+
"github.com/bitrise-io/bitrise-build-cache-cli/v3/internal/toolconfig"
11+
)
12+
13+
func (d *Doctor) xcelerateWrapperPathCheck() Check {
14+
return Check{
15+
Name: "xcelerate-wrapper-path",
16+
Diagnose: func(_ context.Context) Result {
17+
if !d.toolActivated(toolconfig.Xcelerate) {
18+
return Result{State: StateOK, Detail: "skipped (xcode not activated)"}
19+
}
20+
21+
home, err := os.UserHomeDir()
22+
if err != nil {
23+
return Result{State: StateError, Detail: "resolve home dir: " + err.Error()}
24+
}
25+
26+
binDir := paths.FromHome(home).XcelerateBinDir()
27+
28+
for _, name := range []string{"xcodebuild", "xcrun"} {
29+
if res, ok := diagnoseWrapperOnPath(name, binDir, d.LookPath); !ok {
30+
return res
31+
}
32+
}
33+
34+
return Result{State: StateOK, Detail: "xcodebuild, xcrun resolve to the xcelerate wrapper"}
35+
},
36+
}
37+
}
38+
39+
func diagnoseWrapperOnPath(name, binDir string, lookPath func(string) (string, error)) (Result, bool) {
40+
expected := filepath.Join(binDir, name)
41+
42+
actual, err := lookPath(name)
43+
if err != nil {
44+
return Result{State: StateWarn, Detail: fmt.Sprintf("%s not on PATH — open a new terminal or `source ~/.zshrc` so the wrapper installed by `activate xcode` becomes visible", name)}, false
45+
}
46+
47+
if pathsEqual(actual, expected) {
48+
return Result{}, true
49+
}
50+
51+
return Result{State: StateWarn, Detail: fmt.Sprintf("%s resolves to %s; open a new terminal or `source ~/.zshrc` — your current shell hasn't picked up the wrapper PATH added by `activate xcode` (expected %s)", name, actual, expected)}, false
52+
}
53+
54+
func pathsEqual(a, b string) bool {
55+
return resolveSymlinks(a) == resolveSymlinks(b)
56+
}
57+
58+
func resolveSymlinks(p string) string {
59+
if resolved, err := filepath.EvalSymlinks(p); err == nil {
60+
return resolved
61+
}
62+
63+
return p
64+
}

internal/doctor/doctor.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ func (d *Doctor) checks(opts Options) []Check {
263263

264264
checks = append(checks,
265265
d.xcelerateProxyCheck(),
266+
d.xcelerateWrapperPathCheck(),
266267
d.enrichmentCheck(),
267268
d.ccacheHelperCheck(),
268269
d.ccacheBinaryCheck(),

internal/doctor/doctor_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,91 @@ func TestXcelerateProxyCheck_skippedWhenNotActivated(t *testing.T) {
231231
assert.Contains(t, res.Detail, "skipped")
232232
}
233233

234+
// ──────────────────────────── xcelerate wrapper path ────────────────────────────
235+
236+
func TestXcelerateWrapperPathCheck_skippedWhenNotActivated(t *testing.T) {
237+
r := &Doctor{ActivatedTools: func() map[toolconfig.Tool]bool { return nil }}
238+
239+
res := r.xcelerateWrapperPathCheck().Diagnose(context.Background())
240+
assert.Equal(t, StateOK, res.State)
241+
assert.Contains(t, res.Detail, "skipped")
242+
}
243+
244+
func TestXcelerateWrapperPathCheck_wrapperOnPath(t *testing.T) {
245+
home := t.TempDir()
246+
t.Setenv("HOME", home)
247+
binDir := filepath.Join(home, ".bitrise-xcelerate", "bin")
248+
require.NoError(t, os.MkdirAll(binDir, 0o755))
249+
250+
lookPath := func(name string) (string, error) {
251+
return filepath.Join(binDir, name), nil
252+
}
253+
254+
r := &Doctor{
255+
ActivatedTools: func() map[toolconfig.Tool]bool { return map[toolconfig.Tool]bool{toolconfig.Xcelerate: true} },
256+
LookPath: lookPath,
257+
}
258+
259+
res := r.xcelerateWrapperPathCheck().Diagnose(context.Background())
260+
assert.Equal(t, StateOK, res.State)
261+
assert.Contains(t, res.Detail, "xcodebuild")
262+
assert.Contains(t, res.Detail, "xcrun")
263+
}
264+
265+
func TestXcelerateWrapperPathCheck_mismatchIsWarn(t *testing.T) {
266+
home := t.TempDir()
267+
t.Setenv("HOME", home)
268+
269+
r := &Doctor{
270+
ActivatedTools: func() map[toolconfig.Tool]bool { return map[toolconfig.Tool]bool{toolconfig.Xcelerate: true} },
271+
LookPath: func(string) (string, error) { return "/usr/bin/xcodebuild", nil },
272+
}
273+
274+
res := r.xcelerateWrapperPathCheck().Diagnose(context.Background())
275+
assert.Equal(t, StateWarn, res.State)
276+
assert.Contains(t, res.Detail, "xcodebuild resolves to /usr/bin/xcodebuild")
277+
assert.Contains(t, res.Detail, "source ~/.zshrc")
278+
}
279+
280+
func TestXcelerateWrapperPathCheck_notOnPathIsWarn(t *testing.T) {
281+
home := t.TempDir()
282+
t.Setenv("HOME", home)
283+
284+
r := &Doctor{
285+
ActivatedTools: func() map[toolconfig.Tool]bool { return map[toolconfig.Tool]bool{toolconfig.Xcelerate: true} },
286+
LookPath: func(string) (string, error) { return "", errors.New("not found") },
287+
}
288+
289+
res := r.xcelerateWrapperPathCheck().Diagnose(context.Background())
290+
assert.Equal(t, StateWarn, res.State)
291+
assert.Contains(t, res.Detail, "xcodebuild not on PATH")
292+
}
293+
294+
func TestXcelerateWrapperPathCheck_symlinkedWrapperIsOK(t *testing.T) {
295+
home := t.TempDir()
296+
t.Setenv("HOME", home)
297+
binDir := filepath.Join(home, ".bitrise-xcelerate", "bin")
298+
require.NoError(t, os.MkdirAll(binDir, 0o755))
299+
300+
realDir := filepath.Join(home, "real-bin")
301+
require.NoError(t, os.MkdirAll(realDir, 0o755))
302+
for _, name := range []string{"xcodebuild", "xcrun"} {
303+
realPath := filepath.Join(realDir, name)
304+
require.NoError(t, os.WriteFile(realPath, nil, 0o755))
305+
require.NoError(t, os.Symlink(realPath, filepath.Join(binDir, name)))
306+
}
307+
308+
r := &Doctor{
309+
ActivatedTools: func() map[toolconfig.Tool]bool { return map[toolconfig.Tool]bool{toolconfig.Xcelerate: true} },
310+
LookPath: func(name string) (string, error) {
311+
return filepath.Join(realDir, name), nil
312+
},
313+
}
314+
315+
res := r.xcelerateWrapperPathCheck().Diagnose(context.Background())
316+
assert.Equal(t, StateOK, res.State)
317+
}
318+
234319
// ──────────────────────────── ccache ────────────────────────────
235320

236321
func TestCcacheHelperCheck_noSocketIsWarn(t *testing.T) {

0 commit comments

Comments
 (0)