diff --git a/chore/gentests/gentests.go b/chore/gentests/gentests.go index 819dfaf5ed..c306925bf3 100644 --- a/chore/gentests/gentests.go +++ b/chore/gentests/gentests.go @@ -27,10 +27,12 @@ import ( "github.com/goplus/llgo/internal/littest" "github.com/goplus/llgo/internal/llgen" "github.com/goplus/llgo/internal/lto" + "github.com/goplus/llgo/xtool/env/llvm" "github.com/goplus/mod" ) func main() { + llvm.SetupPath() dir, _, err := mod.FindGoMod(".") check(err) diff --git a/chore/litgen/litgen.go b/chore/litgen/litgen.go index 8aca3061a4..34d51fe437 100644 --- a/chore/litgen/litgen.go +++ b/chore/litgen/litgen.go @@ -23,9 +23,11 @@ import ( "path/filepath" "github.com/goplus/llgo/internal/littest" + "github.com/goplus/llgo/xtool/env/llvm" ) func main() { + llvm.SetupPath() flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [...]\n", filepath.Base(os.Args[0])) flag.PrintDefaults() diff --git a/chore/llgen/llgen.go b/chore/llgen/llgen.go index 64927b6eec..7c379b92e1 100644 --- a/chore/llgen/llgen.go +++ b/chore/llgen/llgen.go @@ -23,6 +23,7 @@ import ( "github.com/goplus/llgo/internal/build" "github.com/goplus/llgo/internal/llgen" + "github.com/goplus/llgo/xtool/env/llvm" ) var ( @@ -31,6 +32,7 @@ var ( ) func main() { + llvm.SetupPath() flag.Parse() if len(flag.Args()) != 1 { fmt.Fprintln(os.Stderr, "Usage: llgen [flags] ") diff --git a/cmd/llgo/llvm_path.go b/cmd/llgo/llvm_path.go new file mode 100644 index 0000000000..7a804540ff --- /dev/null +++ b/cmd/llgo/llvm_path.go @@ -0,0 +1,10 @@ +package main + +import "github.com/goplus/llgo/xtool/env/llvm" + +// LLVM is part of the llgo process environment. Prepare PATH before command +// dispatch so build requests and their workers only need an environment +// snapshot; they do not carry or reselect an LLVM installation. +func init() { + llvm.SetupPath() +} diff --git a/internal/build/build.go b/internal/build/build.go index c2f80c4136..3e50566a86 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -61,7 +61,6 @@ import ( "github.com/goplus/llgo/internal/typepatch" "github.com/goplus/llgo/ssa/abi" xenv "github.com/goplus/llgo/xtool/env" - "github.com/goplus/llgo/xtool/env/llvm" gllvm "github.com/xgo-dev/llvm" llruntime "github.com/goplus/llgo/runtime" @@ -213,6 +212,78 @@ type Config struct { type Rewrites map[string]string +// clone returns an independent copy of c for use by a single build. Do +// resolves defaults and target-specific values on this copy so callers can +// safely reuse their input configuration after Do returns. +func (c *Config) clone() *Config { + if c == nil { + return nil + } + cloned := *c + cloned.RunArgs = slices.Clone(c.RunArgs) + cloned.GoBuildFlags = slices.Clone(c.GoBuildFlags) + cloned.Overlay = cloneOverlay(c.Overlay) + if c.GlobalRewrites != nil { + cloned.GlobalRewrites = make(map[string]Rewrites, len(c.GlobalRewrites)) + for pkgPath, rewrites := range c.GlobalRewrites { + if rewrites == nil { + cloned.GlobalRewrites[pkgPath] = nil + continue + } + copied := make(Rewrites, len(rewrites)) + for name, value := range rewrites { + copied[name] = value + } + cloned.GlobalRewrites[pkgPath] = copied + } + } + return &cloned +} + +// resolveBuildConfig validates and fills build-local defaults without +// modifying the caller's Config. Target-derived GOOS/GOARCH values are +// resolved later, after crosscompile.Use has selected the toolchain. +func resolveBuildConfig(input *Config) (*Config, error) { + if input == nil { + return nil, errors.New("build config must not be nil") + } + conf := input.clone() + if conf.Goos == "" { + conf.Goos = runtime.GOOS + } + if conf.Goarch == "" { + conf.Goarch = runtime.GOARCH + } + if conf.AppExt == "" { + conf.AppExt = defaultAppExt(conf) + } + if conf.BuildMode == "" { + conf.BuildMode = BuildModeExe + } + if conf.BuildMode != BuildModeExe { + conf.DeadcodeDrop = false + } + conf.PCLNMode = effectivePCLNMode(conf) + conf.PCLNModeSet = true + if conf.SizeReport && conf.SizeFormat == "" { + conf.SizeFormat = "text" + } + if conf.SizeReport && conf.SizeLevel == "" { + conf.SizeLevel = "module" + } + if err := validatePCLNMode(conf); err != nil { + return nil, err + } + if err := ensureSizeReporting(conf); err != nil { + return nil, err + } + if err := conf.LinkOptions.validate(); err != nil { + return nil, err + } + conf.OptLevel = effectiveOptLevel(conf) + return conf, nil +} + func NewDefaultConf(mode Mode) *Config { bin := os.Getenv("GOBIN") if bin == "" { @@ -222,9 +293,6 @@ func NewDefaultConf(mode Mode) *Config { } bin = filepath.Join(gopath, "bin") } - if err := os.MkdirAll(bin, 0755); err != nil { - panic(fmt.Errorf("cannot create bin directory: %v", err)) - } goos, goarch := os.Getenv("GOOS"), os.Getenv("GOARCH") if goos == "" { goos = runtime.GOOS @@ -292,39 +360,25 @@ const ( ) func Do(args []string, conf *Config) ([]Package, error) { - if conf.Goos == "" { - conf.Goos = runtime.GOOS - } - if conf.Goarch == "" { - conf.Goarch = runtime.GOARCH - } - if conf.AppExt == "" { - conf.AppExt = defaultAppExt(conf) - } - if conf.BuildMode == "" { - conf.BuildMode = BuildModeExe - } - if conf.BuildMode != BuildModeExe { - conf.DeadcodeDrop = false - } - conf.PCLNMode = effectivePCLNMode(conf) - conf.PCLNModeSet = true - if conf.SizeReport && conf.SizeFormat == "" { - conf.SizeFormat = "text" - } - if conf.SizeReport && conf.SizeLevel == "" { - conf.SizeLevel = "module" - } - if err := validatePCLNMode(conf); err != nil { - return nil, err - } - if err := ensureSizeReporting(conf); err != nil { - return nil, err + return Build(Invocation{Args: args, Config: conf}) +} + +// Build executes one build invocation. +func Build(inv Invocation) ([]Package, error) { + dir := inv.Dir + if dir == "" { + var err error + dir, err = os.Getwd() + if err != nil { + return nil, err + } } - if err := conf.LinkOptions.validate(); err != nil { + environ := os.Environ() + commands := commandEnv{dir: dir, environ: environ} + conf, err := resolveBuildConfig(inv.Config) + if err != nil { return nil, err } - conf.OptLevel = effectiveOptLevel(conf) // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled()) @@ -348,7 +402,7 @@ func Do(args []string, conf *Config) ([]Package, error) { } verbose := conf.Verbose - patterns := args + patterns := slices.Clone(inv.Args) tags := defaultBuildTags(conf.Goarch, conf.Target) if conf.PCLNMode == PCLNExternal { // Select the optional runtime loader as part of the normal package @@ -370,9 +424,10 @@ func Do(args []string, conf *Config) ([]Package, error) { cfg := &packages.Config{ Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile, BuildFlags: goBuildFlags, + Dir: dir, Fset: token.NewFileSet(), Tests: conf.Mode == ModeTest, - Env: append(slices.Clone(os.Environ()), "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), + Env: withEnv(environ, "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), } if conf.Mode == ModeTest { cfg.Mode |= packages.NeedForTest @@ -546,11 +601,8 @@ func Do(args []string, conf *Config) ([]Package, error) { patches := make(cl.Patches, len(altPkgPaths)) altSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) - env := llvm.New("") - os.Setenv("PATH", env.BinDir()+":"+os.Getenv("PATH")) // TODO(xsw): check windows - output := conf.OutFile != "" - ctx := &context{env: env, conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, + ctx := &context{conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, patches: patches, callerTracking: cl.NewCallerTracking(), built: make(map[string]none), initial: initial, mode: mode, fingerprinting: make(map[string]bool), @@ -560,6 +612,7 @@ func Do(args []string, conf *Config) ([]Package, error) { passOpt: passOpt, buildConf: conf, crossCompile: export, + commands: commands, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), } defer ctx.closePackageMetas() @@ -601,6 +654,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + resolveOutputs(ctx.commands.dir, outFmts) // Link main package using the output path from buildOutFmts err = linkMainPkg(ctx, pkg, allPkgs, outFmts.Out, verbose) @@ -656,7 +710,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if conf.Target == "" { err = runNative(ctx, outFmts.Out, pkg.Dir, pkg.PkgPath, conf, mode) } else if conf.Emulator { - err = runInEmulator(ctx.crossCompile.Emulator, envMap, pkg.Dir, pkg.PkgPath, conf, mode, verbose) + err = runInEmulator(ctx.commands, ctx.crossCompile.Emulator, envMap, pkg.Dir, pkg.PkgPath, conf, mode, verbose) } else { err = flash.FlashDevice(ctx.crossCompile.Device, envMap, ctx.buildConf.Port, verbose) if err != nil { @@ -798,7 +852,6 @@ const ( ) type context struct { - env *llvm.Env conf *packages.Config progSSA *ssa.Program prog llssa.Program @@ -817,6 +870,7 @@ type context struct { buildConf *Config crossCompile crosscompile.Export + commands commandEnv cTransformer *cabi.Transformer @@ -860,6 +914,8 @@ func (c *context) compiler() *clang.Cmd { c.crossCompile.Linker, ) cmd := clang.NewCompiler(config) + cmd.Dir = c.commands.dir + cmd.Env = slices.Clone(c.commands.environ) cmd.Verbose = c.shouldPrintCommands(false) return cmd } @@ -873,6 +929,8 @@ func (c *context) linker() *clang.Cmd { c.crossCompile.Linker, ) cmd := clang.NewLinker(config) + cmd.Dir = c.commands.dir + cmd.Env = slices.Clone(c.commands.environ) cmd.Verbose = c.shouldPrintCommands(false) return cmd } @@ -1394,6 +1452,11 @@ func isRuntimePkg(pkgPath string) bool { func linkObjFiles(ctx *context, app string, objFiles, linkArgs []string, verbose bool) error { printCmds := ctx.shouldPrintCommands(verbose) + if dir := filepath.Dir(app); dir != "." { + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create output directory %s: %w", dir, err) + } + } // Handle c-archive mode differently - use ar tool instead of linker if ctx.buildConf.BuildMode == BuildModeCArchive { return ctx.createMergedArchiveFile(app, objFiles, printCmds) @@ -1597,7 +1660,7 @@ func (c *context) createMergedArchiveFile(archivePath string, inputs []string, v if err != nil { return err } - cmd := exec.Command(arCmd, "-M") + cmd := c.commands.configure(exec.Command(arCmd, "-M")) cmd.Stdin = strings.NewReader(script.String()) printCmds := c.shouldPrintCommands(len(verbose) > 0 && verbose[0]) if printCmds { @@ -1636,7 +1699,7 @@ func (c *context) createArchiveFile(archivePath string, objFiles []string, verbo args := append([]string{"rcs", tmpName}, objFiles...) arCmd := c.archiver() - cmd := exec.Command(arCmd, args...) + cmd := c.commands.configure(exec.Command(arCmd, args...)) printCmds := c.shouldPrintCommands(len(verbose) > 0 && verbose[0]) if printCmds { fmt.Fprintf(os.Stderr, "%s %s\n", filepath.Base(arCmd), strings.Join(args, " ")) @@ -1843,7 +1906,7 @@ func dumpLLVMIRIfNeeded(ctx *context, pkgPath string, exportFile string, data st return err } if ctx.buildConf.CheckLLFiles { - if msg, err := llcCheck(ctx.env, f.Name()); err != nil { + if msg, err := llcCheck(ctx.commands, f.Name()); err != nil { fmt.Fprintf(os.Stderr, "==> llc %v: %v\n%v\n", pkgPath, f.Name(), msg) } } @@ -1927,7 +1990,7 @@ func exportObjectWithClang(ctx *context, pkgPath string, exportFile string, data return exportFile, err } if ctx.buildConf.CheckLLFiles { - if msg, err := llcCheck(ctx.env, f.Name()); err != nil { + if msg, err := llcCheck(ctx.commands, f.Name()); err != nil { fmt.Fprintf(os.Stderr, "==> llc %v: %v\n%v\n", pkgPath, f.Name(), msg) } } @@ -1955,9 +2018,8 @@ func exportObjectWithClang(ctx *context, pkgPath string, exportFile string, data return objFile.Name(), cmd.Compile(args...) } -func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) { - bin := filepath.Join(env.BinDir(), "llc") - cmd := exec.Command(bin, "-filetype=null", exportFile) +func llcCheck(commands commandEnv, exportFile string) (msg string, err error) { + cmd := commands.configure(exec.Command("llc", "-filetype=null", exportFile)) var buf bytes.Buffer cmd.Stderr = &buf if err = cmd.Run(); err != nil { diff --git a/internal/build/build_test.go b/internal/build/build_test.go index cf3054c1e9..0b2ff86fe9 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -16,6 +16,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "slices" "strconv" @@ -44,6 +45,180 @@ func TestMain(m *testing.M) { os.Exit(code) } +func TestConfigCloneDoesNotAliasInput(t *testing.T) { + input := &Config{ + RunArgs: []string{"run"}, + GoBuildFlags: []string{"-tags=custom"}, + Overlay: map[string][]byte{"input.go": []byte("package input")}, + GlobalRewrites: map[string]Rewrites{ + "example.com/p": {"value": "input"}, + "nil": nil, + }, + } + cloned := input.clone() + cloned.RunArgs[0] = "changed" + cloned.GoBuildFlags[0] = "-tags=changed" + cloned.Overlay["input.go"][0] = 'P' + cloned.GlobalRewrites["example.com/p"]["value"] = "changed" + cloned.GlobalRewrites["new"] = Rewrites{"value": "new"} + + if got := input.RunArgs[0]; got != "run" { + t.Fatalf("input RunArgs changed to %q", got) + } + if got := input.GoBuildFlags[0]; got != "-tags=custom" { + t.Fatalf("input GoBuildFlags changed to %q", got) + } + if got := string(input.Overlay["input.go"]); got != "package input" { + t.Fatalf("input overlay changed to %q", got) + } + if got := input.GlobalRewrites["example.com/p"]["value"]; got != "input" { + t.Fatalf("input rewrite changed to %q", got) + } + if _, ok := input.GlobalRewrites["new"]; ok { + t.Fatal("cloned rewrite map aliases input map") + } + if rewrites, ok := cloned.GlobalRewrites["nil"]; !ok || rewrites != nil { + t.Fatalf("nil rewrite entry was not preserved: %#v", rewrites) + } + if got := (*Config)(nil).clone(); got != nil { + t.Fatalf("nil Config clone = %#v", got) + } +} + +func TestResolveBuildConfigDefaultsAndValidation(t *testing.T) { + resolved, err := resolveBuildConfig(&Config{ + BuildMode: BuildModeCArchive, + DeadcodeDrop: true, + SizeReport: true, + }) + if err != nil { + t.Fatal(err) + } + if resolved.DeadcodeDrop { + t.Fatal("non-executable build retained dead-code dropping") + } + if resolved.SizeFormat != "text" || resolved.SizeLevel != "module" { + t.Fatalf("size report defaults = %q, %q", resolved.SizeFormat, resolved.SizeLevel) + } + if _, err := resolveBuildConfig(&Config{SizeReport: true, SizeLevel: "invalid"}); err == nil { + t.Fatal("invalid size-reporting level succeeded") + } + if _, err := resolveBuildConfig(nil); err == nil { + t.Fatal("nil build config succeeded") + } +} + +func TestNewDefaultConfDoesNotCreateBinDir(t *testing.T) { + binDir := filepath.Join(t.TempDir(), "not-created", "bin") + t.Setenv("GOBIN", binDir) + conf := NewDefaultConf(ModeBuild) + if conf.BinPath != binDir { + t.Fatalf("BinPath = %q, want %q", conf.BinPath, binDir) + } + if _, err := os.Stat(binDir); !os.IsNotExist(err) { + t.Fatalf("NewDefaultConf created bin directory: %v", err) + } +} + +func TestDoDoesNotModifyConfigOnValidationError(t *testing.T) { + input := &Config{ + RunArgs: []string{"arg"}, + GlobalRewrites: map[string]Rewrites{ + "example.com/p": {"value": "input"}, + }, + LinkOptions: LinkOptions{DWARF: DWARFMode(255)}, + } + before := input.clone() + if _, err := Do(nil, input); err == nil { + t.Fatal("Do() succeeded with invalid DWARF mode") + } + if !reflect.DeepEqual(input, before) { + t.Fatalf("Do() modified input config:\n got: %#v\nwant: %#v", input, before) + } + if _, err := Do(nil, nil); err == nil { + t.Fatal("Do() succeeded with nil config") + } +} + +func TestInvocationUsesExplicitWorkingDirectory(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/requestdir\n\ngo 1.24\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "requestdir.go"), []byte("package requestdir\n\nfunc F() {}\n"), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + t.Setenv(llgoBuildCache, "0") + ambientPath := os.Getenv("PATH") + pkgs, err := Build(Invocation{ + Args: []string{"."}, + Config: conf, + Dir: dir, + }) + if err != nil { + t.Fatal(err) + } + if len(pkgs) != 1 || pkgs[0].PkgPath != "example.com/requestdir" { + t.Fatalf("Build returned packages = %+v, want example.com/requestdir", pkgs) + } + if got := os.Getenv("PATH"); got != ambientPath { + t.Fatalf("Build changed process PATH from %q to %q", ambientPath, got) + } + pkgs[0].LPkg.Prog.Dispose() +} + +func TestResolveOutputsUsesInvocationDirectory(t *testing.T) { + dir := t.TempDir() + out := &OutFmtDetails{ + Out: "app", + Bin: filepath.Join("firmware", "app.bin"), + Hex: filepath.Join(dir, "app.hex"), + } + resolveOutputs(dir, out) + if out.Out != filepath.Join(dir, "app") { + t.Fatalf("Out = %q", out.Out) + } + if out.Bin != filepath.Join(dir, "firmware", "app.bin") { + t.Fatalf("Bin = %q", out.Bin) + } + if out.Hex != filepath.Join(dir, "app.hex") { + t.Fatalf("absolute Hex changed to %q", out.Hex) + } +} + +func TestConfigureCommandUsesBuildSnapshot(t *testing.T) { + dir := t.TempDir() + commands := commandEnv{dir: dir, environ: []string{"BUILD_MARKER=before"}} + cmd := commands.configure(exec.Command("unused")) + commands.environ[0] = "BUILD_MARKER=after" + if cmd.Dir != dir { + t.Fatalf("command Dir = %q, want %q", cmd.Dir, dir) + } + if got, want := cmd.Env, []string{"BUILD_MARKER=before"}; !slices.Equal(got, want) { + t.Fatalf("command Env = %q, want %q", got, want) + } +} + +func TestLinkObjFilesReportsOutputDirectoryError(t *testing.T) { + parent := filepath.Join(t.TempDir(), "not-a-directory") + if err := os.WriteFile(parent, nil, 0o644); err != nil { + t.Fatal(err) + } + ctx := &context{buildConf: &Config{BuildMode: BuildModeExe}} + if err := linkObjFiles(ctx, filepath.Join(parent, "app"), nil, nil, false); err == nil { + t.Fatal("linkObjFiles succeeded below a regular file") + } +} + +func TestWithEnvLastValueWins(t *testing.T) { + got := withEnv([]string{"A=old", "B=keep", "malformed", "A=older"}, "A=new", "C=value") + want := []string{"B=keep", "A=new", "C=value"} + if !slices.Equal(got, want) { + t.Fatalf("withEnv = %q, want %q", got, want) + } +} + func TestClosePackageMetas(t *testing.T) { b := meta.NewBuilder() b.Sym("pkg.main") diff --git a/internal/build/cgo.go b/internal/build/cgo.go index ac354e4caf..c6a5d47112 100644 --- a/internal/build/cgo.go +++ b/internal/build/cgo.go @@ -73,7 +73,7 @@ func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, } buildCtx.BuildTags = parseSourcePatchBuildTags(ctx.conf.BuildFlags) - srcFiles, preambles, cdecls, err := parseCgo_(&buildCtx, pkg, files) + srcFiles, preambles, cdecls, err := parseCgoWithCommandEnv(ctx.commands, &buildCtx, pkg, files) if err != nil { return } @@ -126,7 +126,7 @@ func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, tmpName := tmpFile.Name() defer os.Remove(tmpName) code := cgoHeader + "\n\n" + preamble.src - externDecls, err := genExternDeclsByClang(pkg, code, cflags, cgoSymbols, verbose) + externDecls, err := genExternDeclsByClang(ctx.commands, pkg, code, cflags, cgoSymbols, verbose) if err != nil { return nil, nil, fmt.Errorf("failed to generate extern decls: %v", err) } @@ -178,7 +178,7 @@ type clangASTNode struct { Inner []clangASTNode `json:"inner,omitempty"` } -func genExternDeclsByClang(pkg *aPackage, src string, cflags []string, cgoSymbols map[string]string, verbose bool) (string, error) { +func genExternDeclsByClang(commands commandEnv, pkg *aPackage, src string, cflags []string, cgoSymbols map[string]string, verbose bool) (string, error) { tmpSrc, err := os.CreateTemp("", "cgo-src-*.c") if err != nil { return "", fmt.Errorf("failed to create temp file: %v", err) @@ -188,11 +188,11 @@ func genExternDeclsByClang(pkg *aPackage, src string, cflags []string, cgoSymbol return "", fmt.Errorf("failed to write temp file: %v", err) } symbolNames := make(map[string]bool) - if err := getFuncNames(tmpSrc.Name(), cflags, symbolNames, verbose); err != nil { + if err := getFuncNames(commands, tmpSrc.Name(), cflags, symbolNames, verbose); err != nil { return "", fmt.Errorf("failed to get func names: %v", err) } macroNames := make(map[string]bool) - if err := getMacroNames(tmpSrc.Name(), cflags, macroNames, verbose); err != nil { + if err := getMacroNames(commands, tmpSrc.Name(), cflags, macroNames, verbose); err != nil { return "", fmt.Errorf("failed to get macro names: %v", err) } @@ -245,10 +245,10 @@ static void _init_%s() { return b.String(), nil } -func getMacroNames(file string, cflags []string, macroNames map[string]bool, verbose bool) error { +func getMacroNames(commands commandEnv, file string, cflags []string, macroNames map[string]bool, verbose bool) error { args := append([]string{"-dM", "-E"}, cflags...) args = append(args, file) - cmd := execCommandVerbose(verbose, "clang", args...) + cmd := execCommandVerbose(commands, verbose, "clang", args...) output, err := cmd.Output() if err != nil { return err @@ -265,10 +265,10 @@ func getMacroNames(file string, cflags []string, macroNames map[string]bool, ver return nil } -func getFuncNames(file string, cflags []string, symbolNames map[string]bool, verbose bool) error { +func getFuncNames(commands commandEnv, file string, cflags []string, symbolNames map[string]bool, verbose bool) error { args := append([]string{"-Xclang", "-ast-dump=json", "-fsyntax-only"}, cflags...) args = append(args, file) - cmd := execCommandVerbose(verbose, "clang", args...) + cmd := execCommandVerbose(commands, verbose, "clang", args...) cmd.Stderr = os.Stderr output, err := cmd.Output() if err != nil { @@ -295,11 +295,12 @@ func getFuncNames(file string, cflags []string, symbolNames map[string]bool, ver return nil } -func execCommandVerbose(verbose bool, name string, arg ...string) *exec.Cmd { +func execCommandVerbose(commands commandEnv, verbose bool, name string, arg ...string) *exec.Cmd { if verbose { fmt.Fprintf(os.Stderr, "%s %s\n", name, strings.Join(arg, " ")) } - return exec.Command(name, arg...) + cmd := exec.Command(name, arg...) + return commands.configure(cmd) } func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { @@ -311,6 +312,10 @@ func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { } func parseCgo_(buildCtx *build.Context, pkg *aPackage, files []*ast.File) (srcFiles []cgoSrcFile, preambles []cgoPreamble, cdecls []cgoDecl, err error) { + return parseCgoWithCommandEnv(commandEnv{}, buildCtx, pkg, files) +} + +func parseCgoWithCommandEnv(commands commandEnv, buildCtx *build.Context, pkg *aPackage, files []*ast.File) (srcFiles []cgoSrcFile, preambles []cgoPreamble, cdecls []cgoDecl, err error) { dirs := make(map[string]none) for _, file := range files { pos := pkg.Fset.Position(file.Name.NamePos) @@ -379,7 +384,7 @@ func parseCgo_(buildCtx *build.Context, pkg *aPackage, files []*ast.File) (srcFi spec := decl.Specs[0].(*ast.ImportSpec) if spec.Path.Value == "\"unsafe\"" { pos := pkg.Fset.Position(doc.Pos()) - preamble, flags, err := parseCgoPreamble(pos, doc.Text()) + preamble, flags, err := parseCgoPreambleWithCommandEnv(commands, pos, doc.Text()) if err != nil { panic(err) } @@ -395,6 +400,10 @@ func parseCgo_(buildCtx *build.Context, pkg *aPackage, files []*ast.File) (srcFi } func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { + return parseCgoPreambleWithCommandEnv(commandEnv{}, pos, text) +} + +func parseCgoPreambleWithCommandEnv(commands commandEnv, pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { b := strings.Builder{} fline := pos.Line fname := pos.Filename @@ -405,7 +414,7 @@ func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, de line = strings.TrimSpace(line) if strings.HasPrefix(line, "#cgo ") { var cgoDecls []cgoDecl - cgoDecls, err = parseCgoDecl(line) + cgoDecls, err = parseCgoDeclWithCommandEnv(commands, line) if err != nil { return } @@ -432,6 +441,10 @@ func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, de // #cgo CXXFLAGS: -I/usr/include/c++/v1 // #cgo LDFLAGS: -L/usr/lib/python3.12/config-3.12-x86_64-linux-gnu -lpython3.12 func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { + return parseCgoDeclWithCommandEnv(commandEnv{}, line) +} + +func parseCgoDeclWithCommandEnv(commands commandEnv, line string) (cgoDecls []cgoDecl, err error) { idx := strings.Index(line, ":") if idx == -1 { err = fmt.Errorf("invalid cgo format: %v", line) @@ -462,12 +475,16 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { switch flag { case "pkg-config": - ldflags, e := exec.Command("pkg-config", "--libs", arg).Output() + libsCmd := exec.Command("pkg-config", "--libs", arg) + commands.configure(libsCmd) + ldflags, e := libsCmd.Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return } - cflags, e := exec.Command("pkg-config", "--cflags", arg).Output() + flagsCmd := exec.Command("pkg-config", "--cflags", arg) + commands.configure(flagsCmd) + cflags, e := flagsCmd.Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return diff --git a/internal/build/cgo_test.go b/internal/build/cgo_test.go index 3f835ac8f6..91fba3f534 100644 --- a/internal/build/cgo_test.go +++ b/internal/build/cgo_test.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" "reflect" + "runtime" "strings" "testing" @@ -97,6 +98,117 @@ func TestCollectCgoSymbolsStripsPackagePrefix(t *testing.T) { } } +func TestGenExternDeclsUsesProcessLLVMPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a shell script") + } + + clang := filepath.Join(t.TempDir(), "clang") + script := `#!/bin/sh +for arg in "$@"; do + if [ "$arg" = "-dM" ]; then + printf '#define request_macro 1\n' + exit 0 + fi +done +printf '%s\n' '{"kind":"TranslationUnitDecl","inner":[{"kind":"FunctionDecl","name":"request_func"}]}' +` + if err := os.WriteFile(clang, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + + symbols := map[string]string{ + "cgo_func": "request_func", + "cgo_macro": "request_macro", + } + t.Setenv("PATH", filepath.Dir(clang)) + got, err := genExternDeclsByClang(commandEnv{}, nil, "", nil, symbols, true) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "typeof(request_func)* cgo_func;", + "typeof(request_macro) cgo_macro;", + } { + if !strings.Contains(got, want) { + t.Fatalf("generated declarations missing %q:\n%s", want, got) + } + } + if len(symbols) != 0 { + t.Fatalf("resolved symbols were not removed: %v", symbols) + } + + for _, tt := range []struct { + name string + script string + wantErr string + }{ + { + name: "function probe", + script: "#!/bin/sh\nexit 1\n", + wantErr: "failed to get func names", + }, + { + name: "macro probe", + script: `#!/bin/sh +for arg in "$@"; do + if [ "$arg" = "-dM" ]; then + exit 1 + fi +done +printf '%s\n' '{"kind":"TranslationUnitDecl"}' +`, + wantErr: "failed to get macro names", + }, + } { + t.Run(tt.name, func(t *testing.T) { + clang := filepath.Join(t.TempDir(), "clang") + if err := os.WriteFile(clang, []byte(tt.script), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", filepath.Dir(clang)) + if _, err := genExternDeclsByClang(commandEnv{}, nil, "", nil, nil, false); err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("genExternDeclsByClang() error = %v, want %q", err, tt.wantErr) + } + }) + } +} + +func TestBuildCgoReportsClangProbeFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a shell script") + } + + dir := t.TempDir() + clang := filepath.Join(dir, "clang") + if err := os.WriteFile(clang, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("PATH", dir) + + src := `package demo + +/* +int request_value; +*/ +import "unsafe" +` + goFile := filepath.Join(dir, "demo.go") + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, goFile, src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + ctx := &context{ + conf: &packages.Config{}, + buildConf: &Config{}, + } + pkg := &aPackage{Package: &packages.Package{Fset: fset}} + if _, _, err := buildCgo(ctx, pkg, []*ast.File{file}, nil, false); err == nil || !strings.Contains(err.Error(), "failed to generate extern decls") { + t.Fatalf("buildCgo() error = %v, want clang probe failure", err) + } +} + func TestParseCgoCollectsCXXFiles(t *testing.T) { dir := t.TempDir() src := `package demo diff --git a/internal/build/cmptest.go b/internal/build/cmptest.go index b4d2b115db..b60d5d853c 100644 --- a/internal/build/cmptest.go +++ b/internal/build/cmptest.go @@ -27,9 +27,9 @@ import ( "path/filepath" ) -func cmpTest(dir, pkgPath, llApp string, genExpect bool, runArgs []string) { +func cmpTest(commands commandEnv, dir, pkgPath, llApp string, genExpect bool, runArgs []string) { var llgoOut, llgoErr bytes.Buffer - var llgoRunErr = runApp(runArgs, dir, &llgoOut, &llgoErr, llApp) + var llgoRunErr = runApp(commands, runArgs, dir, &llgoOut, &llgoErr, llApp) llgoExpect := formatExpect(llgoOut.Bytes(), llgoErr.Bytes(), llgoRunErr) llgoExpectFile := filepath.Join(dir, "llgo.expect") @@ -50,7 +50,7 @@ func cmpTest(dir, pkgPath, llApp string, genExpect bool, runArgs []string) { } var goOut, goErr bytes.Buffer - var goRunErr = runApp(runArgs, dir, &goOut, &goErr, "go", "run", pkgPath) + var goRunErr = runApp(commands, runArgs, dir, &goOut, &goErr, "go", "run", pkgPath) checkEqual("output", llgoOut.Bytes(), goOut.Bytes()) checkEqual("stderr", llgoErr.Bytes(), goErr.Bytes()) @@ -91,7 +91,7 @@ func checkEqual(prompt string, a, expected []byte) { fatal(errors.New("checkEqual: unexpected " + prompt)) } -func runApp(runArgs []string, dir string, stdout, stderr io.Writer, app string, args ...string) error { +func runApp(commands commandEnv, runArgs []string, dir string, stdout, stderr io.Writer, app string, args ...string) error { if len(runArgs) > 0 { if len(args) > 0 { args = append(args, runArgs...) @@ -100,6 +100,7 @@ func runApp(runArgs []string, dir string, stdout, stderr io.Writer, app string, } } cmd := exec.Command(app, args...) + commands.configure(cmd) cmd.Dir = dir cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/internal/build/collect.go b/internal/build/collect.go index 11735574da..66da887c21 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -280,6 +280,7 @@ func detectLLVMVersion(ctx *context) string { cc = "clang" } versionCmd := exec.Command(cc, "--version") + ctx.commands.configure(versionCmd) output, err := versionCmd.Output() if err != nil { return "" diff --git a/internal/build/invocation.go b/internal/build/invocation.go new file mode 100644 index 0000000000..620f7a326a --- /dev/null +++ b/internal/build/invocation.go @@ -0,0 +1,69 @@ +package build + +import ( + "os/exec" + "path/filepath" + "slices" + "strings" +) + +// Invocation contains the explicit inputs used by one build. An empty Dir +// uses the working directory captured when Build starts. Build inherits one +// snapshot of the process environment; per-invocation overrides are not +// supported. +type Invocation struct { + Args []string + Config *Config + Dir string +} + +// commandEnv is the per-invocation process-execution state. It deliberately +// contains only the working directory and environment used by child processes. +type commandEnv struct { + dir string + environ []string +} + +func (e commandEnv) configure(cmd *exec.Cmd) *exec.Cmd { + cmd.Dir = e.dir + cmd.Env = slices.Clone(e.environ) + return cmd +} + +func resolveOutputs(dir string, out *OutFmtDetails) { + out.Out = resolvePath(dir, out.Out) + out.PCLN = resolvePath(dir, out.PCLN) + out.Bin = resolvePath(dir, out.Bin) + out.Hex = resolvePath(dir, out.Hex) + out.Img = resolvePath(dir, out.Img) + out.Uf2 = resolvePath(dir, out.Uf2) + out.Zip = resolvePath(dir, out.Zip) +} + +func resolvePath(dir, path string) string { + if path == "" || filepath.IsAbs(path) { + return path + } + return filepath.Join(dir, path) +} + +func withEnv(environ []string, values ...string) []string { + keys := make(map[string]struct{}, len(values)) + for _, value := range values { + if key, _, ok := strings.Cut(value, "="); ok { + keys[key] = struct{}{} + } + } + ret := make([]string, 0, len(environ)+len(values)) + for _, value := range environ { + key, _, ok := strings.Cut(value, "=") + // Drop malformed passthrough entries: exec.Cmd requires KEY=VALUE. + if _, replace := keys[key]; ok && replace { + continue + } + if ok { + ret = append(ret, value) + } + } + return append(ret, values...) +} diff --git a/internal/build/pcln_mode_test.go b/internal/build/pcln_mode_test.go index 0b14102d59..2f6f939906 100644 --- a/internal/build/pcln_mode_test.go +++ b/internal/build/pcln_mode_test.go @@ -188,13 +188,23 @@ func TestDoNormalizesLegacyPCLNMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { conf := tt.conf + before := conf // Stop after PCLN normalization without setting up a toolchain. conf.LinkOptions.DWARF = DWARFMode(255) + before.LinkOptions.DWARF = DWARFMode(255) if _, err := Do(nil, &conf); err == nil { t.Fatal("Do() succeeded with an invalid DWARF mode") } - if conf.PCLNMode != tt.want || !conf.PCLNModeSet { - t.Fatalf("normalized PCLN config = (%v, set=%v), want (%v, set=true)", conf.PCLNMode, conf.PCLNModeSet, tt.want) + if !reflect.DeepEqual(conf, before) { + t.Fatalf("Do() modified input config:\n got: %#v\nwant: %#v", conf, before) + } + conf.LinkOptions.DWARF = DWARFDefault + resolved, err := resolveBuildConfig(&conf) + if err != nil { + t.Fatal(err) + } + if resolved.PCLNMode != tt.want || !resolved.PCLNModeSet { + t.Fatalf("resolved PCLN config = (%v, set=%v), want (%v, set=true)", resolved.PCLNMode, resolved.PCLNModeSet, tt.want) } }) } diff --git a/internal/build/plan9asm.go b/internal/build/plan9asm.go index eacec24dc3..30077f5f72 100644 --- a/internal/build/plan9asm.go +++ b/internal/build/plan9asm.go @@ -408,16 +408,16 @@ func pkgSFiles(ctx *context, pkg *packages.Package) ([]string, error) { args = append(args, pkg.PkgPath) cmd := exec.Command("go", args...) + ctx.commands.configure(cmd) // Resolve dependencies from the module or workspace used by packages.Load. // A dependency directory in the module cache may not contain a go.mod. - if ctx.conf != nil { + if ctx.conf != nil && ctx.conf.Dir != "" { cmd.Dir = ctx.conf.Dir } - cmdEnv := os.Environ() if ctx.conf != nil && len(ctx.conf.Env) > 0 { - cmdEnv = append([]string(nil), ctx.conf.Env...) + cmd.Env = append([]string(nil), ctx.conf.Env...) } - cmd.Env = append(cmdEnv, + cmd.Env = withEnv(cmd.Env, "GOOS="+ctx.buildConf.Goos, "GOARCH="+ctx.buildConf.Goarch, ) diff --git a/internal/build/run.go b/internal/build/run.go index b5a99b31e5..e1f39b9527 100644 --- a/internal/build/run.go +++ b/internal/build/run.go @@ -60,6 +60,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(args, " ")) } cmd := exec.Command(app, args...) + ctx.commands.configure(cmd) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -75,6 +76,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(conf.RunArgs, " ")) } cmd := exec.Command(app, conf.RunArgs...) + ctx.commands.configure(cmd) cmd.Dir = pkgDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -92,12 +94,12 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod } } case ModeCmpTest: - cmpTest(pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs) + cmpTest(ctx.commands, pkgDir, pkgName, app, conf.GenExpect, conf.RunArgs) } return nil } -func runInEmulator(emulator string, envMap map[string]string, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error { +func runInEmulator(commands commandEnv, emulator string, envMap map[string]string, pkgDir, pkgName string, conf *Config, mode Mode, verbose bool) error { // Skip execution if CompileOnly is true if conf.CompileOnly { return nil @@ -112,18 +114,18 @@ func runInEmulator(emulator string, envMap map[string]string, pkgDir, pkgName st switch mode { case ModeRun: - return runEmuCmd(envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) + return runEmuCmd(commands, envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) case ModeTest: - return runEmuCmd(envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) + return runEmuCmd(commands, envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) case ModeCmpTest: - cmpTest(pkgDir, pkgName, envMap["out"], conf.GenExpect, conf.RunArgs) + cmpTest(commands, pkgDir, pkgName, envMap["out"], conf.GenExpect, conf.RunArgs) return nil } return nil } // runEmuCmd runs the application in emulator by formatting the emulator command template -func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []string, verbose bool, printCmds bool) error { +func runEmuCmd(commands commandEnv, envMap map[string]string, emulatorTemplate string, runArgs []string, verbose bool, printCmds bool) error { // Expand the emulator command template emulatorCmd := emulatorTemplate for placeholder, path := range envMap { @@ -157,6 +159,7 @@ func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []stri // Execute the emulator command cmd := exec.Command(cmdParts[0], cmdParts[1:]...) + commands.configure(cmd) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/build/run_test.go b/internal/build/run_test.go new file mode 100644 index 0000000000..43f565b6f7 --- /dev/null +++ b/internal/build/run_test.go @@ -0,0 +1,25 @@ +//go:build !llgo && !windows + +package build + +import ( + "os" + "strings" + "testing" +) + +func TestRunInEmulatorValidation(t *testing.T) { + commands := commandEnv{dir: t.TempDir(), environ: os.Environ()} + if err := runInEmulator(commands, "", nil, "", "", &Config{CompileOnly: true}, ModeRun, false); err != nil { + t.Fatalf("compile-only emulator run failed: %v", err) + } + if err := runInEmulator(commands, "", nil, "", "", &Config{Target: "demo"}, ModeRun, false); err == nil { + t.Fatal("missing emulator succeeded") + } + if err := runEmuCmd(commands, nil, "'", nil, false, false); err == nil || !strings.Contains(err.Error(), "parse") { + t.Fatalf("malformed emulator command error = %v", err) + } + if err := runEmuCmd(commands, nil, " ", nil, false, false); err == nil || !strings.Contains(err.Error(), "empty") { + t.Fatalf("empty emulator command error = %v", err) + } +} diff --git a/internal/clang/clang.go b/internal/clang/clang.go index 48ae7cb295..f272a1cc6f 100644 --- a/internal/clang/clang.go +++ b/internal/clang/clang.go @@ -51,6 +51,7 @@ func NewConfig(cc string, ccflags, cflags, ldflags []string, linker string) Conf type Cmd struct { app string config Config + Dir string Env []string Verbose bool Stdin io.Reader @@ -158,6 +159,7 @@ func (c *Cmd) mergeLinkerFlags() []string { // exec executes the clang command with given arguments. func (c *Cmd) exec(args ...string) error { cmd := exec.Command(c.app, args...) + cmd.Dir = c.Dir if c.Verbose { fmt.Fprintf(os.Stderr, "%v\n", cmd) } diff --git a/xtool/env/llvm/llvm.go b/xtool/env/llvm/llvm.go index a1284786d2..d488fb9ec3 100644 --- a/xtool/env/llvm/llvm.go +++ b/xtool/env/llvm/llvm.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "sort" "strings" @@ -87,6 +88,35 @@ func New(llvmConfigBin string) *Env { // means LLVM executables are assumed to be in PATH. func (e *Env) BinDir() string { return e.binDir } +// SetupPath makes the selected LLVM installation part of the process +// environment. Command entry points call it before starting builds; build +// requests and workers then inherit LLVM through the ordinary PATH snapshot. +func SetupPath() { + binDir := New("").BinDir() + if binDir == "" { + return + } + + path := os.Getenv("PATH") + for _, dir := range filepath.SplitList(path) { + if samePath(dir, binDir) { + return + } + } + if path != "" { + binDir += string(os.PathListSeparator) + path + } + _ = os.Setenv("PATH", binDir) +} + +func samePath(x, y string) bool { + x, y = filepath.Clean(x), filepath.Clean(y) + if runtime.GOOS == "windows" { + return strings.EqualFold(x, y) + } + return x == y +} + // Clang returns a new [clang.Cmd] instance. func (e *Env) Clang() *clang.Cmd { bin := filepath.Join(e.BinDir(), "clang++") diff --git a/xtool/env/llvm/llvm_test.go b/xtool/env/llvm/llvm_test.go new file mode 100644 index 0000000000..16a45005fe --- /dev/null +++ b/xtool/env/llvm/llvm_test.go @@ -0,0 +1,56 @@ +//go:build !llgo + +package llvm + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestSetupPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a shell script") + } + + binDir := t.TempDir() + llvmConfig := filepath.Join(t.TempDir(), "llvm-config") + if err := os.WriteFile(llvmConfig, []byte("#!/bin/sh\nprintf '%s\\n' \"${LLGO_TEST_LLVM_BINDIR}\"\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("LLVM_CONFIG", llvmConfig) + t.Setenv("LLGO_TEST_LLVM_BINDIR", binDir) + original := filepath.Join(t.TempDir(), "original") + t.Setenv("PATH", original) + + SetupPath() + want := binDir + string(os.PathListSeparator) + original + if got := os.Getenv("PATH"); got != want { + t.Fatalf("PATH = %q, want %q", got, want) + } + + SetupPath() + if got := os.Getenv("PATH"); got != want { + t.Fatalf("second setup changed PATH to %q, want %q", got, want) + } +} + +func TestSetupPathIgnoresMissingBinDir(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test helper uses a shell script") + } + + llvmConfig := filepath.Join(t.TempDir(), "llvm-config") + if err := os.WriteFile(llvmConfig, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { + t.Fatal(err) + } + t.Setenv("LLVM_CONFIG", llvmConfig) + t.Setenv("PATH", filepath.Join(t.TempDir(), "original")) + before := os.Getenv("PATH") + + SetupPath() + if got := os.Getenv("PATH"); got != before { + t.Fatalf("PATH changed from %q to %q without an LLVM bin directory", before, got) + } +}