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
44 changes: 10 additions & 34 deletions pkg/native/native_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,6 @@ func New(dataDir, jobID, jitConfig, runnerSrc string, log *slog.Logger) (*Runner
filepath.Join(jobDir, "tmp"),
filepath.Join(jobDir, "work"),
filepath.Join(jobDir, "runner"),
filepath.Join(jobDir, "homebrew", "bin"),
filepath.Join(jobDir, "homebrew", "Cellar"),
filepath.Join(jobDir, "keychain"),
}
for _, d := range dirs {
Expand Down Expand Up @@ -264,25 +262,25 @@ func (r *Runner) Start(ctx context.Context) error {
// Non-fatal: jobs that don't need signing will work fine
}

// Symlink Homebrew binaries from host
if err := symlinkHomebrew(filepath.Join(r.jobDir, "homebrew", "bin")); err != nil {
r.log.Warn("failed to symlink homebrew binaries", "error", err)
// Non-fatal: host may not have Homebrew installed
}

// Build environment
homeDir := filepath.Join(r.jobDir, "home")
tmpDir := filepath.Join(r.jobDir, "tmp")
workDir := filepath.Join(r.jobDir, "work")
brewDir := filepath.Join(r.jobDir, "homebrew")

// Use the host's real Homebrew (read-only: the sandbox denies writes to
// /opt/homebrew). Pointing HOMEBREW_PREFIX/CELLAR at a per-job empty prefix
// made `brew list`-style checks (e.g. spc doctor) see nothing installed, so
// tools tried to (re)install formulae the host already has — then failed on
// the write-deny. Sharing the host prefix read-only lets jobs USE the
// pre-installed deps without being able to mutate them.
const hostBrewPrefix = "/opt/homebrew"
env := []string{
"HOME=" + homeDir,
"TMPDIR=" + tmpDir,
"RUNNER_WORK_FOLDER=" + workDir,
"PATH=" + filepath.Join(brewDir, "bin") + ":/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"HOMEBREW_PREFIX=" + brewDir,
"HOMEBREW_CELLAR=" + filepath.Join(brewDir, "Cellar"),
"PATH=" + hostBrewPrefix + "/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
"HOMEBREW_PREFIX=" + hostBrewPrefix,
"HOMEBREW_CELLAR=" + hostBrewPrefix + "/Cellar",
"HOMEBREW_TEMP=" + tmpDir,
"LANG=en_US.UTF-8",
}
Expand Down Expand Up @@ -545,28 +543,6 @@ func GenerateSandboxProfile(jobDir, dataDir string) string {
`, homeDir, absDataDir, absJobDir)
}

// symlinkHomebrew creates symlinks from /opt/homebrew/bin/* into the
// per-job homebrew bin directory, giving jobs read access to pre-installed
// tools while keeping their own installs isolated.
func symlinkHomebrew(destBin string) error {
const hostBin = "/opt/homebrew/bin"
entries, err := os.ReadDir(hostBin)
if err != nil {
return fmt.Errorf("reading %s: %w", hostBin, err)
}
for _, e := range entries {
src := filepath.Join(hostBin, e.Name())
dst := filepath.Join(destBin, e.Name())
if err := os.Symlink(src, dst); err != nil {
// Skip if symlink already exists
if !os.IsExist(err) {
return fmt.Errorf("symlinking %s: %w", e.Name(), err)
}
}
}
return nil
}

// copyRunnerFiles copies the runner directory to the per-job location.
// Uses hard links for efficiency, falling back to full copy on error.
func copyRunnerFiles(src, dst string) error {
Expand Down
6 changes: 3 additions & 3 deletions pkg/native/native_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ func TestNewCreatesWorkspace(t *testing.T) {
t.Fatalf("New() error: %v", err)
}

// Verify expected directories exist
// Verify expected directories exist. Note: there is no per-job "homebrew"
// dir — native jobs use the host's shared /opt/homebrew (read-only) so tool
// checks like `spc doctor` see the host's installed formulae.
expectedDirs := []string{
"home",
"tmp",
"work",
"runner",
filepath.Join("homebrew", "bin"),
filepath.Join("homebrew", "Cellar"),
"keychain",
}
for _, d := range expectedDirs {
Expand Down