From 036034a4d571f87886bc45b353dd3594ee62486d Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 00:38:14 +0800 Subject: [PATCH 1/9] build: isolate per-request process state --- cl/caller_frame_test.go | 12 +- cl/compile.go | 69 ++++- cl/import.go | 4 +- cl/instr.go | 12 +- internal/build/build.go | 312 ++++++++++++++++------- internal/build/build_test.go | 161 ++++++++++++ internal/build/cgo.go | 42 +-- internal/build/cmptest.go | 10 +- internal/build/collect.go | 11 +- internal/build/main_module.go | 2 +- internal/build/pcln_mode.go | 6 +- internal/build/pcln_mode_test.go | 13 +- internal/build/plan9asm.go | 32 ++- internal/build/request.go | 94 +++++++ internal/build/run.go | 22 +- internal/clang/clang.go | 30 ++- internal/clang/clang_test.go | 28 +- internal/crosscompile/compile/compile.go | 8 +- internal/crosscompile/crosscompile.go | 65 +++-- internal/crosscompile/fetch.go | 33 ++- internal/crosscompile/libc.go | 12 +- internal/env/env.go | 25 +- internal/processenv/processenv.go | 85 ++++++ internal/processenv/processenv_test.go | 41 +++ xtool/env/env.go | 29 ++- xtool/env/llvm/llvm.go | 73 ++++-- xtool/env/llvm/llvm_test.go | 62 +++++ 27 files changed, 1050 insertions(+), 243 deletions(-) create mode 100644 internal/build/request.go create mode 100644 internal/processenv/processenv.go create mode 100644 internal/processenv/processenv_test.go create mode 100644 xtool/env/llvm/llvm_test.go diff --git a/cl/caller_frame_test.go b/cl/caller_frame_test.go index 84b835adcc..ad47f5e71d 100644 --- a/cl/caller_frame_test.go +++ b/cl/caller_frame_test.go @@ -499,9 +499,7 @@ func TestRuntimeFrameNameNormalization(t *testing.T) { } func TestCompileRuntimeCallerFrameInstrumentation(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime/debug" @@ -744,9 +742,7 @@ func top() { } func TestCompileRuntimeCallerFrameUsesGoNameForLinkname(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "command-line-arguments", `package main import "runtime" @@ -825,9 +821,7 @@ func f() { _ = runtime.FuncForPC(0) } } func TestCompileRuntimeCallerLocationOnlyForRuntimePaths(t *testing.T) { - old := emitShadowStackInstrumentation - emitShadowStackInstrumentation = true - defer func() { emitShadowStackInstrumentation = old }() + t.Setenv("LLGO_SHADOW_STACK", "1") ssapkg, files := buildCallerFrameSSAPackage(t, "example.com/foo", `package foo import "runtime" diff --git a/cl/compile.go b/cl/compile.go index 8f99b9a0f5..a4f9cbe150 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -65,6 +65,27 @@ var ( enableExportRename bool ) +// Options contains frontend behavior for one package compilation. Drivers that +// may host multiple builds in one process should pass Options explicitly +// instead of changing the legacy package-level Enable* settings. +type Options struct { + Debug bool + DebugSymbols bool + Trace bool + ExportRename bool + ShadowStack bool +} + +func legacyOptions() Options { + return Options{ + Debug: enableDbg, + DebugSymbols: enableDbgSyms, + Trace: enableCallTracing, + ExportRename: enableExportRename, + ShadowStack: os.Getenv("LLGO_SHADOW_STACK") == "1", + } +} + // SetDebug sets debug flags. func SetDebug(dbgFlags dbgFlags) { debugInstr = (dbgFlags & DbgFlagInstruction) != 0 @@ -115,20 +136,27 @@ func dbgGoSSAln(args ...any) { } } +// EnableDebug changes the legacy process-wide default. +// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. func EnableDebug(b bool) { enableDbg = b } +// EnableDbgSyms changes the legacy process-wide default. +// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. func EnableDbgSyms(b bool) { enableDbgSyms = b } +// EnableTrace changes the legacy process-wide default. +// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. func EnableTrace(b bool) { enableCallTracing = b } // EnableExportRename enables or disables //export with different C symbol names. // This is enabled when using -target flag for TinyGo compatibility. +// Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. func EnableExportRename(b bool) { enableExportRename = b } @@ -180,6 +208,8 @@ type context struct { debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool pcLineSeq uint64 + options Options + optionsSet bool patches Patches blkInfos []blocks.Info @@ -213,6 +243,13 @@ type context struct { locality localityLowering } +func (p *context) frontendOptions() Options { + if p != nil && p.optionsSet { + return p.options + } + return legacyOptions() +} + func (p *context) rewriteValue(name string) (string, bool) { if p.rewrites == nil { return "", false @@ -601,8 +638,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun if f.Recover != nil { // set recover block fn.SetRecover(fn.Block(f.Recover.Index)) } - dbgEnabled := enableDbg - dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) + dbgEnabled := p.frontendOptions().Debug + dbgSymsEnabled := p.frontendOptions().DebugSymbols && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark oldLocalityFunction := p.locality.function @@ -857,11 +894,11 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) } - if block.Index == 0 && enableCallTracing && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { + if block.Index == 0 && p.frontendOptions().Trace && !strings.HasPrefix(fn.Name(), "github.com/goplus/llgo/runtime/internal/runtime.Print") { b.Printf("call " + fn.Name() + "\n\x00") } // place here to avoid wrong current-block - if enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 { + if p.frontendOptions().DebugSymbols && block.Parent().Origin() == nil && block.Index == 0 { p.debugParams(b, block.Parent()) } @@ -1647,7 +1684,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if _, ok := p.staticInitInstrs[instr]; ok { return } - if enableDbg && instr.Parent().Origin() == nil { + if p.frontendOptions().Debug && instr.Parent().Origin() == nil { if _, isDebugRef := instr.(*ssa.DebugRef); !isDebugRef { scope := p.getDebugLocScope(instr.Parent(), instr.Pos()) if scope != nil { @@ -1757,7 +1794,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.recordPanicLocation(b, v.Pos()) b.Send(ch, x) case *ssa.DebugRef: - if enableDbgSyms && v.Parent().Origin() == nil { + if p.frontendOptions().DebugSymbols && v.Parent().Origin() == nil { p.debugRef(b, v) } default: @@ -1820,7 +1857,7 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if isCgoVar(varName) { p.cgoSymbols = append(p.cgoSymbols, val.Name()) } - if enableDbgSyms && p.localityAllowsGlobalDebug(v) { + if p.frontendOptions().DebugSymbols && p.localityAllowsGlobalDebug(v) { pos := p.fset.Position(v.Pos()) b.DIGlobal(val, v.Name(), pos) } @@ -2055,7 +2092,7 @@ func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret ll // The rewrites map uses short variable names (without package qualifier) and // only affects string-typed globals defined in the current package. func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, false) + return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, false, legacyOptions()) } // NewPackageExWithEmbed compiles a package using pre-loaded go:embed metadata. @@ -2066,14 +2103,20 @@ func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin // of one compilation (like patches). nil means one-shot: a fresh // instance is created for this call. func NewPackageExWithEmbed(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, false) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, false, legacyOptions()) } func NewPackageExWithEmbedMeta(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap, metaCollect bool) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, legacyOptions()) +} + +// NewPackageExWithEmbedMetaOptions is NewPackageExWithEmbedMeta with explicit +// per-package frontend options. +func NewPackageExWithEmbedMetaOptions(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap, metaCollect bool, options Options) (ret llssa.Package, externs []string, err error) { + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, metaCollect, options) } -func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap *goembed.VarMap, metaCollect bool) (ret llssa.Package, externs []string, err error) { +func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap *goembed.VarMap, metaCollect bool, options Options) (ret llssa.Package, externs []string, err error) { pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes @@ -2097,7 +2140,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri prog.SetRuntime(pkgTypes) } ret = prog.NewPackageEx(pkgName, pkgPath, metaCollect) - if enableDbg { + if options.Debug { ret.InitDebug(pkgName, pkgPath, pkgProg.Fset) defer ret.FinalizeDebug() } @@ -2113,6 +2156,8 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri goTyps: pkgTypes, goPkg: pkg, patches: patches, + options: options, + optionsSet: true, skips: make(map[string]none), vargs: make(map[*ssa.Alloc][]llssa.Expr), funcs: make(map[*ssa.Function]llssa.Function), diff --git a/cl/import.go b/cl/import.go index 5b18496eac..907cf4d881 100644 --- a/cl/import.go +++ b/cl/import.go @@ -296,7 +296,7 @@ func (p *context) processLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgNam for n := len(doc.List) - 1; n >= 0; n-- { line := doc.List[n].Text ret := p.initLinkname(line, allowExport, func(name string, isExport bool) (_ string, _, ok bool) { - return fullName, isVar, name == inPkgName || (isExport && enableExportRename) + return fullName, isVar, name == inPkgName || (isExport && p.frontendOptions().ExportRename) }) if ret != unknownDirective { return ret == hasLinkname @@ -370,7 +370,7 @@ func (p *context) initLink(line string, prefix int, export bool, f func(inPkgNam } } else { // Export with different names already processed by initLinknameByDoc - if export && enableExportRename { + if export && p.frontendOptions().ExportRename { return } if export { diff --git a/cl/instr.go b/cl/instr.go index 678c4afc6d..7e091eee82 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1481,16 +1481,8 @@ func (p *context) runtimeCallerFrameName() string { return "" } -// emitShadowStackInstrumentation gates the legacy shadow-stack calls -// (PushCallerLocationFrame / RecordCallerLocation / RecordPanicLocation). -// The FP-chain unwinder supersedes them: physical pcs resolve through the -// prebuilt ftab and pcline labels, so tracked functions keep only noinline, -// no-tail-call and the label records. The emitters stay for one release as -// an escape hatch (LLGO_SHADOW_STACK=1). -var emitShadowStackInstrumentation = os.Getenv("LLGO_SHADOW_STACK") == "1" - func (p *context) pushCallerLocationFrame(b llssa.Builder, fn *ssa.Function) { - if !emitShadowStackInstrumentation { + if !p.frontendOptions().ShadowStack { return } if fn == nil { @@ -1516,7 +1508,7 @@ func (p *context) recordPanicLocation(b llssa.Builder, pos token.Pos) { } func (p *context) recordRuntimeLocation(b llssa.Builder, pos token.Pos, fn string) { - if !emitShadowStackInstrumentation || !p.shouldTrackCallerFrames() { + if !p.frontendOptions().ShadowStack || !p.shouldTrackCallerFrames() { return } position := p.fset.Position(pos) diff --git a/internal/build/build.go b/internal/build/build.go index c2f80c4136..d46805cf36 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -180,8 +180,8 @@ type Config struct { OmitDWARFByDefault bool PCLNMode PCLNMode // PCLNModeSet marks PCLNMode as authoritative. Command flags set it for - // explicit requests; Do sets it after resolving the legacy environment - // default. + // explicit requests; the build-local resolved configuration sets it after + // resolving the legacy environment default. PCLNModeSet bool AllowNoBody bool // allow declarations without bodies, as go tool compile does // DisableBoundsChecks disables index, slice, and slice-to-array conversion @@ -209,10 +209,90 @@ type Config struct { GlobalRewrites map[string]Rewrites ModuleHook ModuleHook Overlay map[string][]byte + + environment []string + llgoRoot string } 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.environment = slices.Clone(c.environment) + 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, environ ...[]string) (*Config, error) { + if input == nil { + return nil, errors.New("build config must not be nil") + } + conf := input.clone() + if len(environ) != 0 { + conf.environment = slices.Clone(environ[0]) + } + conf.llgoRoot = env.LLGoROOTWithEnv(conf.environment) + 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 +302,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 @@ -282,6 +359,13 @@ func (c *Config) packageMetaEnabled() bool { return c.CollectPackageMeta || c.deadcodeDropEnabled() } +func (c *Config) llgoRuntimeDir() string { + if c == nil || c.llgoRoot == "" { + return "" + } + return filepath.Join(c.llgoRoot, env.LLGoRuntimePkgName) +} + // ----------------------------------------------------------------------------- const ( @@ -291,43 +375,27 @@ const ( loadSyntax = loadTypes | packages.NeedSyntax | packages.NeedTypesInfo ) +var llssaInitOnce sync.Once + 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 Build(BuildRequest{Args: args, Config: conf}) +} + +// Build executes one build from an explicit request. Process inputs omitted by +// command-line callers are snapshotted once before any package or toolchain +// work begins. +func Build(req BuildRequest) ([]Package, error) { + snapshot, err := snapshotProcess(req) + if err != nil { return nil, err } - if err := conf.LinkOptions.validate(); err != nil { + conf, err := resolveBuildConfig(req.Config, snapshot.Env) + 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()) + export, err := crosscompile.UseWithEnv(conf.Goos, conf.Goarch, conf.Target, isEnvOnConfig(conf, llgoWasiThreads, false), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), snapshot.Env, snapshot.Dir, conf.llgoRoot) if err != nil { return nil, fmt.Errorf("failed to setup crosscompile: %w", err) } @@ -342,13 +410,8 @@ func Do(args []string, conf *Config) ([]Package, error) { if err := validateLinkOptions(conf, &export); err != nil { return nil, err } - // Enable different export names for TinyGo compatibility when using -target - if conf.Target != "" { - cl.EnableExportRename(true) - } - verbose := conf.Verbose - patterns := args + patterns := slices.Clone(req.Args) tags := defaultBuildTags(conf.Goarch, conf.Target) if conf.PCLNMode == PCLNExternal { // Select the optional runtime loader as part of the normal package @@ -370,9 +433,10 @@ func Do(args []string, conf *Config) ([]Package, error) { cfg := &packages.Config{ Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile, BuildFlags: goBuildFlags, + Dir: snapshot.Dir, Fset: token.NewFileSet(), Tests: conf.Mode == ModeTest, - Env: append(slices.Clone(os.Environ()), "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), + Env: withEnv(snapshot.Env, "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), } if conf.Mode == ModeTest { cfg.Mode |= packages.NeedForTest @@ -380,10 +444,16 @@ func Do(args []string, conf *Config) ([]Package, error) { abi.SetRewriteMainPrefix(conf.RewriteMainPrefix) emitDebugInfo := shouldEmitDebugInfo(conf, &export) - cl.EnableDebug(emitDebugInfo) - cl.EnableDbgSyms(emitDebugInfo) - cl.EnableTrace(IsTraceEnabled()) - llssa.Initialize(llssa.InitAll) + frontendOptions := cl.Options{ + Debug: emitDebugInfo, + DebugSymbols: emitDebugInfo, + Trace: isEnvOnConfig(conf, llgoTrace, false), + ExportRename: conf.Target != "", + ShadowStack: isEnvOnConfig(conf, llgoShadowStack, false), + } + llssaInitOnce.Do(func() { + llssa.Initialize(llssa.InitAll) + }) target := &llssa.Target{ GOOS: conf.Goos, @@ -455,7 +525,7 @@ func Do(args []string, conf *Config) ([]Package, error) { return nil, err } var llgoFiles map[string][]string - conf.Overlay, llgoFiles, err = buildSourcePatchOverlayForGOROOT(conf.Overlay, env.LLGoRuntimeDir(), sourcePatchGOROOT, sourcePatchBuildContext{ + conf.Overlay, llgoFiles, err = buildSourcePatchOverlayForGOROOT(conf.Overlay, conf.llgoRuntimeDir(), sourcePatchGOROOT, sourcePatchBuildContext{ goos: conf.Goos, goarch: conf.Goarch, goversion: sourcePatchGoVersion, @@ -510,7 +580,7 @@ func Do(args []string, conf *Config) ([]Package, error) { altPkgPaths := altPkgs(initial, conf, llssa.PkgRuntime) altCfg := *cfg - altCfg.Dir = env.LLGoRuntimeDir() + altCfg.Dir = conf.llgoRuntimeDir() altPkgs, err := packages.LoadEx(dedup, sizes, &altCfg, altPkgPaths...) if err != nil { return nil, err @@ -539,28 +609,29 @@ func Do(args []string, conf *Config) ([]Package, error) { buildMode |= ssa.GlobalDebug cabiOptimize = false } - if !IsOptimizeEnabled() { + if !isEnvOnConfig(conf, llgoOptimize, true) { buildMode |= ssa.NaiveForm } progSSA := ssa.NewProgram(initial[0].Fset, buildMode) 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 + env := llvm.NewWithEnv("", snapshot.Env, snapshot.Dir) output := conf.OutFile != "" ctx := &context{env: env, 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), - pkgs: map[*packages.Package]Package{}, - pkgByID: map[string]Package{}, - output: output, - passOpt: passOpt, - buildConf: conf, - crossCompile: export, - cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), + fingerprinting: make(map[string]bool), + pkgs: map[*packages.Package]Package{}, + pkgByID: map[string]Package{}, + output: output, + passOpt: passOpt, + buildConf: conf, + crossCompile: export, + process: snapshot, + frontendOptions: frontendOptions, + cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), } defer ctx.closePackageMetas() @@ -601,6 +672,7 @@ func Do(args []string, conf *Config) ([]Package, error) { if err != nil { return nil, err } + ctx.process.resolveOutputs(outFmts) // Link main package using the output path from buildOutFmts err = linkMainPkg(ctx, pkg, allPkgs, outFmts.Out, verbose) @@ -656,7 +728,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, 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 { @@ -815,8 +887,10 @@ type context struct { output bool passOpt bool - buildConf *Config - crossCompile crosscompile.Export + buildConf *Config + crossCompile crosscompile.Export + process processSnapshot + frontendOptions cl.Options cTransformer *cabi.Transformer @@ -839,6 +913,11 @@ type context struct { pclnExternal *pclnmap.Data } +// frontendDebugMu protects the legacy cl/ssa instruction-debug switches. +// Code-generation options are request-local; this process-wide lock remains +// only for verbose diagnostic logging until those helpers accept a logger. +var frontendDebugMu sync.RWMutex + // closePackageMetas releases metadata mappings owned by this build. Metadata // remains available to hooks and whole-program consumers until Do returns. func (c *context) closePackageMetas() { @@ -860,6 +939,8 @@ func (c *context) compiler() *clang.Cmd { c.crossCompile.Linker, ) cmd := clang.NewCompiler(config) + cmd.Dir = c.process.Dir + cmd.Env = slices.Clone(c.process.Env) cmd.Verbose = c.shouldPrintCommands(false) return cmd } @@ -873,6 +954,8 @@ func (c *context) linker() *clang.Cmd { c.crossCompile.Linker, ) cmd := clang.NewLinker(config) + cmd.Dir = c.process.Dir + cmd.Env = slices.Clone(c.process.Env) cmd.Verbose = c.shouldPrintCommands(false) return cmd } @@ -1028,7 +1111,7 @@ func appendExternalLinkArgs(ctx *context, aPkg *aPackage, spec string) { for _, alt := range altParts { alt = strings.TrimSpace(alt) if strings.ContainsRune(alt, '$') { - expdArgs = append(expdArgs, xenv.ExpandEnvToArgs(alt)...) + expdArgs = append(expdArgs, xenv.ExpandEnvToArgsWithEnv(alt, ctx.process.Env, ctx.process.Dir)...) atomic.AddInt32(&ctx.nLibdir, 1) } else { fields := strings.Fields(alt) @@ -1129,7 +1212,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) { printCmds := ctx.shouldPrintCommands(verbose) var objFiles []string - llgoRoot := env.LLGoROOT() + llgoRoot := ctx.buildConf.llgoRoot for _, extraFile := range ctx.crossCompile.ExtraFiles { // Resolve the file path relative to llgo root @@ -1205,7 +1288,7 @@ func rewritePrebuiltFuncTab(ctx *context, out string, verbose bool) { if ctx.buildConf.BuildMode != BuildModeExe { return } - if os.Getenv("LLGO_PCLNPOST") == "0" { // escape hatch: keep first-use construction + if envConfigValue(ctx.buildConf, "LLGO_PCLNPOST") == "0" { // escape hatch: keep first-use construction return } st, err := pclnpost.Rewrite(out) @@ -1341,7 +1424,7 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa linkInputs = append(linkInputs, extraObjFiles...) linkInputs = append(linkInputs, archiveInputs...) - if IsFullRpathEnabled() { + if isEnvOnConfig(ctx.buildConf, llgoFullRpath, false) { // Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well. // This is to ensure the final executable can locate libraries with a relocatable install_name // (e.g., "@rpath/libfoo.dylib") at runtime. @@ -1394,6 +1477,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) @@ -1531,7 +1619,7 @@ func (c *context) archiver() string { } } // Allow user override - if ar := os.Getenv("LLGO_AR"); ar != "" { + if ar := envConfigValue(c.buildConf, "LLGO_AR"); ar != "" { return ar } if c.buildConf.ltoEnabled() || c.buildConf.Goarch == "wasm" || strings.Contains(c.crossCompile.LLVMTarget, "wasm") { @@ -1546,7 +1634,7 @@ func (c *context) archiver() string { // flatten package archives into the final c-archive instead of nesting .a // files as members. LLVM is already a required LLGo toolchain dependency. func (c *context) archiveMerger() (string, error) { - if ar := os.Getenv("LLGO_AR"); ar != "" { + if ar := envConfigValue(c.buildConf, "LLGO_AR"); ar != "" { return ar, nil } if c.crossCompile.CC != "" { @@ -1597,7 +1685,7 @@ func (c *context) createMergedArchiveFile(archivePath string, inputs []string, v if err != nil { return err } - cmd := exec.Command(arCmd, "-M") + cmd := c.process.command(arCmd, "-M") cmd.Stdin = strings.NewReader(script.String()) printCmds := c.shouldPrintCommands(len(verbose) > 0 && verbose[0]) if printCmds { @@ -1636,7 +1724,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.process.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, " ")) @@ -1691,22 +1779,27 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error { syntax = append(syntax, altPkg.Syntax...) } showDetail := verbose && pkgExists(ctx.initial, pkg) - if showDetail { - llssa.SetDebug(llssa.DbgFlagAll) - cl.SetDebug(cl.DbgFlagAll) - defer func() { - llssa.SetDebug(0) - cl.SetDebug(0) - }() - } - - embedMap, err := goembed.LoadDirectives(ctx.conf.Fset, syntax) - if err != nil { - return fmt.Errorf("load go:embed directives for %s failed: %w", pkgPath, err) - } - needMeta := !aPkg.CacheHit && ctx.buildConf.packageMetaEnabled() - ret, externs, err := cl.NewPackageExWithEmbedMeta(ctx.prog, ctx.callerTracking, ctx.patches, aPkg.rewriteVars, aPkg.SSA, syntax, embedMap, needMeta) + ret, externs, err := func() (llssa.Package, []string, error) { + if showDetail { + frontendDebugMu.Lock() + defer frontendDebugMu.Unlock() + llssa.SetDebug(llssa.DbgFlagAll) + cl.SetDebug(cl.DbgFlagAll) + defer func() { + llssa.SetDebug(0) + cl.SetDebug(0) + }() + } else { + frontendDebugMu.RLock() + defer frontendDebugMu.RUnlock() + } + embedMap, err := goembed.LoadDirectives(ctx.conf.Fset, syntax) + if err != nil { + return nil, nil, fmt.Errorf("load go:embed directives for %s failed: %w", pkgPath, err) + } + return cl.NewPackageExWithEmbedMetaOptions(ctx.prog, ctx.callerTracking, ctx.patches, aPkg.rewriteVars, aPkg.SSA, syntax, embedMap, needMeta, ctx.frontendOptions) + }() check(err) aPkg.LPkg = ret @@ -1843,7 +1936,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, f.Name()); err != nil { fmt.Fprintf(os.Stderr, "==> llc %v: %v\n%v\n", pkgPath, f.Name(), msg) } } @@ -1927,7 +2020,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, f.Name()); err != nil { fmt.Fprintf(os.Stderr, "==> llc %v: %v\n%v\n", pkgPath, f.Name(), msg) } } @@ -1955,9 +2048,9 @@ 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(ctx *context, exportFile string) (msg string, err error) { + bin := filepath.Join(ctx.env.BinDir(), "llc") + cmd := ctx.process.command(bin, "-filetype=null", exportFile) var buf bytes.Buffer cmd.Stderr = &buf if err = cmd.Run(); err != nil { @@ -2286,6 +2379,7 @@ const llgoWasiThreads = "LLGO_WASI_THREADS" const llgoStdioNobuf = "LLGO_STDIO_NOBUF" const llgoFullRpath = "LLGO_FULL_RPATH" const llgoBuildCache = "LLGO_BUILD_CACHE" +const llgoShadowStack = "LLGO_SHADOW_STACK" // for Plan9 asm translation debug const llgoPlan9ASMPkgs = "LLGO_PLAN9ASM_PKGS" @@ -2310,10 +2404,37 @@ func isEnvOn(env string, defVal bool) bool { // cacheEnabled checks if build cache is enabled. // Cache can be disabled by setting LLGO_BUILD_CACHE=off|0 -func cacheEnabled() bool { +func cacheEnabled(conf ...*Config) bool { + if len(conf) != 0 { + return isEnvOnConfig(conf[0], llgoBuildCache, true) + } return isEnvOn(llgoBuildCache, true) } +func isEnvOnConfig(conf *Config, key string, defVal bool) bool { + if conf == nil || conf.environment == nil { + return isEnvOn(key, defVal) + } + value, ok := envValue(conf.environment, key) + if !ok || value == "" { + return defVal + } + switch strings.ToLower(value) { + case "1", "true", "on": + return true + default: + return false + } +} + +func envConfigValue(conf *Config, key string) string { + if conf == nil || conf.environment == nil { + return os.Getenv(key) + } + value, _ := envValue(conf.environment, key) + return value +} + func IsTraceEnabled() bool { return isEnvOn(llgoTrace, false) } @@ -2375,6 +2496,13 @@ func WasmRuntime() string { return defaultEnv(llgoWasmRuntime, defaultWasmRuntime) } +func WasmRuntimeForConfig(conf *Config) string { + if value := envConfigValue(conf, llgoWasmRuntime); value != "" { + return value + } + return defaultWasmRuntime +} + func concatPkgLinkFiles(ctx *context, pkg *packages.Package, verbose bool) (parts []string) { llgoPkgLinkFiles(ctx, pkg, func(linkFile string) { parts = append(parts, linkFile) @@ -2400,7 +2528,7 @@ func clFiles(ctx *context, files string, pkg *packages.Package, procFile func(li args := make([]string, 0, 16) if strings.HasPrefix(files, "$") { // has cflags if pos := strings.IndexByte(files, ':'); pos > 0 { - cflags := xenv.ExpandEnvToArgs(files[:pos]) + cflags := xenv.ExpandEnvToArgsWithEnv(files[:pos], ctx.process.Env, ctx.process.Dir) files = files[pos+1:] args = append(args, cflags...) } diff --git a/internal/build/build_test.go b/internal/build/build_test.go index cf3054c1e9..6df41bd01f 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,90 @@ func TestMain(m *testing.M) { os.Exit(code) } +func TestResolveBuildConfigDoesNotAliasInput(t *testing.T) { + input := &Config{ + RunArgs: []string{"run"}, + GoBuildFlags: []string{"-tags=custom"}, + GlobalRewrites: map[string]Rewrites{ + "example.com/p": {"value": "input"}, + "nil": nil, + }, + } + resolved, err := resolveBuildConfig(input) + if err != nil { + t.Fatal(err) + } + + resolved.RunArgs[0] = "changed" + resolved.GoBuildFlags[0] = "-tags=changed" + resolved.GlobalRewrites["example.com/p"]["value"] = "changed" + resolved.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 := input.GlobalRewrites["example.com/p"]["value"]; got != "input" { + t.Fatalf("input rewrite changed to %q", got) + } + if _, ok := input.GlobalRewrites["new"]; ok { + t.Fatal("resolved rewrite map aliases input map") + } + if rewrites, ok := resolved.GlobalRewrites["nil"]; !ok || rewrites != nil { + t.Fatalf("nil rewrite entry was not preserved: %#v", rewrites) + } +} + +func TestResolveBuildConfigUsesExplicitEnvironment(t *testing.T) { + t.Setenv(llgoFuncInfo, "0") + t.Setenv(llgoTrace, "0") + + resolved, err := resolveBuildConfig(&Config{}, []string{ + llgoFuncInfo + "=1", + llgoTrace + "=1", + }) + if err != nil { + t.Fatal(err) + } + if resolved.PCLNMode != PCLNEmbedded { + t.Fatalf("PCLNMode = %v, want explicit request environment to enable metadata", resolved.PCLNMode) + } + if !isEnvOnConfig(resolved, llgoTrace, false) { + t.Fatal("trace did not use explicit request environment") + } +} + +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) + } +} + func TestClosePackageMetas(t *testing.T) { b := meta.NewBuilder() b.Sym("pkg.main") @@ -1092,10 +1177,18 @@ func F() {} } conf := NewDefaultConf(ModeGen) conf.AllowNoBody = true + before := conf.clone() + pathBefore := os.Getenv("PATH") pkgs, err := Do([]string{file}, conf) if err != nil { t.Fatal(err) } + if !reflect.DeepEqual(conf, before) { + t.Fatalf("successful Do() modified input config:\n got: %#v\nwant: %#v", conf, before) + } + if pathAfter := os.Getenv("PATH"); pathAfter != pathBefore { + t.Fatalf("successful Do() modified PATH:\n got: %q\nwant: %q", pathAfter, pathBefore) + } if len(pkgs) != 1 || pkgs[0].LPkg == nil { t.Fatalf("Do returned packages = %+v, want one compiled package", pkgs) } @@ -1166,6 +1259,74 @@ func Invalid() {} } } +func TestConcurrentBuildRequestsKeepInputsIsolated(t *testing.T) { + file := filepath.Join(t.TempDir(), "concurrent.go") + if err := os.WriteFile(file, []byte("package concurrent\n\nfunc F() {}\n"), 0o644); err != nil { + t.Fatal(err) + } + conf := NewDefaultConf(ModeGen) + before := conf.clone() + pathBefore := os.Getenv("PATH") + baseEnv := withEnv(os.Environ(), llgoBuildCache+"=0") + + type result struct { + pkgs []Package + err error + } + results := make(chan result, 2) + for _, optimize := range []string{"0", "1"} { + environ := withEnv(baseEnv, llgoOptimize+"="+optimize) + go func() { + pkgs, err := Build(BuildRequest{ + Args: []string{file}, + Config: conf, + Env: environ, + }) + results <- result{pkgs: pkgs, err: err} + }() + } + for range 2 { + got := <-results + if got.err != nil { + t.Fatal(got.err) + } + if len(got.pkgs) != 1 || got.pkgs[0].LPkg == nil { + t.Fatalf("Build returned packages = %+v, want one compiled package", got.pkgs) + } + got.pkgs[0].LPkg.Prog.Dispose() + } + if !reflect.DeepEqual(conf, before) { + t.Fatalf("concurrent builds modified shared input:\n got: %#v\nwant: %#v", conf, before) + } + if pathAfter := os.Getenv("PATH"); pathAfter != pathBefore { + t.Fatalf("concurrent builds modified PATH:\n got: %q\nwant: %q", pathAfter, pathBefore) + } +} + +func TestBuildRequestUsesExplicitWorkingDirectory(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) + pkgs, err := Build(BuildRequest{ + Args: []string{"."}, + Config: conf, + Dir: dir, + Env: withEnv(os.Environ(), llgoBuildCache+"=0"), + }) + 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) + } + pkgs[0].LPkg.Prog.Dispose() +} + func TestFormatPackageError(t *testing.T) { tests := []struct { name string diff --git a/internal/build/cgo.go b/internal/build/cgo.go index ac354e4caf..67a3dfd318 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 := parseCgoWithProcess(ctx.process, &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.process, 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(process processSnapshot, 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(process, 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(process, 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(process processSnapshot, 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(process, 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(process processSnapshot, 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(process, verbose, "clang", args...) cmd.Stderr = os.Stderr output, err := cmd.Output() if err != nil { @@ -295,11 +295,11 @@ 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(process processSnapshot, 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...) + return process.command(name, arg...) } func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { @@ -311,6 +311,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 parseCgoWithProcess(processSnapshot{}, buildCtx, pkg, files) +} + +func parseCgoWithProcess(process processSnapshot, 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 +383,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 := parseCgoPreambleWithProcess(process, pos, doc.Text()) if err != nil { panic(err) } @@ -395,6 +399,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 parseCgoPreambleWithProcess(processSnapshot{}, pos, text) +} + +func parseCgoPreambleWithProcess(process processSnapshot, pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { b := strings.Builder{} fline := pos.Line fname := pos.Filename @@ -405,7 +413,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 = parseCgoDeclWithProcess(process, line) if err != nil { return } @@ -432,6 +440,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 parseCgoDeclWithProcess(processSnapshot{}, line) +} + +func parseCgoDeclWithProcess(process processSnapshot, line string) (cgoDecls []cgoDecl, err error) { idx := strings.Index(line, ":") if idx == -1 { err = fmt.Errorf("invalid cgo format: %v", line) @@ -462,12 +474,12 @@ func parseCgoDecl(line string) (cgoDecls []cgoDecl, err error) { switch flag { case "pkg-config": - ldflags, e := exec.Command("pkg-config", "--libs", arg).Output() + ldflags, e := process.command("pkg-config", "--libs", arg).Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return } - cflags, e := exec.Command("pkg-config", "--cflags", arg).Output() + cflags, e := process.command("pkg-config", "--cflags", arg).Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return diff --git a/internal/build/cmptest.go b/internal/build/cmptest.go index b4d2b115db..cf3cd1b64a 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(process processSnapshot, dir, pkgPath, llApp string, genExpect bool, runArgs []string) { var llgoOut, llgoErr bytes.Buffer - var llgoRunErr = runApp(runArgs, dir, &llgoOut, &llgoErr, llApp) + var llgoRunErr = runApp(process, 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(process, 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(process processSnapshot, 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...) @@ -99,7 +99,7 @@ func runApp(runArgs []string, dir string, stdout, stderr io.Writer, app string, args = runArgs } } - cmd := exec.Command(app, args...) + cmd := process.command(app, args...) cmd.Dir = dir cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/internal/build/collect.go b/internal/build/collect.go index 11735574da..88fa67ec7f 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -20,7 +20,6 @@ import ( "fmt" "io" "os" - "os/exec" "path/filepath" "runtime" "sort" @@ -90,14 +89,14 @@ func (c *context) collectEnvInputs(m *manifestBuilder) { llgoFullRpath, } for _, envVar := range envVars { - if v := os.Getenv(envVar); v != "" { + if v := envConfigValue(c.buildConf, envVar); v != "" { m.env.Vars = m.env.Vars.Add(envVar, v) } } if effectivePCLNMode(c.buildConf) != PCLNNone { // Record the effective value so equivalent spellings (unset, 1, // true, on) share a cache entry. - m.env.Vars = m.env.Vars.Add(llgoFuncInfoSites, strconv.FormatBool(IsFuncInfoSitesEnabled())) + m.env.Vars = m.env.Vars.Add(llgoFuncInfoSites, strconv.FormatBool(isEnvOnConfig(c.buildConf, llgoFuncInfoSites, true))) } } @@ -279,7 +278,7 @@ func detectLLVMVersion(ctx *context) string { if cc == "" { cc = "clang" } - versionCmd := exec.Command(cc, "--version") + versionCmd := ctx.process.command(cc, "--version") output, err := versionCmd.Output() if err != nil { return "" @@ -324,7 +323,7 @@ func (c *context) ensureCacheManager() *cacheManager { // tryLoadFromCache attempts to load a package from cache. // Returns true if cache hit, false otherwise. func (c *context) tryLoadFromCache(pkg *aPackage) bool { - if !cacheEnabled() { + if !cacheEnabled(c.buildConf) { return false } @@ -449,7 +448,7 @@ type cacheArchiveMetadata struct { // saveToCache saves a built package to cache. func (c *context) saveToCache(pkg *aPackage) error { - if !cacheEnabled() { + if !cacheEnabled(c.buildConf) { return nil } diff --git a/internal/build/main_module.go b/internal/build/main_module.go index cb0c2a418b..2698dd669e 100644 --- a/internal/build/main_module.go +++ b/internal/build/main_module.go @@ -259,7 +259,7 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa } b.Store(argcVar.Expr, fn.Param(0)) b.Store(argvVar.Expr, fn.Param(1)) - if IsStdioNobuf() { + if isEnvOnConfig(ctx.buildConf, llgoStdioNobuf, false) { emitStdioNobuf(b, pkg, ctx.buildConf.Goos) } if fns.pyInit != nil { diff --git a/internal/build/pcln_mode.go b/internal/build/pcln_mode.go index 56682058fe..ca80172d9e 100644 --- a/internal/build/pcln_mode.go +++ b/internal/build/pcln_mode.go @@ -63,7 +63,7 @@ func (m PCLNMode) validate() error { // effectivePCLNMode translates the legacy environment escape hatch once at // the configuration boundary. An explicit -pclntab value always wins. func effectivePCLNMode(conf *Config) PCLNMode { - if !conf.PCLNModeSet && conf.PCLNMode == PCLNEmbedded && !IsFuncInfoEnabled() { + if !conf.PCLNModeSet && conf.PCLNMode == PCLNEmbedded && !isEnvOnConfig(conf, llgoFuncInfo, true) { return PCLNNone } return conf.PCLNMode @@ -75,7 +75,7 @@ func effectivePCLNMode(conf *Config) PCLNMode { // reconstruct all Go entry PCs with dlsym: most Go symbols are intentionally // absent from .dynsym, so Linux keeps sites even when it emits DWARF. func shouldEnablePCLNSites(conf *Config, funcInfo, emitDebugInfo bool) bool { - if conf == nil || !funcInfo || !IsFuncInfoSitesEnabled() { + if conf == nil || !funcInfo || !isEnvOnConfig(conf, llgoFuncInfoSites, true) { return false } return !emitDebugInfo || conf.Goos == "linux" || conf.PCLNMode == PCLNExternal @@ -110,7 +110,7 @@ func validatePCLNMode(conf *Config) error { default: return fmt.Errorf("external PCLN metadata is not supported for GOARCH=%s", conf.Goarch) } - if !IsFuncInfoSitesEnabled() { + if !isEnvOnConfig(conf, llgoFuncInfoSites, true) { return fmt.Errorf("external PCLN metadata requires LLGO_FUNCINFO_SITES to be enabled") } return nil diff --git a/internal/build/pcln_mode_test.go b/internal/build/pcln_mode_test.go index 0b14102d59..77e650cc9d 100644 --- a/internal/build/pcln_mode_test.go +++ b/internal/build/pcln_mode_test.go @@ -190,11 +190,20 @@ func TestDoNormalizesLegacyPCLNMode(t *testing.T) { conf := tt.conf // Stop after PCLN normalization without setting up a toolchain. conf.LinkOptions.DWARF = DWARFMode(255) + before := conf 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: got %+v, want %+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..f0462239fb 100644 --- a/internal/build/plan9asm.go +++ b/internal/build/plan9asm.go @@ -307,7 +307,7 @@ func cabiSkipFuncsForPlan9Asm(ctx *context, pkgPath string, mod gllvm.Module) [] func (ctx *context) plan9asmEnabled(pkgPath string) bool { ctx.plan9asmOnce.Do(func() { - cfg := parsePlan9AsmPkgsEnv(Plan9ASMPkgs()) + cfg := parsePlan9AsmPkgsEnv(envConfigValue(ctx.buildConf, llgoPlan9ASMPkgs)) ctx.plan9asmMode = cfg.mode switch cfg.mode { case plan9asmEnvSelected: @@ -343,22 +343,30 @@ func hasAltPkgForTarget(conf *Config, pkgPath string) bool { } // When Plan9 asm translation is enabled, avoid also pulling in alt packages // that provide the same symbols as pure-Go fallbacks. - if plan9asmEnabledByDefault(conf, pkgPath) && !plan9asmDisabledByEnv() { + if plan9asmEnabledByDefault(conf, pkgPath) && !plan9asmDisabledByEnv(conf) { return false } // In ABI0/1, allow explicit env opt-in to prefer plan9asm over alt. - if conf != nil && conf.AbiMode != cabi.ModeAllFunc && plan9asmEnabledByEnv(pkgPath) { + if conf != nil && conf.AbiMode != cabi.ModeAllFunc && plan9asmEnabledByEnv(pkgPath, conf) { return false } return true } -func plan9asmDisabledByEnv() bool { - return parsePlan9AsmPkgsEnv(Plan9ASMPkgs()).mode == plan9asmEnvNone +func plan9asmDisabledByEnv(confs ...*Config) bool { + var conf *Config + if len(confs) != 0 { + conf = confs[0] + } + return parsePlan9AsmPkgsEnv(envConfigValue(conf, llgoPlan9ASMPkgs)).mode == plan9asmEnvNone } -func plan9asmEnabledByEnv(pkgPath string) bool { - cfg := parsePlan9AsmPkgsEnv(Plan9ASMPkgs()) +func plan9asmEnabledByEnv(pkgPath string, confs ...*Config) bool { + var conf *Config + if len(confs) != 0 { + conf = confs[0] + } + cfg := parsePlan9AsmPkgsEnv(envConfigValue(conf, llgoPlan9ASMPkgs)) if cfg.mode == plan9asmEnvAll { return true } @@ -407,17 +415,17 @@ func pkgSFiles(ctx *context, pkg *packages.Package) ([]string, error) { } args = append(args, pkg.PkgPath) - cmd := exec.Command("go", args...) + cmd := ctx.process.command("go", args...) // 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 { cmd.Dir = ctx.conf.Dir } - cmdEnv := os.Environ() - if ctx.conf != nil && len(ctx.conf.Env) > 0 { - cmdEnv = append([]string(nil), ctx.conf.Env...) + cmdEnv := ctx.process.Env + if ctx.conf != nil && ctx.conf.Env != nil { + cmdEnv = ctx.conf.Env } - cmd.Env = append(cmdEnv, + cmd.Env = withEnv(cmdEnv, "GOOS="+ctx.buildConf.Goos, "GOARCH="+ctx.buildConf.Goarch, ) diff --git a/internal/build/request.go b/internal/build/request.go new file mode 100644 index 0000000000..e0b5a4c010 --- /dev/null +++ b/internal/build/request.go @@ -0,0 +1,94 @@ +package build + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "github.com/goplus/llgo/internal/processenv" +) + +// BuildRequest contains the process-derived inputs used by one build. Empty +// Dir and a nil Env preserve the command-line behavior by snapshotting the +// current working directory and environment once, before the build starts. +type BuildRequest struct { + Args []string + Config *Config + Dir string + Env []string +} + +type processSnapshot struct { + Dir string + Env []string +} + +func (p processSnapshot) command(name string, args ...string) *exec.Cmd { + return processenv.Command(p.Env, p.Dir, name, args...) +} + +func (p processSnapshot) path(path string) string { + if path == "" || filepath.IsAbs(path) { + return path + } + return filepath.Join(p.Dir, path) +} + +func (p processSnapshot) resolveOutputs(out *OutFmtDetails) { + out.Out = p.path(out.Out) + out.PCLN = p.path(out.PCLN) + out.Bin = p.path(out.Bin) + out.Hex = p.path(out.Hex) + out.Img = p.path(out.Img) + out.Uf2 = p.path(out.Uf2) + out.Zip = p.path(out.Zip) +} + +func snapshotProcess(req BuildRequest) (processSnapshot, error) { + dir := req.Dir + if dir == "" { + var err error + dir, err = os.Getwd() + if err != nil { + return processSnapshot{}, fmt.Errorf("get working directory: %w", err) + } + } + env := slices.Clone(req.Env) + if req.Env == nil { + env = os.Environ() + } + return processSnapshot{Dir: dir, Env: env}, nil +} + +func envValue(environ []string, key string) (string, bool) { + prefix := key + "=" + for i := len(environ) - 1; i >= 0; i-- { + if strings.HasPrefix(environ[i], prefix) { + return strings.TrimPrefix(environ[i], prefix), true + } + } + return "", false +} + +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, "=") + if _, replace := keys[key]; ok && replace { + continue + } + if ok { + ret = append(ret, value) + } + } + return append(ret, values...) +} diff --git a/internal/build/run.go b/internal/build/run.go index b5a99b31e5..a9db1eadc5 100644 --- a/internal/build/run.go +++ b/internal/build/run.go @@ -36,7 +36,9 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod case ModeRun: args := make([]string, 0, len(conf.RunArgs)+1) if isWasmTarget(conf.Goos) { - wasmer := os.ExpandEnv(WasmRuntime()) + wasmer := os.Expand(WasmRuntimeForConfig(conf), func(key string) string { + return envConfigValue(conf, key) + }) wasmerArgs := strings.Split(wasmer, " ") wasmerCmd := wasmerArgs[0] wasmerArgs = wasmerArgs[1:] @@ -59,7 +61,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod if conf.PrintCommands { fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(args, " ")) } - cmd := exec.Command(app, args...) + cmd := ctx.process.command(app, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -74,7 +76,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod if conf.PrintCommands { fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(conf.RunArgs, " ")) } - cmd := exec.Command(app, conf.RunArgs...) + cmd := ctx.process.command(app, conf.RunArgs...) 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.process, 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(ctx *context, 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(ctx.process, envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) case ModeTest: - return runEmuCmd(envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) + return runEmuCmd(ctx.process, envMap, emulator, conf.RunArgs, verbose, conf.PrintCommands) case ModeCmpTest: - cmpTest(pkgDir, pkgName, envMap["out"], conf.GenExpect, conf.RunArgs) + cmpTest(ctx.process, 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(process processSnapshot, 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 { @@ -156,7 +158,7 @@ func runEmuCmd(envMap map[string]string, emulatorTemplate string, runArgs []stri } // Execute the emulator command - cmd := exec.Command(cmdParts[0], cmdParts[1:]...) + cmd := process.command(cmdParts[0], cmdParts[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/clang/clang.go b/internal/clang/clang.go index 48ae7cb295..0efb21328b 100644 --- a/internal/clang/clang.go +++ b/internal/clang/clang.go @@ -20,10 +20,10 @@ import ( "fmt" "io" "os" - "os/exec" "runtime" "strings" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/xtool/safesplit" ) @@ -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 @@ -66,6 +67,7 @@ func New(app string, config Config) *Cmd { return &Cmd{ app: app, config: config, + Dir: "", Env: nil, Verbose: false, Stdin: nil, @@ -117,12 +119,12 @@ func (c *Cmd) mergeCompilerFlags() []string { var flags []string // Add environment CCFLAGS - if envCCFlags := os.Getenv("CCFLAGS"); envCCFlags != "" { + if envCCFlags := c.getenv("CCFLAGS"); envCCFlags != "" { flags = append(flags, safesplit.SplitPkgConfigFlags(envCCFlags)...) } // Add environment CFLAGS - if envCFlags := os.Getenv("CFLAGS"); envCFlags != "" { + if envCFlags := c.getenv("CFLAGS"); envCFlags != "" { flags = append(flags, safesplit.SplitPkgConfigFlags(envCFlags)...) } @@ -140,12 +142,12 @@ func (c *Cmd) mergeLinkerFlags() []string { var flags []string // Add environment CCFLAGS (for linker) - if envCCFlags := os.Getenv("CCFLAGS"); envCCFlags != "" { + if envCCFlags := c.getenv("CCFLAGS"); envCCFlags != "" { flags = append(flags, safesplit.SplitPkgConfigFlags(envCCFlags)...) } // Add environment LDFLAGS - if envLDFlags := os.Getenv("LDFLAGS"); envLDFlags != "" { + if envLDFlags := c.getenv("LDFLAGS"); envLDFlags != "" { flags = append(flags, safesplit.SplitPkgConfigFlags(envLDFlags)...) } @@ -157,19 +159,29 @@ 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 := processenv.Command(c.Env, c.Dir, c.app, args...) if c.Verbose { fmt.Fprintf(os.Stderr, "%v\n", cmd) } cmd.Stdin = c.Stdin cmd.Stdout = c.Stdout cmd.Stderr = c.Stderr - if c.Env != nil { - cmd.Env = c.Env - } return cmd.Run() } +func (c *Cmd) getenv(key string) string { + if c.Env == nil { + return os.Getenv(key) + } + prefix := key + "=" + for i := len(c.Env) - 1; i >= 0; i-- { + if strings.HasPrefix(c.Env[i], prefix) { + return strings.TrimPrefix(c.Env[i], prefix) + } + } + return "" +} + // CheckLinkArgs validates linking arguments by attempting a test compile. func (c *Cmd) CheckLinkArgs(cmdArgs []string, wasm bool) error { // Create a temporary file with appropriate extension diff --git a/internal/clang/clang_test.go b/internal/clang/clang_test.go index 89dd3d7d90..87a55144c5 100644 --- a/internal/clang/clang_test.go +++ b/internal/clang/clang_test.go @@ -22,6 +22,7 @@ package clang import ( "bytes" "os" + "os/exec" "reflect" "strings" "testing" @@ -319,7 +320,11 @@ func TestVerboseMode(t *testing.T) { func TestCmdEnvironment(t *testing.T) { config := Config{} - cmd := New("echo", config) + echo, err := exec.LookPath("echo") + if err != nil { + t.Fatal(err) + } + cmd := New(echo, config) cmd.Env = []string{"TEST_VAR=test_value"} var stdout bytes.Buffer @@ -327,7 +332,7 @@ func TestCmdEnvironment(t *testing.T) { // Use a command that will show environment variables // Note: This is a simplified test that just ensures the Env field is set - err := cmd.Compile("-c", "test.c") + err = cmd.Compile("-c", "test.c") if err != nil { t.Errorf("Compile failed: %v", err) } @@ -337,6 +342,25 @@ func TestCmdEnvironment(t *testing.T) { } } +func TestCmdFlagsUseExplicitEnvironment(t *testing.T) { + t.Setenv("CCFLAGS", "-DPROCESS") + t.Setenv("CFLAGS", "-DPROCESS_C") + t.Setenv("LDFLAGS", "-lprocess") + + cmd := New("echo", Config{}) + cmd.Env = []string{ + "CCFLAGS=-DREQUEST", + "CFLAGS=-DREQUEST_C", + "LDFLAGS=-lrequest", + } + if got, want := cmd.mergeCompilerFlags(), []string{"-DREQUEST", "-DREQUEST_C"}; !reflect.DeepEqual(got, want) { + t.Fatalf("compiler flags = %v, want %v", got, want) + } + if got, want := cmd.mergeLinkerFlags(), []string{"-DREQUEST", "-lrequest"}; !reflect.DeepEqual(got, want) { + t.Fatalf("linker flags = %v, want %v", got, want) + } +} + func TestCmdIO(t *testing.T) { config := Config{} cmd := New("cat", config) // Use cat to test stdin/stdout diff --git a/internal/crosscompile/compile/compile.go b/internal/crosscompile/compile/compile.go index ded1153079..f204a9f655 100644 --- a/internal/crosscompile/compile/compile.go +++ b/internal/crosscompile/compile/compile.go @@ -3,17 +3,19 @@ package compile import ( "fmt" "os" - "os/exec" "path/filepath" "slices" "strings" "github.com/goplus/llgo/internal/clang" + "github.com/goplus/llgo/internal/processenv" ) type CompileOptions struct { CC string // Compiler to use Linker string + Env []string + Dir string CCFLAGS []string CFLAGS []string LDFLAGS []string @@ -60,6 +62,8 @@ func (g CompileGroup) Compile( compiler := clang.NewCompiler(cfg) compiler.Verbose = true + compiler.Env = slices.Clone(options.Env) + compiler.Dir = options.Dir archive := filepath.Join(outputDir, filepath.Base(g.OutputFileName)) fmt.Fprintf(os.Stderr, "Start to compile group %s to %s...\n", g.OutputFileName, archive) @@ -89,7 +93,7 @@ func (g CompileGroup) Compile( ccDir := filepath.Dir(options.CC) llvmAr := filepath.Join(ccDir, "llvm-ar") - cmd := exec.Command(llvmAr, args...) + cmd := processenv.Command(options.Env, options.Dir, llvmAr, args...) // TODO(MeteorsLiu): support verbose // cmd.Stdout = os.Stdout // cmd.Stderr = os.Stderr diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index e41c3e811f..91ee1172df 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -5,7 +5,6 @@ import ( "fmt" "io/fs" "os" - "os/exec" "path/filepath" "runtime" "strings" @@ -15,6 +14,7 @@ import ( "github.com/goplus/llgo/internal/flash" "github.com/goplus/llgo/internal/lto" "github.com/goplus/llgo/internal/optlevel" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/internal/targets" "github.com/goplus/llgo/internal/xtool/llvm" envllvm "github.com/goplus/llgo/xtool/env/llvm" @@ -47,6 +47,16 @@ type Export struct { Device flash.Device // Device configuration for flashing/debugging } +type processInputs struct { + environ []string + dir string + llgoRoot string +} + +func currentProcessInputs() processInputs { + return processInputs{llgoRoot: env.LLGoROOT()} +} + // DebugInfoPolicy describes how a selected linker handles debug information. // Build orchestration consumes this typed capability instead of inferring it // from a target name or linker executable. @@ -116,8 +126,8 @@ func getCanonicalArchName(triple string) string { } // getMacOSSysroot returns the macOS SDK path using xcrun -func getMacOSSysroot() (string, error) { - cmd := exec.Command("xcrun", "--sdk", "macosx", "--show-sdk-path") +func getMacOSSysroot(process processInputs) (string, error) { + cmd := processenv.Command(process.environ, process.dir, "xcrun", "--sdk", "macosx", "--show-sdk-path") output, err := cmd.Output() if err != nil { return "", err @@ -127,11 +137,9 @@ func getMacOSSysroot() (string, error) { // getESPClangRoot returns the ESP Clang root directory, checking LLGoROOT first, // then downloading if needed and platform is supported -func getESPClangRoot(forceEspClang bool) (clangRoot string, err error) { - llgoRoot := env.LLGoROOT() - +func getESPClangRoot(forceEspClang bool, process processInputs) (clangRoot string, err error) { // First check if clang exists in LLGoROOT - espClangRoot := filepath.Join(llgoRoot, envllvm.CrosscompileClangPath) + espClangRoot := filepath.Join(process.llgoRoot, envllvm.CrosscompileClangPath) if _, err = os.Stat(espClangRoot); err == nil { clangRoot = espClangRoot return @@ -150,7 +158,7 @@ func getESPClangRoot(forceEspClang bool) (clangRoot string, err error) { return } fmt.Fprintln(os.Stderr, "ESP Clang not found in LLGO_ROOT or cache, will download.") - if err = checkDownloadAndExtractESPClang(platformSuffix, cacheClangDir); err != nil { + if err = checkDownloadAndExtractESPClangWithProcess(platformSuffix, cacheClangDir, process); err != nil { return } } @@ -218,11 +226,15 @@ func compileWithConfig( } func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { + return useWithProcess(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, currentProcessInputs()) +} + +func useWithProcess(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, process processInputs) (export Export, err error) { targetTriple := llvm.GetTargetTriple(goos, goarch) - llgoRoot := env.LLGoROOT() + llgoRoot := process.llgoRoot // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(forceEspClang) + clangRoot, err := getESPClangRoot(forceEspClang, process) if err != nil { return } @@ -287,7 +299,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le // Add sysroot for macOS only if goos == "darwin" { - sysrootPath, sysrootErr := getMacOSSysroot() + sysrootPath, sysrootErr := getMacOSSysroot(process) if sysrootErr != nil { err = fmt.Errorf("failed to get macOS SDK path: %w", sysrootErr) return @@ -339,7 +351,7 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le // If not exists in LLGoROOT, download and use cached wasiSdkRoot if _, err = os.Stat(wasiSdkRoot); err != nil { sdkDir := filepath.Join(cacheDir(), llvm.GetTargetTriple(goos, goarch)) - if wasiSdkRoot, err = checkDownloadAndExtractWasiSDK(sdkDir); err != nil { + if wasiSdkRoot, err = checkDownloadAndExtractWasiSDKWithProcess(sdkDir, process); err != nil { return } } @@ -462,6 +474,10 @@ func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Le // UseTarget loads configuration from a target name (e.g., "rp2040", "wasi") func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (export Export, err error) { + return useTargetWithProcess(targetName, level, ltoMode, currentProcessInputs()) +} + +func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.Mode, process processInputs) (export Export, err error) { resolver := targets.NewDefaultResolver() config, err := resolver.Resolve(targetName) @@ -480,7 +496,7 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor } // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(true) + clangRoot, err := getESPClangRoot(true, process) if err != nil { return } @@ -522,7 +538,7 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor } // Build environment map for template variable expansion - envs := buildEnvMap(env.LLGoROOT()) + envs := buildEnvMap(process.llgoRoot) // Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS // ICF off for Go pc-identity semantics (see the non-cross flags above). @@ -648,7 +664,7 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor if config.LinkerScript != "" { ldflags = append(ldflags, "-T", config.LinkerScript) } - ldflags = append(ldflags, "-L", env.LLGoROOT()) // search targets/*.ld + ldflags = append(ldflags, "-L", process.llgoRoot) // search targets/*.ld var libcIncludeDir []string @@ -658,13 +674,15 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor var compileConfig compile.CompileConfig baseDir := filepath.Join(cacheRoot(), "crosscompile") - outputDir, compileConfig, err = getLibcCompileConfigByName(baseDir, config.Libc, config.LLVMTarget, config.CPU) + outputDir, compileConfig, err = getLibcCompileConfigByNameWithProcess(baseDir, config.Libc, config.LLVMTarget, config.CPU, process) if err != nil { return } libcLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{ CC: export.CC, Linker: export.Linker, + Env: process.environ, + Dir: process.dir, CCFLAGS: ccflags, LDFLAGS: ldflags, }) @@ -684,13 +702,15 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor var compileConfig compile.CompileConfig baseDir := filepath.Join(cacheRoot(), "crosscompile") - outputDir, compileConfig, err = getRTCompileConfigByName(baseDir, config.RTLib, config.LLVMTarget) + outputDir, compileConfig, err = getRTCompileConfigByNameWithProcess(baseDir, config.RTLib, config.LLVMTarget, process) if err != nil { return } rtLibLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{ CC: export.CC, Linker: export.Linker, + Env: process.environ, + Dir: process.dir, CCFLAGS: ccflags, LDFLAGS: ldflags, CFLAGS: libcIncludeDir, @@ -713,8 +733,15 @@ func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (expor // Use extends the original Use function to support target-based configuration // If targetName is provided, it takes precedence over goos/goarch func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { + return UseWithEnv(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, nil, "", env.LLGoROOT()) +} + +// UseWithEnv selects a toolchain using explicit process inputs. Download and +// cache operations remain protected by their existing cross-process locks. +func UseWithEnv(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, environ []string, dir, llgoRoot string) (export Export, err error) { + process := processInputs{environ: environ, dir: dir, llgoRoot: llgoRoot} if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") { - return UseTarget(targetName, level, ltoMode) + return useTargetWithProcess(targetName, level, ltoMode, process) } - return use(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE) + return useWithProcess(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, process) } diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go index d32003bacc..39ccd5f9a3 100644 --- a/internal/crosscompile/fetch.go +++ b/internal/crosscompile/fetch.go @@ -8,15 +8,20 @@ import ( "io" "net/http" "os" - "os/exec" "path" "path/filepath" "strings" "syscall" + + "github.com/goplus/llgo/internal/processenv" ) // checkDownloadAndExtractWasiSDK downloads and extracts WASI SDK func checkDownloadAndExtractWasiSDK(dir string) (wasiSdkRoot string, err error) { + return checkDownloadAndExtractWasiSDKWithProcess(dir, currentProcessInputs()) +} + +func checkDownloadAndExtractWasiSDKWithProcess(dir string, process processInputs) (wasiSdkRoot string, err error) { wasiSdkRoot = filepath.Join(dir, wasiMacosSubdir) // Check if already exists @@ -37,12 +42,16 @@ func checkDownloadAndExtractWasiSDK(dir string) (wasiSdkRoot string, err error) return wasiSdkRoot, nil } - err = downloadAndExtractArchive(wasiSdkUrl, dir, "WASI SDK") + err = downloadAndExtractArchiveWithProcess(wasiSdkUrl, dir, "WASI SDK", process) return wasiSdkRoot, err } // checkDownloadAndExtractESPClang downloads and extracts ESP Clang binaries and libraries func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { + return checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir, currentProcessInputs()) +} + +func checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir string, process processInputs) error { // Check if already exists if _, err := os.Stat(dir); err == nil { return nil @@ -66,7 +75,7 @@ func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { // Use temporary extraction directory for ESP Clang special handling tempExtractDir := dir + ".extract" - if err := downloadAndExtractArchive(clangUrl, tempExtractDir, description); err != nil { + if err := downloadAndExtractArchiveWithProcess(clangUrl, tempExtractDir, description, process); err != nil { return err } defer os.RemoveAll(tempExtractDir) @@ -81,6 +90,10 @@ func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { } func checkDownloadAndExtractLib(url, dstDir, internalArchiveSrcDir string) error { + return checkDownloadAndExtractLibWithProcess(url, dstDir, internalArchiveSrcDir, currentProcessInputs()) +} + +func checkDownloadAndExtractLibWithProcess(url, dstDir, internalArchiveSrcDir string, process processInputs) error { // Check if already exists if _, err := os.Stat(dstDir); err == nil { return nil @@ -104,7 +117,7 @@ func checkDownloadAndExtractLib(url, dstDir, internalArchiveSrcDir string) error // Use temporary extraction directory tempExtractDir := dstDir + ".extract" - if err := downloadAndExtractArchive(url, tempExtractDir, description); err != nil { + if err := downloadAndExtractArchiveWithProcess(url, tempExtractDir, description, process); err != nil { return err } defer os.RemoveAll(tempExtractDir) @@ -154,6 +167,10 @@ func releaseLock(lockFile *os.File) error { // downloadAndExtractArchive downloads and extracts an archive to the destination directory (without locking) func downloadAndExtractArchive(url, destDir, description string) error { + return downloadAndExtractArchiveWithProcess(url, destDir, description, currentProcessInputs()) +} + +func downloadAndExtractArchiveWithProcess(url, destDir, description string, process processInputs) error { fmt.Fprintf(os.Stderr, "Downloading %s...\n", description) // Use temporary extraction directory @@ -180,7 +197,7 @@ func downloadAndExtractArchive(url, destDir, description string) error { return fmt.Errorf("failed to extract %s archive: %w", description, err) } } else if strings.HasSuffix(filename, ".tar.xz") { - err := extractTarXz(localFile, tempDir) + err := extractTarXzWithProcess(localFile, tempDir, process) if err != nil { return fmt.Errorf("failed to extract %s archive: %w", description, err) } @@ -267,8 +284,12 @@ func extractTarGz(tarGzFile, dest string) error { } func extractTarXz(tarXzFile, dest string) error { + return extractTarXzWithProcess(tarXzFile, dest, currentProcessInputs()) +} + +func extractTarXzWithProcess(tarXzFile, dest string, process processInputs) error { // Use external tar command to extract .tar.xz files - cmd := exec.Command("tar", "-xf", tarXzFile, "-C", dest) + cmd := processenv.Command(process.environ, process.dir, "tar", "-xf", tarXzFile, "-C", dest) return cmd.Run() } diff --git a/internal/crosscompile/libc.go b/internal/crosscompile/libc.go index 225a4a56c4..62a468ffb6 100644 --- a/internal/crosscompile/libc.go +++ b/internal/crosscompile/libc.go @@ -15,6 +15,10 @@ var needSkipDownload = false // getLibcCompileConfigByName retrieves libc compilation configuration by name // Returns the actual libc output dir, compilation config and err func getLibcCompileConfigByName(baseDir, libcName, target, mcpu string) (outputDir string, cfg compile.CompileConfig, err error) { + return getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu, currentProcessInputs()) +} + +func getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu string, process processInputs) (outputDir string, cfg compile.CompileConfig, err error) { if libcName == "" { err = fmt.Errorf("libc name cannot be empty") return @@ -40,7 +44,7 @@ func getLibcCompileConfigByName(baseDir, libcName, target, mcpu string) (outputD return libcDir, compileConfig, err } - if err = checkDownloadAndExtractLib(config.Url, libcDir, config.ResourceSubDir); err != nil { + if err = checkDownloadAndExtractLibWithProcess(config.Url, libcDir, config.ResourceSubDir, process); err != nil { return } @@ -50,6 +54,10 @@ func getLibcCompileConfigByName(baseDir, libcName, target, mcpu string) (outputD // getRTCompileConfigByName retrieves runtime library compilation configuration by name // Returns the actual libc output dir, compilation config and err func getRTCompileConfigByName(baseDir, rtName, target string) (outputDir string, cfg compile.CompileConfig, err error) { + return getRTCompileConfigByNameWithProcess(baseDir, rtName, target, currentProcessInputs()) +} + +func getRTCompileConfigByNameWithProcess(baseDir, rtName, target string, process processInputs) (outputDir string, cfg compile.CompileConfig, err error) { if rtName == "" { err = fmt.Errorf("rt name cannot be empty") return @@ -70,7 +78,7 @@ func getRTCompileConfigByName(baseDir, rtName, target string) (outputDir string, return rtDir, compileConfig, err } - if err = checkDownloadAndExtractLib(config.Url, rtDir, config.ResourceSubDir); err != nil { + if err = checkDownloadAndExtractLibWithProcess(config.Url, rtDir, config.ResourceSubDir, process); err != nil { return } diff --git a/internal/env/env.go b/internal/env/env.go index c5b36034c1..083df32bfc 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -81,7 +81,11 @@ func LLGoCacheDir() string { } func LLGoRuntimeDir() string { - root := LLGoROOT() + return LLGoRuntimeDirWithEnv(nil) +} + +func LLGoRuntimeDirWithEnv(environ []string) string { + root := LLGoROOTWithEnv(environ) if root != "" { return filepath.Join(root, LLGoRuntimePkgName) } @@ -89,7 +93,11 @@ func LLGoRuntimeDir() string { } func LLGoROOT() string { - llgoRootEnv := os.Getenv("LLGO_ROOT") + return LLGoROOTWithEnv(nil) +} + +func LLGoROOTWithEnv(environ []string) string { + llgoRootEnv := lookupEnv(environ, "LLGO_ROOT") if llgoRootEnv != "" { if root, ok := isLLGoRoot(llgoRootEnv); ok { return root @@ -128,6 +136,19 @@ func LLGoROOT() string { return "" } +func lookupEnv(environ []string, key string) string { + if environ == nil { + return os.Getenv(key) + } + prefix := key + "=" + for i := len(environ) - 1; i >= 0; i-- { + if strings.HasPrefix(environ[i], prefix) { + return strings.TrimPrefix(environ[i], prefix) + } + } + return "" +} + func isLLGoRoot(root string) (string, bool) { if root == "" { return "", false diff --git a/internal/processenv/processenv.go b/internal/processenv/processenv.go new file mode 100644 index 0000000000..98b087d179 --- /dev/null +++ b/internal/processenv/processenv.go @@ -0,0 +1,85 @@ +package processenv + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "slices" + "strings" +) + +// Get returns the last value for key, matching the convention used when an +// exec.Cmd environment contains duplicate entries. +func Get(environ []string, key string) string { + prefix := key + "=" + for i := len(environ) - 1; i >= 0; i-- { + if strings.HasPrefix(environ[i], prefix) { + return strings.TrimPrefix(environ[i], prefix) + } + } + return "" +} + +// Command constructs a command whose path lookup, environment and working +// directory all come from the same snapshot. A nil environ preserves the +// standard os/exec process-inheritance behavior. +func Command(environ []string, dir, name string, args ...string) *exec.Cmd { + cmd := exec.Command(name, args...) + cmd.Dir = dir + if environ == nil { + return cmd + } + cmd.Env = slices.Clone(environ) + path, err := LookPath(environ, dir, name) + cmd.Path = path + cmd.Err = err + return cmd +} + +// LookPath searches file using PATH from environ. Relative PATH entries are +// interpreted from dir, which is where the resulting command will run. +func LookPath(environ []string, dir, file string) (string, error) { + if environ == nil { + return exec.LookPath(file) + } + if strings.ContainsRune(file, os.PathSeparator) { + path := file + if !filepath.IsAbs(path) && dir != "" { + path = filepath.Join(dir, path) + } + if executable(path) { + return path, nil + } + return "", exec.ErrNotFound + } + extensions := []string{""} + if runtime.GOOS == "windows" && filepath.Ext(file) == "" { + if pathExt := Get(environ, "PATHEXT"); pathExt != "" { + extensions = filepath.SplitList(strings.ToLower(pathExt)) + } + } + for _, pathDir := range filepath.SplitList(Get(environ, "PATH")) { + if pathDir == "" { + pathDir = "." + } + if !filepath.IsAbs(pathDir) && dir != "" { + pathDir = filepath.Join(dir, pathDir) + } + for _, extension := range extensions { + candidate := filepath.Join(pathDir, file+extension) + if executable(candidate) { + return candidate, nil + } + } + } + return "", exec.ErrNotFound +} + +func executable(path string) bool { + info, err := os.Stat(path) + if err != nil || info.IsDir() { + return false + } + return runtime.GOOS == "windows" || info.Mode()&0o111 != 0 +} diff --git a/internal/processenv/processenv_test.go b/internal/processenv/processenv_test.go new file mode 100644 index 0000000000..26354860f5 --- /dev/null +++ b/internal/processenv/processenv_test.go @@ -0,0 +1,41 @@ +//go:build !llgo + +package processenv + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { + workDir := t.TempDir() + binDir := filepath.Join(workDir, "bin") + if err := os.Mkdir(binDir, 0o755); err != nil { + t.Fatal(err) + } + tool := filepath.Join(binDir, "snapshot-tool") + if err := os.WriteFile(tool, []byte("#!/bin/sh\nprintf '%s:%s' \"$REQUEST_VALUE\" \"$PWD\"\n"), 0o755); err != nil { + t.Fatal(err) + } + + cmd := Command([]string{ + "PATH=bin", + "REQUEST_VALUE=snapshot", + }, workDir, "snapshot-tool") + out, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + resolvedWorkDir, err := filepath.EvalSymlinks(workDir) + if err != nil { + t.Fatal(err) + } + if got, want := string(out), "snapshot:"+resolvedWorkDir; got != want { + t.Fatalf("output = %q, want %q", got, want) + } + if !strings.HasSuffix(cmd.Path, filepath.Join("bin", "snapshot-tool")) { + t.Fatalf("command path = %q", cmd.Path) + } +} diff --git a/xtool/env/env.go b/xtool/env/env.go index 469b7e97f2..423fb9464a 100644 --- a/xtool/env/env.go +++ b/xtool/env/env.go @@ -19,10 +19,10 @@ package env import ( "fmt" "os" - "os/exec" "regexp" "strings" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/xtool/safesplit" ) @@ -32,7 +32,11 @@ var ( ) func ExpandEnvToArgs(s string) []string { - r, config := expandEnvWithCmd(s) + return ExpandEnvToArgsWithEnv(s, nil, "") +} + +func ExpandEnvToArgsWithEnv(s string, environ []string, dir string) []string { + r, config := expandEnvWithCmd(s, environ, dir) if r == "" { return nil } @@ -43,11 +47,11 @@ func ExpandEnvToArgs(s string) []string { } func ExpandEnv(s string) string { - r, _ := expandEnvWithCmd(s) + r, _ := expandEnvWithCmd(s, nil, "") return r } -func expandEnvWithCmd(s string) (string, bool) { +func expandEnvWithCmd(s string, environ []string, dir string) (string, bool) { var config bool expanded := reSubcmd.ReplaceAllStringFunc(s, func(m string) string { subcmd := strings.TrimSpace(m[2 : len(m)-1]) @@ -61,7 +65,8 @@ func expandEnvWithCmd(s string) (string, bool) { var out []byte var err error - out, err = exec.Command(cmd, args[1:]...).Output() + command := processenv.Command(environ, dir, cmd, args[1:]...) + out, err = command.Output() if err != nil { // TODO(kindy): log in verbose mode @@ -70,7 +75,19 @@ func expandEnvWithCmd(s string) (string, bool) { return strings.Replace(strings.TrimSpace(string(out)), "\n", " ", -1) }) - return strings.TrimSpace(os.Expand(expanded, os.Getenv)), config + getenv := os.Getenv + if environ != nil { + getenv = func(key string) string { + prefix := key + "=" + for i := len(environ) - 1; i >= 0; i-- { + if strings.HasPrefix(environ[i], prefix) { + return strings.TrimPrefix(environ[i], prefix) + } + } + return "" + } + } + return strings.TrimSpace(os.Expand(expanded, getenv)), config } func parseSubcmd(s string) []string { diff --git a/xtool/env/llvm/llvm.go b/xtool/env/llvm/llvm.go index a1284786d2..50a435051e 100644 --- a/xtool/env/llvm/llvm.go +++ b/xtool/env/llvm/llvm.go @@ -21,10 +21,12 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "github.com/goplus/llgo/internal/env" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/xtool/clang" "github.com/goplus/llgo/xtool/llvm/install_name_tool" "github.com/goplus/llgo/xtool/llvm/llvmlink" @@ -43,17 +45,17 @@ const ( // defaultLLVMConfigBin returns the default path to the llvm-config binary. It // checks the LLVM_CONFIG environment variable first, then searches in PATH. If // not found, it returns [ldLLVMConfigBin] as a last resort. -func defaultLLVMConfigBin() string { - bin := os.Getenv("LLVM_CONFIG") +func defaultLLVMConfigBin(environ []string, dir string) string { + bin := getenv(environ, "LLVM_CONFIG") if bin != "" { return bin } - bin, _ = exec.LookPath("llvm-config") + bin, _ = processenv.LookPath(environ, dir, "llvm-config") if bin != "" { return bin } - llgoRoot := env.LLGoROOT() + llgoRoot := env.LLGoROOTWithEnv(environ) // Check LLGO_ROOT/crosscompile/clang for llvm-config crossLLVMConfigBin := filepath.Join(llgoRoot, CrosscompileClangPath, "bin", "llvm-config") if _, err := os.Stat(crossLLVMConfigBin); err == nil { @@ -66,20 +68,35 @@ func defaultLLVMConfigBin() string { // Env represents an LLVM installation. type Env struct { - binDir string + binDir string + environ []string + dir string } // New creates a new [Env] instance. func New(llvmConfigBin string) *Env { + return NewWithEnv(llvmConfigBin, nil) +} + +// NewWithEnv creates an Env using a stable environment snapshot for tool +// discovery and subprocess execution. A nil environ inherits the current +// process environment for compatibility with existing callers. If supplied, +// dir is used to resolve relative PATH entries and as the subprocess directory. +func NewWithEnv(llvmConfigBin string, environ []string, dirs ...string) *Env { + dir := "" + if len(dirs) != 0 { + dir = dirs[0] + } if llvmConfigBin == "" { - llvmConfigBin = defaultLLVMConfigBin() + llvmConfigBin = defaultLLVMConfigBin(environ, dir) } // Note that an empty binDir is acceptable. In this case, LLVM // executables are assumed to be in PATH. - binDir, _ := exec.Command(llvmConfigBin, "--bindir").Output() + cmd := processenv.Command(environ, dir, llvmConfigBin, "--bindir") + binDir, _ := cmd.Output() - e := &Env{binDir: strings.TrimSpace(string(binDir))} + e := &Env{binDir: strings.TrimSpace(string(binDir)), environ: slices.Clone(environ), dir: dir} return e } @@ -116,7 +133,7 @@ func (e *Env) FileCheck(args ...string) (*exec.Cmd, error) { if err != nil { return nil, err } - return exec.Command(path, args...), nil + return e.command(path, args...), nil } // Readelf returns a command to execute llvm-readelf with given arguments. @@ -125,17 +142,17 @@ func (e *Env) Readelf(args ...string) (*exec.Cmd, error) { if err != nil { return nil, err } - return exec.Command(path, args...), nil + return e.command(path, args...), nil } func (e *Env) toolPath(base string) (string, error) { - if tool := searchTool(e.binDir, base); tool != "" { + if tool := searchTool(resolveDir(e.binDir, e.dir), base); tool != "" { return tool, nil } - if tool, err := exec.LookPath(base); err == nil { + if tool, err := processenv.LookPath(e.environ, e.dir, base); err == nil { return tool, nil } - if tool := searchToolInPath(base); tool != "" { + if tool := searchToolInPath(e.environ, e.dir, base); tool != "" { return tool, nil } return "", fmt.Errorf("%s not found", base) @@ -160,15 +177,39 @@ func searchTool(dir, base string) string { return "" } -func searchToolInPath(base string) string { - for _, dir := range filepath.SplitList(os.Getenv("PATH")) { - if tool := searchTool(dir, base); tool != "" { +func searchToolInPath(environ []string, workingDir, base string) string { + for _, dir := range filepath.SplitList(getenv(environ, "PATH")) { + if tool := searchTool(resolveDir(dir, workingDir), base); tool != "" { return tool } } return "" } +func (e *Env) command(path string, args ...string) *exec.Cmd { + return processenv.Command(e.environ, e.dir, path, args...) +} + +func resolveDir(dir, workingDir string) string { + if dir == "" || filepath.IsAbs(dir) || workingDir == "" { + return dir + } + return filepath.Join(workingDir, dir) +} + +func getenv(environ []string, key string) string { + if environ == nil { + return os.Getenv(key) + } + prefix := key + "=" + for i := len(environ) - 1; i >= 0; i-- { + if strings.HasPrefix(environ[i], prefix) { + return strings.TrimPrefix(environ[i], prefix) + } + } + return "" +} + func isExecutable(path string) bool { if path == "" { return false diff --git a/xtool/env/llvm/llvm_test.go b/xtool/env/llvm/llvm_test.go new file mode 100644 index 0000000000..3c2124daac --- /dev/null +++ b/xtool/env/llvm/llvm_test.go @@ -0,0 +1,62 @@ +//go:build !llgo + +package llvm + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNewWithEnvUsesExplicitToolchainEnvironment(t *testing.T) { + binDir := t.TempDir() + llvmConfig := filepath.Join(binDir, "llvm-config") + readelf := filepath.Join(binDir, "llvm-readelf") + writeExecutable(t, llvmConfig, "#!/bin/sh\nprintf '%s\\n' \""+binDir+"\"\n") + writeExecutable(t, readelf, "#!/bin/sh\ntest \"$REQUEST_MARKER\" = expected\n") + + environ := []string{ + "PATH=" + binDir, + "LLVM_CONFIG=" + llvmConfig, + "REQUEST_MARKER=expected", + } + env := NewWithEnv("", environ) + if got := env.BinDir(); got != binDir { + t.Fatalf("BinDir() = %q, want %q", got, binDir) + } + cmd, err := env.Readelf("--version") + if err != nil { + t.Fatal(err) + } + if err := cmd.Run(); err != nil { + t.Fatalf("Readelf did not use explicit environment: %v", err) + } +} + +func TestNewWithEnvResolvesRelativePathFromWorkingDirectory(t *testing.T) { + dir := t.TempDir() + binDir := filepath.Join(dir, "bin") + if err := os.Mkdir(binDir, 0o755); err != nil { + t.Fatal(err) + } + llvmConfig := filepath.Join(binDir, "llvm-config") + readelf := filepath.Join(binDir, "llvm-readelf") + writeExecutable(t, llvmConfig, "#!/bin/sh\nprintf 'bin\\n'\n") + writeExecutable(t, readelf, "#!/bin/sh\nexit 0\n") + + env := NewWithEnv("", []string{"PATH=bin"}, dir) + cmd, err := env.Readelf("--version") + if err != nil { + t.Fatal(err) + } + if err := cmd.Run(); err != nil { + t.Fatalf("Readelf did not resolve relative PATH from request directory: %v", err) + } +} + +func writeExecutable(t *testing.T, path, contents string) { + t.Helper() + if err := os.WriteFile(path, []byte(contents), 0o755); err != nil { + t.Fatal(err) + } +} From 6ecd26b4dbdd34a56951d58a74a1337969d6b6b4 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 01:15:41 +0800 Subject: [PATCH 2/9] processenv: reject relative PATH execution --- cl/compile.go | 1 - internal/build/build.go | 21 ++++--- internal/build/request.go | 9 +-- internal/clang/clang.go | 8 +-- internal/env/env.go | 10 +--- internal/processenv/processenv.go | 25 +++++--- internal/processenv/processenv_test.go | 80 +++++++++++++++++++++++++- xtool/env/env.go | 8 +-- xtool/env/llvm/llvm.go | 32 ++++++----- xtool/env/llvm/llvm_test.go | 14 +++-- 10 files changed, 142 insertions(+), 66 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index a4f9cbe150..f1c104cca5 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -155,7 +155,6 @@ func EnableTrace(b bool) { } // EnableExportRename enables or disables //export with different C symbol names. -// This is enabled when using -target flag for TinyGo compatibility. // Deprecated: pass Options to NewPackageExWithEmbedMetaOptions. func EnableExportRename(b bool) { enableExportRename = b diff --git a/internal/build/build.go b/internal/build/build.go index d46805cf36..31f45a50e5 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -2395,11 +2395,19 @@ func defaultEnv(env string, defVal string) string { } func isEnvOn(env string, defVal bool) bool { - envVal := strings.ToLower(os.Getenv(env)) - if envVal == "" { + return parseEnvBool(os.Getenv(env), defVal) +} + +func parseEnvBool(value string, defVal bool) bool { + if value == "" { return defVal } - return envVal == "1" || envVal == "true" || envVal == "on" + switch strings.ToLower(value) { + case "1", "true", "on": + return true + default: + return false + } } // cacheEnabled checks if build cache is enabled. @@ -2419,12 +2427,7 @@ func isEnvOnConfig(conf *Config, key string, defVal bool) bool { if !ok || value == "" { return defVal } - switch strings.ToLower(value) { - case "1", "true", "on": - return true - default: - return false - } + return parseEnvBool(value, defVal) } func envConfigValue(conf *Config, key string) string { diff --git a/internal/build/request.go b/internal/build/request.go index e0b5a4c010..cb0cef97f7 100644 --- a/internal/build/request.go +++ b/internal/build/request.go @@ -64,13 +64,7 @@ func snapshotProcess(req BuildRequest) (processSnapshot, error) { } func envValue(environ []string, key string) (string, bool) { - prefix := key + "=" - for i := len(environ) - 1; i >= 0; i-- { - if strings.HasPrefix(environ[i], prefix) { - return strings.TrimPrefix(environ[i], prefix), true - } - } - return "", false + return processenv.Lookup(environ, key) } func withEnv(environ []string, values ...string) []string { @@ -83,6 +77,7 @@ func withEnv(environ []string, values ...string) []string { ret := make([]string, 0, len(environ)+len(values)) for _, value := range environ { key, _, ok := strings.Cut(value, "=") + // Ignore malformed entries: exec.Cmd requires KEY=VALUE strings. if _, replace := keys[key]; ok && replace { continue } diff --git a/internal/clang/clang.go b/internal/clang/clang.go index 0efb21328b..43a37250bb 100644 --- a/internal/clang/clang.go +++ b/internal/clang/clang.go @@ -173,13 +173,7 @@ func (c *Cmd) getenv(key string) string { if c.Env == nil { return os.Getenv(key) } - prefix := key + "=" - for i := len(c.Env) - 1; i >= 0; i-- { - if strings.HasPrefix(c.Env[i], prefix) { - return strings.TrimPrefix(c.Env[i], prefix) - } - } - return "" + return processenv.Get(c.Env, key) } // CheckLinkArgs validates linking arguments by attempting a test compile. diff --git a/internal/env/env.go b/internal/env/env.go index 083df32bfc..276dcbfe9e 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -8,6 +8,8 @@ import ( "path/filepath" "runtime" "strings" + + "github.com/goplus/llgo/internal/processenv" ) const ( @@ -140,13 +142,7 @@ func lookupEnv(environ []string, key string) string { if environ == nil { return os.Getenv(key) } - prefix := key + "=" - for i := len(environ) - 1; i >= 0; i-- { - if strings.HasPrefix(environ[i], prefix) { - return strings.TrimPrefix(environ[i], prefix) - } - } - return "" + return processenv.Get(environ, key) } func isLLGoRoot(root string) (string, bool) { diff --git a/internal/processenv/processenv.go b/internal/processenv/processenv.go index 98b087d179..a8a2ae1854 100644 --- a/internal/processenv/processenv.go +++ b/internal/processenv/processenv.go @@ -12,18 +12,24 @@ import ( // Get returns the last value for key, matching the convention used when an // exec.Cmd environment contains duplicate entries. func Get(environ []string, key string) string { + value, _ := Lookup(environ, key) + return value +} + +// Lookup returns the last value for key and whether it was present. +func Lookup(environ []string, key string) (string, bool) { prefix := key + "=" for i := len(environ) - 1; i >= 0; i-- { if strings.HasPrefix(environ[i], prefix) { - return strings.TrimPrefix(environ[i], prefix) + return strings.TrimPrefix(environ[i], prefix), true } } - return "" + return "", false } // Command constructs a command whose path lookup, environment and working -// directory all come from the same snapshot. A nil environ preserves the -// standard os/exec process-inheritance behavior. +// directory all come from the same snapshot. A nil environ inherits the +// process environment while still applying dir. func Command(environ []string, dir, name string, args ...string) *exec.Cmd { cmd := exec.Command(name, args...) cmd.Dir = dir @@ -37,8 +43,9 @@ func Command(environ []string, dir, name string, args ...string) *exec.Cmd { return cmd } -// LookPath searches file using PATH from environ. Relative PATH entries are -// interpreted from dir, which is where the resulting command will run. +// LookPath searches file using PATH from environ. A match through a relative +// PATH entry is resolved against dir but returned with exec.ErrDot, matching +// the standard library safeguard against executing from a working directory. func LookPath(environ []string, dir, file string) (string, error) { if environ == nil { return exec.LookPath(file) @@ -63,12 +70,16 @@ func LookPath(environ []string, dir, file string) (string, error) { if pathDir == "" { pathDir = "." } - if !filepath.IsAbs(pathDir) && dir != "" { + relative := !filepath.IsAbs(pathDir) + if relative && dir != "" { pathDir = filepath.Join(dir, pathDir) } for _, extension := range extensions { candidate := filepath.Join(pathDir, file+extension) if executable(candidate) { + if relative { + return candidate, exec.ErrDot + } return candidate, nil } } diff --git a/internal/processenv/processenv_test.go b/internal/processenv/processenv_test.go index 26354860f5..a99414b004 100644 --- a/internal/processenv/processenv_test.go +++ b/internal/processenv/processenv_test.go @@ -3,7 +3,9 @@ package processenv import ( + "errors" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -21,7 +23,7 @@ func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { } cmd := Command([]string{ - "PATH=bin", + "PATH=" + binDir, "REQUEST_VALUE=snapshot", }, workDir, "snapshot-tool") out, err := cmd.Output() @@ -39,3 +41,79 @@ func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { t.Fatalf("command path = %q", cmd.Path) } } + +func TestLookPathRejectsRelativePathEntries(t *testing.T) { + workDir := t.TempDir() + binDir := filepath.Join(workDir, "bin") + if err := os.Mkdir(binDir, 0o755); err != nil { + t.Fatal(err) + } + tool := filepath.Join(binDir, "snapshot-tool") + if err := os.WriteFile(tool, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + + path, err := LookPath([]string{"PATH=bin"}, workDir, "snapshot-tool") + if !errors.Is(err, exec.ErrDot) { + t.Fatalf("LookPath error = %v, want exec.ErrDot", err) + } + if path != tool { + t.Fatalf("LookPath path = %q, want %q", path, tool) + } + cmd := Command([]string{"PATH=bin"}, workDir, "snapshot-tool") + if err := cmd.Run(); !errors.Is(err, exec.ErrDot) { + t.Fatalf("Command error = %v, want exec.ErrDot", err) + } + + rootTool := filepath.Join(workDir, "root-tool") + if err := os.WriteFile(rootTool, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + path, err = LookPath([]string{"PATH=" + string(os.PathListSeparator)}, workDir, "root-tool") + if !errors.Is(err, exec.ErrDot) || path != rootTool { + t.Fatalf("empty PATH entry resolved to %q, %v; want %q, exec.ErrDot", path, err, rootTool) + } +} + +func TestLookPathAllowsExplicitRelativeName(t *testing.T) { + workDir := t.TempDir() + binDir := filepath.Join(workDir, "bin") + if err := os.Mkdir(binDir, 0o755); err != nil { + t.Fatal(err) + } + tool := filepath.Join(binDir, "snapshot-tool") + if err := os.WriteFile(tool, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + cmd := Command([]string{}, workDir, filepath.Join("bin", "snapshot-tool")) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } +} + +func TestLookupAndMissingExecutable(t *testing.T) { + environ := []string{"KEY=first", "EMPTY=", "KEY=last"} + if got, ok := Lookup(environ, "KEY"); !ok || got != "last" { + t.Fatalf("Lookup(KEY) = %q, %v, want last, true", got, ok) + } + if got := Get(environ, "EMPTY"); got != "" { + t.Fatalf("Get(EMPTY) = %q, want empty", got) + } + if _, ok := Lookup(environ, "MISSING"); ok { + t.Fatal("Lookup(MISSING) reported present") + } + if _, err := LookPath([]string{"PATH=" + t.TempDir()}, "", "missing-tool"); !errors.Is(err, exec.ErrNotFound) { + t.Fatalf("LookPath missing error = %v, want exec.ErrNotFound", err) + } + + nonExecutable := filepath.Join(t.TempDir(), "non-executable") + if err := os.WriteFile(nonExecutable, []byte("data"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := LookPath([]string{}, "", nonExecutable); !errors.Is(err, exec.ErrNotFound) { + t.Fatalf("LookPath non-executable error = %v, want exec.ErrNotFound", err) + } + if _, err := LookPath([]string{}, "", t.TempDir()); !errors.Is(err, exec.ErrNotFound) { + t.Fatalf("LookPath directory error = %v, want exec.ErrNotFound", err) + } +} diff --git a/xtool/env/env.go b/xtool/env/env.go index 423fb9464a..2854270728 100644 --- a/xtool/env/env.go +++ b/xtool/env/env.go @@ -78,13 +78,7 @@ func expandEnvWithCmd(s string, environ []string, dir string) (string, bool) { getenv := os.Getenv if environ != nil { getenv = func(key string) string { - prefix := key + "=" - for i := len(environ) - 1; i >= 0; i-- { - if strings.HasPrefix(environ[i], prefix) { - return strings.TrimPrefix(environ[i], prefix) - } - } - return "" + return processenv.Get(environ, key) } } return strings.TrimSpace(os.Expand(expanded, getenv)), config diff --git a/xtool/env/llvm/llvm.go b/xtool/env/llvm/llvm.go index 50a435051e..9b3c1f1a93 100644 --- a/xtool/env/llvm/llvm.go +++ b/xtool/env/llvm/llvm.go @@ -17,6 +17,7 @@ package llvm import ( + "errors" "fmt" "os" "os/exec" @@ -50,8 +51,8 @@ func defaultLLVMConfigBin(environ []string, dir string) string { if bin != "" { return bin } - bin, _ = processenv.LookPath(environ, dir, "llvm-config") - if bin != "" { + bin, err := processenv.LookPath(environ, dir, "llvm-config") + if err == nil { return bin } @@ -151,9 +152,11 @@ func (e *Env) toolPath(base string) (string, error) { } if tool, err := processenv.LookPath(e.environ, e.dir, base); err == nil { return tool, nil + } else if errors.Is(err, exec.ErrDot) { + return "", err } - if tool := searchToolInPath(e.environ, e.dir, base); tool != "" { - return tool, nil + if tool, err := searchToolInPath(e.environ, e.dir, base); tool != "" || err != nil { + return tool, err } return "", fmt.Errorf("%s not found", base) } @@ -177,13 +180,20 @@ func searchTool(dir, base string) string { return "" } -func searchToolInPath(environ []string, workingDir, base string) string { +func searchToolInPath(environ []string, workingDir, base string) (string, error) { for _, dir := range filepath.SplitList(getenv(environ, "PATH")) { + if dir == "" { + dir = "." + } + relative := !filepath.IsAbs(dir) if tool := searchTool(resolveDir(dir, workingDir), base); tool != "" { - return tool + if relative { + return tool, exec.ErrDot + } + return tool, nil } } - return "" + return "", nil } func (e *Env) command(path string, args ...string) *exec.Cmd { @@ -201,13 +211,7 @@ func getenv(environ []string, key string) string { if environ == nil { return os.Getenv(key) } - prefix := key + "=" - for i := len(environ) - 1; i >= 0; i-- { - if strings.HasPrefix(environ[i], prefix) { - return strings.TrimPrefix(environ[i], prefix) - } - } - return "" + return processenv.Get(environ, key) } func isExecutable(path string) bool { diff --git a/xtool/env/llvm/llvm_test.go b/xtool/env/llvm/llvm_test.go index 3c2124daac..a3979cd990 100644 --- a/xtool/env/llvm/llvm_test.go +++ b/xtool/env/llvm/llvm_test.go @@ -3,7 +3,9 @@ package llvm import ( + "errors" "os" + "os/exec" "path/filepath" "testing" ) @@ -33,7 +35,7 @@ func TestNewWithEnvUsesExplicitToolchainEnvironment(t *testing.T) { } } -func TestNewWithEnvResolvesRelativePathFromWorkingDirectory(t *testing.T) { +func TestNewWithEnvRejectsRelativePathFromWorkingDirectory(t *testing.T) { dir := t.TempDir() binDir := filepath.Join(dir, "bin") if err := os.Mkdir(binDir, 0o755); err != nil { @@ -44,13 +46,13 @@ func TestNewWithEnvResolvesRelativePathFromWorkingDirectory(t *testing.T) { writeExecutable(t, llvmConfig, "#!/bin/sh\nprintf 'bin\\n'\n") writeExecutable(t, readelf, "#!/bin/sh\nexit 0\n") - env := NewWithEnv("", []string{"PATH=bin"}, dir) + env := NewWithEnv("", []string{"PATH=bin", "LLVM_CONFIG=/bin/false"}, dir) cmd, err := env.Readelf("--version") - if err != nil { - t.Fatal(err) + if !errors.Is(err, exec.ErrDot) { + t.Fatalf("Readelf error = %v, want exec.ErrDot", err) } - if err := cmd.Run(); err != nil { - t.Fatalf("Readelf did not resolve relative PATH from request directory: %v", err) + if cmd != nil { + t.Fatalf("Readelf command = %v, want nil", cmd) } } From cfcb78baefa874e85836e023e572d5ce4f267de2 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 01:53:50 +0800 Subject: [PATCH 3/9] processenv: cover inherited environment paths --- internal/processenv/processenv_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/processenv/processenv_test.go b/internal/processenv/processenv_test.go index a99414b004..ffe4fc4941 100644 --- a/internal/processenv/processenv_test.go +++ b/internal/processenv/processenv_test.go @@ -42,6 +42,28 @@ func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { } } +func TestNilEnvironmentUsesProcessEnvironment(t *testing.T) { + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + cmd := Command(nil, "", executable) + if cmd.Env != nil { + t.Fatalf("Command environment = %v, want inherited environment", cmd.Env) + } + if cmd.Path != executable { + t.Fatalf("Command path = %q, want %q", cmd.Path, executable) + } + + path, err := LookPath(nil, "", executable) + if err != nil { + t.Fatal(err) + } + if path != executable { + t.Fatalf("LookPath path = %q, want %q", path, executable) + } +} + func TestLookPathRejectsRelativePathEntries(t *testing.T) { workDir := t.TempDir() binDir := filepath.Join(workDir, "bin") From e43a3a3bbb45d421adc281327a48b10758644583 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 02:47:32 +0800 Subject: [PATCH 4/9] test: cover process-isolation compatibility paths --- cl/debug_compile_test.go | 2 +- internal/crosscompile/fetch_test.go | 6 ++++++ internal/env/env_test.go | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cl/debug_compile_test.go b/cl/debug_compile_test.go index 2f177005b2..795b51beec 100644 --- a/cl/debug_compile_test.go +++ b/cl/debug_compile_test.go @@ -69,7 +69,7 @@ var anonymous = func(seed int) int { OptLevel: optlevel.O0, }) defer prog.Dispose() - pkg, err := NewPackage(prog, ssaPkg, []*ast.File{file}) + pkg, _, err := NewPackageExWithEmbedMeta(prog, nil, nil, nil, ssaPkg, []*ast.File{file}, nil, false) if err != nil { t.Fatal(err) } diff --git a/internal/crosscompile/fetch_test.go b/internal/crosscompile/fetch_test.go index 9be771dd16..c71339deb9 100644 --- a/internal/crosscompile/fetch_test.go +++ b/internal/crosscompile/fetch_test.go @@ -243,6 +243,12 @@ func TestExtractTarGzPathTraversal(t *testing.T) { } } +func TestExtractTarXzMissingArchive(t *testing.T) { + if err := extractTarXz(filepath.Join(t.TempDir(), "missing.tar.xz"), t.TempDir()); err == nil { + t.Fatal("extractTarXz succeeded for a missing archive") + } +} + func TestDownloadAndExtractArchive(t *testing.T) { // Create test archive files := map[string]string{ diff --git a/internal/env/env_test.go b/internal/env/env_test.go index e9e8b1c70b..3d3b325c9e 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -129,6 +129,9 @@ func TestLLGoRuntimeDir(t *testing.T) { if got := LLGoRuntimeDir(); got != expected { t.Errorf("LLGoRuntimeDir() = %v, want %v", got, expected) } + if got := LLGoRuntimeDirWithEnv([]string{"LLGO_ROOT=" + tmpDir}); got != expected { + t.Errorf("LLGoRuntimeDirWithEnv() = %v, want %v", got, expected) + } }) // Test with invalid LLGO_ROOT From b5624e5f564d2b8bc8521dd1dd8fa980bead47a5 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 07:57:06 +0800 Subject: [PATCH 5/9] processenv: align executable lookup with user access --- internal/build/build.go | 7 ++--- internal/build/plan9asm.go | 12 ++------ internal/processenv/executable_other.go | 16 ++++++++++ internal/processenv/executable_unix.go | 28 +++++++++++++++++ internal/processenv/executable_unix_test.go | 34 +++++++++++++++++++++ internal/processenv/processenv.go | 8 ----- 6 files changed, 82 insertions(+), 23 deletions(-) create mode 100644 internal/processenv/executable_other.go create mode 100644 internal/processenv/executable_unix.go create mode 100644 internal/processenv/executable_unix_test.go diff --git a/internal/build/build.go b/internal/build/build.go index 31f45a50e5..a4b4d22d5e 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -2412,11 +2412,8 @@ func parseEnvBool(value string, defVal bool) bool { // cacheEnabled checks if build cache is enabled. // Cache can be disabled by setting LLGO_BUILD_CACHE=off|0 -func cacheEnabled(conf ...*Config) bool { - if len(conf) != 0 { - return isEnvOnConfig(conf[0], llgoBuildCache, true) - } - return isEnvOn(llgoBuildCache, true) +func cacheEnabled(conf *Config) bool { + return isEnvOnConfig(conf, llgoBuildCache, true) } func isEnvOnConfig(conf *Config, key string, defVal bool) bool { diff --git a/internal/build/plan9asm.go b/internal/build/plan9asm.go index f0462239fb..88e0dedfcf 100644 --- a/internal/build/plan9asm.go +++ b/internal/build/plan9asm.go @@ -353,19 +353,11 @@ func hasAltPkgForTarget(conf *Config, pkgPath string) bool { return true } -func plan9asmDisabledByEnv(confs ...*Config) bool { - var conf *Config - if len(confs) != 0 { - conf = confs[0] - } +func plan9asmDisabledByEnv(conf *Config) bool { return parsePlan9AsmPkgsEnv(envConfigValue(conf, llgoPlan9ASMPkgs)).mode == plan9asmEnvNone } -func plan9asmEnabledByEnv(pkgPath string, confs ...*Config) bool { - var conf *Config - if len(confs) != 0 { - conf = confs[0] - } +func plan9asmEnabledByEnv(pkgPath string, conf *Config) bool { cfg := parsePlan9AsmPkgsEnv(envConfigValue(conf, llgoPlan9ASMPkgs)) if cfg.mode == plan9asmEnvAll { return true diff --git a/internal/processenv/executable_other.go b/internal/processenv/executable_other.go new file mode 100644 index 0000000000..23f42b7ddf --- /dev/null +++ b/internal/processenv/executable_other.go @@ -0,0 +1,16 @@ +//go:build !unix + +package processenv + +import ( + "os" + "runtime" +) + +func executable(path string) bool { + info, err := os.Stat(path) + if err != nil || info.IsDir() { + return false + } + return runtime.GOOS == "windows" || info.Mode()&0o111 != 0 +} diff --git a/internal/processenv/executable_unix.go b/internal/processenv/executable_unix.go new file mode 100644 index 0000000000..dd9b09589f --- /dev/null +++ b/internal/processenv/executable_unix.go @@ -0,0 +1,28 @@ +//go:build unix + +package processenv + +import ( + "errors" + "os" + "syscall" + + "golang.org/x/sys/unix" +) + +func executable(path string) bool { + info, err := os.Stat(path) + if err != nil || info.IsDir() { + return false + } + err = unix.Access(path, unix.X_OK) + if err == nil { + return true + } + // Match os/exec's fallback when the access check is unavailable or blocked + // by a sandbox that filters the syscall. + if errors.Is(err, syscall.ENOSYS) || errors.Is(err, syscall.EPERM) { + return info.Mode()&0o111 != 0 + } + return false +} diff --git a/internal/processenv/executable_unix_test.go b/internal/processenv/executable_unix_test.go new file mode 100644 index 0000000000..78938b58db --- /dev/null +++ b/internal/processenv/executable_unix_test.go @@ -0,0 +1,34 @@ +//go:build !llgo && unix + +package processenv + +import ( + "os" + "path/filepath" + "testing" +) + +func TestLookPathSkipsFileNotExecutableByCurrentUser(t *testing.T) { + if os.Geteuid() == 0 { + t.Skip("root execute-access semantics differ") + } + firstDir := t.TempDir() + secondDir := t.TempDir() + firstTool := filepath.Join(firstDir, "snapshot-tool") + if err := os.WriteFile(firstTool, []byte("#!/bin/sh\nexit 1\n"), 0o001); err != nil { + t.Fatal(err) + } + secondTool := filepath.Join(secondDir, "snapshot-tool") + if err := os.WriteFile(secondTool, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil { + t.Fatal(err) + } + + pathEnv := firstDir + string(os.PathListSeparator) + secondDir + path, err := LookPath([]string{"PATH=" + pathEnv}, "", "snapshot-tool") + if err != nil { + t.Fatal(err) + } + if path != secondTool { + t.Fatalf("LookPath path = %q, want %q", path, secondTool) + } +} diff --git a/internal/processenv/processenv.go b/internal/processenv/processenv.go index a8a2ae1854..f5297b59e9 100644 --- a/internal/processenv/processenv.go +++ b/internal/processenv/processenv.go @@ -86,11 +86,3 @@ func LookPath(environ []string, dir, file string) (string, error) { } return "", exec.ErrNotFound } - -func executable(path string) bool { - info, err := os.Stat(path) - if err != nil || info.IsDir() { - return false - } - return runtime.GOOS == "windows" || info.Mode()&0o111 != 0 -} From 47b33d4915278a6e0ad46c4e101accab65393dd6 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 21:10:07 +0800 Subject: [PATCH 6/9] processenv: unify request execution context --- internal/build/build.go | 25 ++++---- internal/build/cgo.go | 33 +++++----- internal/build/cmptest.go | 8 ++- internal/build/collect.go | 2 +- internal/build/plan9asm.go | 2 +- internal/build/request.go | 53 +++------------- internal/build/run.go | 9 +-- internal/crosscompile/crosscompile.go | 67 ++++++++++---------- internal/crosscompile/fetch.go | 30 ++++----- internal/crosscompile/libc.go | 13 ++-- internal/processenv/processenv.go | 88 ++++++++++++++++++++++---- internal/processenv/processenv_test.go | 60 ++++++++++++++++-- xtool/env/llvm/llvm.go | 46 +++++++------- 13 files changed, 260 insertions(+), 176 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index a4b4d22d5e..cf863ee615 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -58,6 +58,7 @@ import ( "github.com/goplus/llgo/internal/packages" "github.com/goplus/llgo/internal/pclnmap" "github.com/goplus/llgo/internal/pclnpost" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/internal/typepatch" "github.com/goplus/llgo/ssa/abi" xenv "github.com/goplus/llgo/xtool/env" @@ -385,17 +386,17 @@ func Do(args []string, conf *Config) ([]Package, error) { // command-line callers are snapshotted once before any package or toolchain // work begins. func Build(req BuildRequest) ([]Package, error) { - snapshot, err := snapshotProcess(req) + process, err := processenv.Capture(req.Dir, req.Env) if err != nil { return nil, err } - conf, err := resolveBuildConfig(req.Config, snapshot.Env) + conf, err := resolveBuildConfig(req.Config, process.Env) if err != nil { return nil, err } // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" - export, err := crosscompile.UseWithEnv(conf.Goos, conf.Goarch, conf.Target, isEnvOnConfig(conf, llgoWasiThreads, false), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), snapshot.Env, snapshot.Dir, conf.llgoRoot) + export, err := crosscompile.UseWithContext(conf.Goos, conf.Goarch, conf.Target, isEnvOnConfig(conf, llgoWasiThreads, false), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), process, conf.llgoRoot) if err != nil { return nil, fmt.Errorf("failed to setup crosscompile: %w", err) } @@ -433,10 +434,10 @@ func Build(req BuildRequest) ([]Package, error) { cfg := &packages.Config{ Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile, BuildFlags: goBuildFlags, - Dir: snapshot.Dir, + Dir: process.Dir, Fset: token.NewFileSet(), Tests: conf.Mode == ModeTest, - Env: withEnv(snapshot.Env, "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), + Env: withEnv(process.Env, "GOOS="+conf.Goos, "GOARCH="+conf.Goarch), } if conf.Mode == ModeTest { cfg.Mode |= packages.NeedForTest @@ -616,7 +617,7 @@ func Build(req BuildRequest) ([]Package, error) { patches := make(cl.Patches, len(altPkgPaths)) altSSAPkgs(progSSA, patches, altPkgs[1:], conf, verbose) - env := llvm.NewWithEnv("", snapshot.Env, snapshot.Dir) + env := llvm.NewWithContext("", process) output := conf.OutFile != "" ctx := &context{env: env, conf: cfg, progSSA: progSSA, prog: prog, dedup: dedup, @@ -629,7 +630,7 @@ func Build(req BuildRequest) ([]Package, error) { passOpt: passOpt, buildConf: conf, crossCompile: export, - process: snapshot, + process: process, frontendOptions: frontendOptions, cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize), } @@ -672,7 +673,7 @@ func Build(req BuildRequest) ([]Package, error) { if err != nil { return nil, err } - ctx.process.resolveOutputs(outFmts) + resolveOutputs(ctx.process, outFmts) // Link main package using the output path from buildOutFmts err = linkMainPkg(ctx, pkg, allPkgs, outFmts.Out, verbose) @@ -889,7 +890,7 @@ type context struct { buildConf *Config crossCompile crosscompile.Export - process processSnapshot + process processenv.Context frontendOptions cl.Options cTransformer *cabi.Transformer @@ -1685,7 +1686,7 @@ func (c *context) createMergedArchiveFile(archivePath string, inputs []string, v if err != nil { return err } - cmd := c.process.command(arCmd, "-M") + cmd := c.process.Command(arCmd, "-M") cmd.Stdin = strings.NewReader(script.String()) printCmds := c.shouldPrintCommands(len(verbose) > 0 && verbose[0]) if printCmds { @@ -1724,7 +1725,7 @@ func (c *context) createArchiveFile(archivePath string, objFiles []string, verbo args := append([]string{"rcs", tmpName}, objFiles...) arCmd := c.archiver() - cmd := c.process.command(arCmd, args...) + cmd := c.process.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, " ")) @@ -2050,7 +2051,7 @@ func exportObjectWithClang(ctx *context, pkgPath string, exportFile string, data func llcCheck(ctx *context, exportFile string) (msg string, err error) { bin := filepath.Join(ctx.env.BinDir(), "llc") - cmd := ctx.process.command(bin, "-filetype=null", exportFile) + cmd := ctx.process.Command(bin, "-filetype=null", exportFile) var buf bytes.Buffer cmd.Stderr = &buf if err = cmd.Run(); err != nil { diff --git a/internal/build/cgo.go b/internal/build/cgo.go index 67a3dfd318..e0b9182fb2 100644 --- a/internal/build/cgo.go +++ b/internal/build/cgo.go @@ -31,6 +31,7 @@ import ( "strings" "github.com/goplus/llgo/internal/buildtags" + "github.com/goplus/llgo/internal/processenv" llssa "github.com/goplus/llgo/ssa" "github.com/goplus/llgo/xtool/safesplit" ) @@ -73,7 +74,7 @@ func buildCgo(ctx *context, pkg *aPackage, files []*ast.File, externs []string, } buildCtx.BuildTags = parseSourcePatchBuildTags(ctx.conf.BuildFlags) - srcFiles, preambles, cdecls, err := parseCgoWithProcess(ctx.process, &buildCtx, pkg, files) + srcFiles, preambles, cdecls, err := parseCgoWithContext(ctx.process, &buildCtx, pkg, files) if err != nil { return } @@ -178,7 +179,7 @@ type clangASTNode struct { Inner []clangASTNode `json:"inner,omitempty"` } -func genExternDeclsByClang(process processSnapshot, pkg *aPackage, src string, cflags []string, cgoSymbols map[string]string, verbose bool) (string, error) { +func genExternDeclsByClang(process processenv.Context, 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) @@ -245,7 +246,7 @@ static void _init_%s() { return b.String(), nil } -func getMacroNames(process processSnapshot, file string, cflags []string, macroNames map[string]bool, verbose bool) error { +func getMacroNames(process processenv.Context, file string, cflags []string, macroNames map[string]bool, verbose bool) error { args := append([]string{"-dM", "-E"}, cflags...) args = append(args, file) cmd := execCommandVerbose(process, verbose, "clang", args...) @@ -265,7 +266,7 @@ func getMacroNames(process processSnapshot, file string, cflags []string, macroN return nil } -func getFuncNames(process processSnapshot, file string, cflags []string, symbolNames map[string]bool, verbose bool) error { +func getFuncNames(process processenv.Context, 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(process, verbose, "clang", args...) @@ -295,11 +296,11 @@ func getFuncNames(process processSnapshot, file string, cflags []string, symbolN return nil } -func execCommandVerbose(process processSnapshot, verbose bool, name string, arg ...string) *exec.Cmd { +func execCommandVerbose(process processenv.Context, verbose bool, name string, arg ...string) *exec.Cmd { if verbose { fmt.Fprintf(os.Stderr, "%s %s\n", name, strings.Join(arg, " ")) } - return process.command(name, arg...) + return process.Command(name, arg...) } func extractFuncNames(node *clangASTNode, funcNames map[string]bool) { @@ -311,10 +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 parseCgoWithProcess(processSnapshot{}, buildCtx, pkg, files) + return parseCgoWithContext(processenv.Context{}, buildCtx, pkg, files) } -func parseCgoWithProcess(process processSnapshot, buildCtx *build.Context, pkg *aPackage, files []*ast.File) (srcFiles []cgoSrcFile, preambles []cgoPreamble, cdecls []cgoDecl, err error) { +func parseCgoWithContext(process processenv.Context, 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) @@ -383,7 +384,7 @@ func parseCgoWithProcess(process processSnapshot, buildCtx *build.Context, pkg * spec := decl.Specs[0].(*ast.ImportSpec) if spec.Path.Value == "\"unsafe\"" { pos := pkg.Fset.Position(doc.Pos()) - preamble, flags, err := parseCgoPreambleWithProcess(process, pos, doc.Text()) + preamble, flags, err := parseCgoPreambleWithContext(process, pos, doc.Text()) if err != nil { panic(err) } @@ -399,10 +400,10 @@ func parseCgoWithProcess(process processSnapshot, buildCtx *build.Context, pkg * } func parseCgoPreamble(pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { - return parseCgoPreambleWithProcess(processSnapshot{}, pos, text) + return parseCgoPreambleWithContext(processenv.Context{}, pos, text) } -func parseCgoPreambleWithProcess(process processSnapshot, pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { +func parseCgoPreambleWithContext(process processenv.Context, pos token.Position, text string) (preamble cgoPreamble, decls []cgoDecl, err error) { b := strings.Builder{} fline := pos.Line fname := pos.Filename @@ -413,7 +414,7 @@ func parseCgoPreambleWithProcess(process processSnapshot, pos token.Position, te line = strings.TrimSpace(line) if strings.HasPrefix(line, "#cgo ") { var cgoDecls []cgoDecl - cgoDecls, err = parseCgoDeclWithProcess(process, line) + cgoDecls, err = parseCgoDeclWithContext(process, line) if err != nil { return } @@ -440,10 +441,10 @@ func parseCgoPreambleWithProcess(process processSnapshot, pos token.Position, te // #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 parseCgoDeclWithProcess(processSnapshot{}, line) + return parseCgoDeclWithContext(processenv.Context{}, line) } -func parseCgoDeclWithProcess(process processSnapshot, line string) (cgoDecls []cgoDecl, err error) { +func parseCgoDeclWithContext(process processenv.Context, line string) (cgoDecls []cgoDecl, err error) { idx := strings.Index(line, ":") if idx == -1 { err = fmt.Errorf("invalid cgo format: %v", line) @@ -474,12 +475,12 @@ func parseCgoDeclWithProcess(process processSnapshot, line string) (cgoDecls []c switch flag { case "pkg-config": - ldflags, e := process.command("pkg-config", "--libs", arg).Output() + ldflags, e := process.Command("pkg-config", "--libs", arg).Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return } - cflags, e := process.command("pkg-config", "--cflags", arg).Output() + cflags, e := process.Command("pkg-config", "--cflags", arg).Output() if e != nil { err = fmt.Errorf("pkg-config: %v", e) return diff --git a/internal/build/cmptest.go b/internal/build/cmptest.go index cf3cd1b64a..192cad3591 100644 --- a/internal/build/cmptest.go +++ b/internal/build/cmptest.go @@ -25,9 +25,11 @@ import ( "os" "os/exec" "path/filepath" + + "github.com/goplus/llgo/internal/processenv" ) -func cmpTest(process processSnapshot, dir, pkgPath, llApp string, genExpect bool, runArgs []string) { +func cmpTest(process processenv.Context, dir, pkgPath, llApp string, genExpect bool, runArgs []string) { var llgoOut, llgoErr bytes.Buffer var llgoRunErr = runApp(process, runArgs, dir, &llgoOut, &llgoErr, llApp) @@ -91,7 +93,7 @@ func checkEqual(prompt string, a, expected []byte) { fatal(errors.New("checkEqual: unexpected " + prompt)) } -func runApp(process processSnapshot, runArgs []string, dir string, stdout, stderr io.Writer, app string, args ...string) error { +func runApp(process processenv.Context, 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...) @@ -99,7 +101,7 @@ func runApp(process processSnapshot, runArgs []string, dir string, stdout, stder args = runArgs } } - cmd := process.command(app, args...) + cmd := process.Command(app, args...) cmd.Dir = dir cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/internal/build/collect.go b/internal/build/collect.go index 88fa67ec7f..45de22bc39 100644 --- a/internal/build/collect.go +++ b/internal/build/collect.go @@ -278,7 +278,7 @@ func detectLLVMVersion(ctx *context) string { if cc == "" { cc = "clang" } - versionCmd := ctx.process.command(cc, "--version") + versionCmd := ctx.process.Command(cc, "--version") output, err := versionCmd.Output() if err != nil { return "" diff --git a/internal/build/plan9asm.go b/internal/build/plan9asm.go index 88e0dedfcf..2ab8a3cdeb 100644 --- a/internal/build/plan9asm.go +++ b/internal/build/plan9asm.go @@ -407,7 +407,7 @@ func pkgSFiles(ctx *context, pkg *packages.Package) ([]string, error) { } args = append(args, pkg.PkgPath) - cmd := ctx.process.command("go", args...) + cmd := ctx.process.Command("go", args...) // 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 { diff --git a/internal/build/request.go b/internal/build/request.go index cb0cef97f7..fd023bfe1d 100644 --- a/internal/build/request.go +++ b/internal/build/request.go @@ -1,11 +1,6 @@ package build import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "slices" "strings" "github.com/goplus/llgo/internal/processenv" @@ -21,46 +16,14 @@ type BuildRequest struct { Env []string } -type processSnapshot struct { - Dir string - Env []string -} - -func (p processSnapshot) command(name string, args ...string) *exec.Cmd { - return processenv.Command(p.Env, p.Dir, name, args...) -} - -func (p processSnapshot) path(path string) string { - if path == "" || filepath.IsAbs(path) { - return path - } - return filepath.Join(p.Dir, path) -} - -func (p processSnapshot) resolveOutputs(out *OutFmtDetails) { - out.Out = p.path(out.Out) - out.PCLN = p.path(out.PCLN) - out.Bin = p.path(out.Bin) - out.Hex = p.path(out.Hex) - out.Img = p.path(out.Img) - out.Uf2 = p.path(out.Uf2) - out.Zip = p.path(out.Zip) -} - -func snapshotProcess(req BuildRequest) (processSnapshot, error) { - dir := req.Dir - if dir == "" { - var err error - dir, err = os.Getwd() - if err != nil { - return processSnapshot{}, fmt.Errorf("get working directory: %w", err) - } - } - env := slices.Clone(req.Env) - if req.Env == nil { - env = os.Environ() - } - return processSnapshot{Dir: dir, Env: env}, nil +func resolveOutputs(process processenv.Context, out *OutFmtDetails) { + out.Out = process.Abs(out.Out) + out.PCLN = process.Abs(out.PCLN) + out.Bin = process.Abs(out.Bin) + out.Hex = process.Abs(out.Hex) + out.Img = process.Abs(out.Img) + out.Uf2 = process.Abs(out.Uf2) + out.Zip = process.Abs(out.Zip) } func envValue(environ []string, key string) (string, bool) { diff --git a/internal/build/run.go b/internal/build/run.go index a9db1eadc5..b2a86c61e2 100644 --- a/internal/build/run.go +++ b/internal/build/run.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/goplus/llgo/internal/mockable" + "github.com/goplus/llgo/internal/processenv" "github.com/goplus/llgo/internal/shellparse" ) @@ -61,7 +62,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod if conf.PrintCommands { fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(args, " ")) } - cmd := ctx.process.command(app, args...) + cmd := ctx.process.Command(app, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -76,7 +77,7 @@ func runNative(ctx *context, app, pkgDir, pkgName string, conf *Config, mode Mod if conf.PrintCommands { fmt.Fprintf(os.Stderr, "%s %s\n", app, strings.Join(conf.RunArgs, " ")) } - cmd := ctx.process.command(app, conf.RunArgs...) + cmd := ctx.process.Command(app, conf.RunArgs...) cmd.Dir = pkgDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -125,7 +126,7 @@ func runInEmulator(ctx *context, emulator string, envMap map[string]string, pkgD } // runEmuCmd runs the application in emulator by formatting the emulator command template -func runEmuCmd(process processSnapshot, envMap map[string]string, emulatorTemplate string, runArgs []string, verbose bool, printCmds bool) error { +func runEmuCmd(process processenv.Context, 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 { @@ -158,7 +159,7 @@ func runEmuCmd(process processSnapshot, envMap map[string]string, emulatorTempla } // Execute the emulator command - cmd := process.command(cmdParts[0], cmdParts[1:]...) + cmd := process.Command(cmdParts[0], cmdParts[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 91ee1172df..8e1ba61f3b 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -47,16 +47,6 @@ type Export struct { Device flash.Device // Device configuration for flashing/debugging } -type processInputs struct { - environ []string - dir string - llgoRoot string -} - -func currentProcessInputs() processInputs { - return processInputs{llgoRoot: env.LLGoROOT()} -} - // DebugInfoPolicy describes how a selected linker handles debug information. // Build orchestration consumes this typed capability instead of inferring it // from a target name or linker executable. @@ -126,8 +116,8 @@ func getCanonicalArchName(triple string) string { } // getMacOSSysroot returns the macOS SDK path using xcrun -func getMacOSSysroot(process processInputs) (string, error) { - cmd := processenv.Command(process.environ, process.dir, "xcrun", "--sdk", "macosx", "--show-sdk-path") +func getMacOSSysroot(process processenv.Context) (string, error) { + cmd := process.Command("xcrun", "--sdk", "macosx", "--show-sdk-path") output, err := cmd.Output() if err != nil { return "", err @@ -137,9 +127,9 @@ func getMacOSSysroot(process processInputs) (string, error) { // getESPClangRoot returns the ESP Clang root directory, checking LLGoROOT first, // then downloading if needed and platform is supported -func getESPClangRoot(forceEspClang bool, process processInputs) (clangRoot string, err error) { +func getESPClangRoot(forceEspClang bool, process processenv.Context, llgoRoot string) (clangRoot string, err error) { // First check if clang exists in LLGoROOT - espClangRoot := filepath.Join(process.llgoRoot, envllvm.CrosscompileClangPath) + espClangRoot := filepath.Join(llgoRoot, envllvm.CrosscompileClangPath) if _, err = os.Stat(espClangRoot); err == nil { clangRoot = espClangRoot return @@ -158,7 +148,7 @@ func getESPClangRoot(forceEspClang bool, process processInputs) (clangRoot strin return } fmt.Fprintln(os.Stderr, "ESP Clang not found in LLGO_ROOT or cache, will download.") - if err = checkDownloadAndExtractESPClangWithProcess(platformSuffix, cacheClangDir, process); err != nil { + if err = checkDownloadAndExtractESPClangWithContext(platformSuffix, cacheClangDir, process); err != nil { return } } @@ -226,15 +216,14 @@ func compileWithConfig( } func use(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { - return useWithProcess(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, currentProcessInputs()) + return useWithContext(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, processenv.Context{}, env.LLGoROOT()) } -func useWithProcess(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, process processInputs) (export Export, err error) { +func useWithContext(goos, goarch string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, process processenv.Context, llgoRoot string) (export Export, err error) { targetTriple := llvm.GetTargetTriple(goos, goarch) - llgoRoot := process.llgoRoot // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(forceEspClang, process) + clangRoot, err := getESPClangRoot(forceEspClang, process, llgoRoot) if err != nil { return } @@ -351,7 +340,7 @@ func useWithProcess(goos, goarch string, wasiThreads, forceEspClang bool, level // If not exists in LLGoROOT, download and use cached wasiSdkRoot if _, err = os.Stat(wasiSdkRoot); err != nil { sdkDir := filepath.Join(cacheDir(), llvm.GetTargetTriple(goos, goarch)) - if wasiSdkRoot, err = checkDownloadAndExtractWasiSDKWithProcess(sdkDir, process); err != nil { + if wasiSdkRoot, err = checkDownloadAndExtractWasiSDKWithContext(sdkDir, process); err != nil { return } } @@ -474,10 +463,10 @@ func useWithProcess(goos, goarch string, wasiThreads, forceEspClang bool, level // UseTarget loads configuration from a target name (e.g., "rp2040", "wasi") func UseTarget(targetName string, level optlevel.Level, ltoMode lto.Mode) (export Export, err error) { - return useTargetWithProcess(targetName, level, ltoMode, currentProcessInputs()) + return useTargetWithContext(targetName, level, ltoMode, processenv.Context{}, env.LLGoROOT()) } -func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.Mode, process processInputs) (export Export, err error) { +func useTargetWithContext(targetName string, level optlevel.Level, ltoMode lto.Mode, process processenv.Context, llgoRoot string) (export Export, err error) { resolver := targets.NewDefaultResolver() config, err := resolver.Resolve(targetName) @@ -496,7 +485,7 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M } // Check for ESP Clang support for target-based builds - clangRoot, err := getESPClangRoot(true, process) + clangRoot, err := getESPClangRoot(true, process, llgoRoot) if err != nil { return } @@ -538,7 +527,7 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M } // Build environment map for template variable expansion - envs := buildEnvMap(process.llgoRoot) + envs := buildEnvMap(llgoRoot) // Convert LLVMTarget, CPU, Features to CCFLAGS/LDFLAGS // ICF off for Go pc-identity semantics (see the non-cross flags above). @@ -664,7 +653,7 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M if config.LinkerScript != "" { ldflags = append(ldflags, "-T", config.LinkerScript) } - ldflags = append(ldflags, "-L", process.llgoRoot) // search targets/*.ld + ldflags = append(ldflags, "-L", llgoRoot) // search targets/*.ld var libcIncludeDir []string @@ -674,15 +663,15 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M var compileConfig compile.CompileConfig baseDir := filepath.Join(cacheRoot(), "crosscompile") - outputDir, compileConfig, err = getLibcCompileConfigByNameWithProcess(baseDir, config.Libc, config.LLVMTarget, config.CPU, process) + outputDir, compileConfig, err = getLibcCompileConfigByNameWithContext(baseDir, config.Libc, config.LLVMTarget, config.CPU, process) if err != nil { return } libcLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{ CC: export.CC, Linker: export.Linker, - Env: process.environ, - Dir: process.dir, + Env: process.Env, + Dir: process.Dir, CCFLAGS: ccflags, LDFLAGS: ldflags, }) @@ -702,15 +691,15 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M var compileConfig compile.CompileConfig baseDir := filepath.Join(cacheRoot(), "crosscompile") - outputDir, compileConfig, err = getRTCompileConfigByNameWithProcess(baseDir, config.RTLib, config.LLVMTarget, process) + outputDir, compileConfig, err = getRTCompileConfigByNameWithContext(baseDir, config.RTLib, config.LLVMTarget, process) if err != nil { return } rtLibLDFlags, err = compileWithConfig(compileConfig, outputDir, compile.CompileOptions{ CC: export.CC, Linker: export.Linker, - Env: process.environ, - Dir: process.dir, + Env: process.Env, + Dir: process.Dir, CCFLAGS: ccflags, LDFLAGS: ldflags, CFLAGS: libcIncludeDir, @@ -733,15 +722,23 @@ func useTargetWithProcess(targetName string, level optlevel.Level, ltoMode lto.M // Use extends the original Use function to support target-based configuration // If targetName is provided, it takes precedence over goos/goarch func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool) (export Export, err error) { - return UseWithEnv(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, nil, "", env.LLGoROOT()) + return UseWithContext(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, processenv.Context{}, env.LLGoROOT()) } // UseWithEnv selects a toolchain using explicit process inputs. Download and // cache operations remain protected by their existing cross-process locks. func UseWithEnv(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, environ []string, dir, llgoRoot string) (export Export, err error) { - process := processInputs{environ: environ, dir: dir, llgoRoot: llgoRoot} + return UseWithContext(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, processenv.Context{ + Env: environ, + Dir: dir, + }, llgoRoot) +} + +// UseWithContext selects a toolchain using explicit process context. Download +// and cache operations remain protected by their existing cross-process locks. +func UseWithContext(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, process processenv.Context, llgoRoot string) (export Export, err error) { if targetName != "" && !strings.HasPrefix(targetName, "wasm") && !strings.HasPrefix(targetName, "wasi") { - return useTargetWithProcess(targetName, level, ltoMode, process) + return useTargetWithContext(targetName, level, ltoMode, process, llgoRoot) } - return useWithProcess(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, process) + return useWithContext(goos, goarch, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, process, llgoRoot) } diff --git a/internal/crosscompile/fetch.go b/internal/crosscompile/fetch.go index 39ccd5f9a3..d093b37557 100644 --- a/internal/crosscompile/fetch.go +++ b/internal/crosscompile/fetch.go @@ -18,10 +18,10 @@ import ( // checkDownloadAndExtractWasiSDK downloads and extracts WASI SDK func checkDownloadAndExtractWasiSDK(dir string) (wasiSdkRoot string, err error) { - return checkDownloadAndExtractWasiSDKWithProcess(dir, currentProcessInputs()) + return checkDownloadAndExtractWasiSDKWithContext(dir, processenv.Context{}) } -func checkDownloadAndExtractWasiSDKWithProcess(dir string, process processInputs) (wasiSdkRoot string, err error) { +func checkDownloadAndExtractWasiSDKWithContext(dir string, process processenv.Context) (wasiSdkRoot string, err error) { wasiSdkRoot = filepath.Join(dir, wasiMacosSubdir) // Check if already exists @@ -42,16 +42,16 @@ func checkDownloadAndExtractWasiSDKWithProcess(dir string, process processInputs return wasiSdkRoot, nil } - err = downloadAndExtractArchiveWithProcess(wasiSdkUrl, dir, "WASI SDK", process) + err = downloadAndExtractArchiveWithContext(wasiSdkUrl, dir, "WASI SDK", process) return wasiSdkRoot, err } // checkDownloadAndExtractESPClang downloads and extracts ESP Clang binaries and libraries func checkDownloadAndExtractESPClang(platformSuffix, dir string) error { - return checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir, currentProcessInputs()) + return checkDownloadAndExtractESPClangWithContext(platformSuffix, dir, processenv.Context{}) } -func checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir string, process processInputs) error { +func checkDownloadAndExtractESPClangWithContext(platformSuffix, dir string, process processenv.Context) error { // Check if already exists if _, err := os.Stat(dir); err == nil { return nil @@ -75,7 +75,7 @@ func checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir string, proc // Use temporary extraction directory for ESP Clang special handling tempExtractDir := dir + ".extract" - if err := downloadAndExtractArchiveWithProcess(clangUrl, tempExtractDir, description, process); err != nil { + if err := downloadAndExtractArchiveWithContext(clangUrl, tempExtractDir, description, process); err != nil { return err } defer os.RemoveAll(tempExtractDir) @@ -90,10 +90,10 @@ func checkDownloadAndExtractESPClangWithProcess(platformSuffix, dir string, proc } func checkDownloadAndExtractLib(url, dstDir, internalArchiveSrcDir string) error { - return checkDownloadAndExtractLibWithProcess(url, dstDir, internalArchiveSrcDir, currentProcessInputs()) + return checkDownloadAndExtractLibWithContext(url, dstDir, internalArchiveSrcDir, processenv.Context{}) } -func checkDownloadAndExtractLibWithProcess(url, dstDir, internalArchiveSrcDir string, process processInputs) error { +func checkDownloadAndExtractLibWithContext(url, dstDir, internalArchiveSrcDir string, process processenv.Context) error { // Check if already exists if _, err := os.Stat(dstDir); err == nil { return nil @@ -117,7 +117,7 @@ func checkDownloadAndExtractLibWithProcess(url, dstDir, internalArchiveSrcDir st // Use temporary extraction directory tempExtractDir := dstDir + ".extract" - if err := downloadAndExtractArchiveWithProcess(url, tempExtractDir, description, process); err != nil { + if err := downloadAndExtractArchiveWithContext(url, tempExtractDir, description, process); err != nil { return err } defer os.RemoveAll(tempExtractDir) @@ -167,10 +167,10 @@ func releaseLock(lockFile *os.File) error { // downloadAndExtractArchive downloads and extracts an archive to the destination directory (without locking) func downloadAndExtractArchive(url, destDir, description string) error { - return downloadAndExtractArchiveWithProcess(url, destDir, description, currentProcessInputs()) + return downloadAndExtractArchiveWithContext(url, destDir, description, processenv.Context{}) } -func downloadAndExtractArchiveWithProcess(url, destDir, description string, process processInputs) error { +func downloadAndExtractArchiveWithContext(url, destDir, description string, process processenv.Context) error { fmt.Fprintf(os.Stderr, "Downloading %s...\n", description) // Use temporary extraction directory @@ -197,7 +197,7 @@ func downloadAndExtractArchiveWithProcess(url, destDir, description string, proc return fmt.Errorf("failed to extract %s archive: %w", description, err) } } else if strings.HasSuffix(filename, ".tar.xz") { - err := extractTarXzWithProcess(localFile, tempDir, process) + err := extractTarXzWithContext(localFile, tempDir, process) if err != nil { return fmt.Errorf("failed to extract %s archive: %w", description, err) } @@ -284,12 +284,12 @@ func extractTarGz(tarGzFile, dest string) error { } func extractTarXz(tarXzFile, dest string) error { - return extractTarXzWithProcess(tarXzFile, dest, currentProcessInputs()) + return extractTarXzWithContext(tarXzFile, dest, processenv.Context{}) } -func extractTarXzWithProcess(tarXzFile, dest string, process processInputs) error { +func extractTarXzWithContext(tarXzFile, dest string, process processenv.Context) error { // Use external tar command to extract .tar.xz files - cmd := processenv.Command(process.environ, process.dir, "tar", "-xf", tarXzFile, "-C", dest) + cmd := process.Command("tar", "-xf", tarXzFile, "-C", dest) return cmd.Run() } diff --git a/internal/crosscompile/libc.go b/internal/crosscompile/libc.go index 62a468ffb6..4eed50d7ed 100644 --- a/internal/crosscompile/libc.go +++ b/internal/crosscompile/libc.go @@ -7,6 +7,7 @@ import ( "github.com/goplus/llgo/internal/crosscompile/compile" "github.com/goplus/llgo/internal/crosscompile/compile/libc" "github.com/goplus/llgo/internal/crosscompile/compile/rtlib" + "github.com/goplus/llgo/internal/processenv" ) // for testing, in testing env, we use fake path, it will cause downloading failure @@ -15,10 +16,10 @@ var needSkipDownload = false // getLibcCompileConfigByName retrieves libc compilation configuration by name // Returns the actual libc output dir, compilation config and err func getLibcCompileConfigByName(baseDir, libcName, target, mcpu string) (outputDir string, cfg compile.CompileConfig, err error) { - return getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu, currentProcessInputs()) + return getLibcCompileConfigByNameWithContext(baseDir, libcName, target, mcpu, processenv.Context{}) } -func getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu string, process processInputs) (outputDir string, cfg compile.CompileConfig, err error) { +func getLibcCompileConfigByNameWithContext(baseDir, libcName, target, mcpu string, process processenv.Context) (outputDir string, cfg compile.CompileConfig, err error) { if libcName == "" { err = fmt.Errorf("libc name cannot be empty") return @@ -44,7 +45,7 @@ func getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu strin return libcDir, compileConfig, err } - if err = checkDownloadAndExtractLibWithProcess(config.Url, libcDir, config.ResourceSubDir, process); err != nil { + if err = checkDownloadAndExtractLibWithContext(config.Url, libcDir, config.ResourceSubDir, process); err != nil { return } @@ -54,10 +55,10 @@ func getLibcCompileConfigByNameWithProcess(baseDir, libcName, target, mcpu strin // getRTCompileConfigByName retrieves runtime library compilation configuration by name // Returns the actual libc output dir, compilation config and err func getRTCompileConfigByName(baseDir, rtName, target string) (outputDir string, cfg compile.CompileConfig, err error) { - return getRTCompileConfigByNameWithProcess(baseDir, rtName, target, currentProcessInputs()) + return getRTCompileConfigByNameWithContext(baseDir, rtName, target, processenv.Context{}) } -func getRTCompileConfigByNameWithProcess(baseDir, rtName, target string, process processInputs) (outputDir string, cfg compile.CompileConfig, err error) { +func getRTCompileConfigByNameWithContext(baseDir, rtName, target string, process processenv.Context) (outputDir string, cfg compile.CompileConfig, err error) { if rtName == "" { err = fmt.Errorf("rt name cannot be empty") return @@ -78,7 +79,7 @@ func getRTCompileConfigByNameWithProcess(baseDir, rtName, target string, process return rtDir, compileConfig, err } - if err = checkDownloadAndExtractLibWithProcess(config.Url, rtDir, config.ResourceSubDir, process); err != nil { + if err = checkDownloadAndExtractLibWithContext(config.Url, rtDir, config.ResourceSubDir, process); err != nil { return } diff --git a/internal/processenv/processenv.go b/internal/processenv/processenv.go index f5297b59e9..2cc4042c0c 100644 --- a/internal/processenv/processenv.go +++ b/internal/processenv/processenv.go @@ -1,6 +1,7 @@ package processenv import ( + "fmt" "os" "os/exec" "path/filepath" @@ -9,6 +10,61 @@ import ( "strings" ) +// Context contains the process-derived inputs used to resolve paths and launch +// subprocesses for one request. +type Context struct { + Dir string + Env []string +} + +// Clone returns an independent copy of the context. +func (c Context) Clone() Context { + c.Env = slices.Clone(c.Env) + return c +} + +// Capture resolves omitted process inputs once. A nil environ snapshots the +// current process environment; an empty non-nil environ remains empty. +func Capture(dir string, environ []string) (Context, error) { + if dir == "" { + var err error + dir, err = os.Getwd() + if err != nil { + return Context{}, fmt.Errorf("get working directory: %w", err) + } + } + env := slices.Clone(environ) + if environ == nil { + env = os.Environ() + } + return Context{Dir: dir, Env: env}, nil +} + +// Abs resolves path relative to the context working directory. +func (c Context) Abs(path string) string { + if path == "" || filepath.IsAbs(path) { + return path + } + return filepath.Join(c.Dir, path) +} + +// Get returns the last value for key in the context environment. +func (c Context) Get(key string) string { + if c.Env == nil { + return os.Getenv(key) + } + return Get(c.Env, key) +} + +// Lookup returns the last value for key in the context environment and whether +// it was present. +func (c Context) Lookup(key string) (string, bool) { + if c.Env == nil { + return os.LookupEnv(key) + } + return Lookup(c.Env, key) +} + // Get returns the last value for key, matching the convention used when an // exec.Cmd environment contains duplicate entries. func Get(environ []string, key string) string { @@ -31,13 +87,18 @@ func Lookup(environ []string, key string) (string, bool) { // directory all come from the same snapshot. A nil environ inherits the // process environment while still applying dir. func Command(environ []string, dir, name string, args ...string) *exec.Cmd { + return (Context{Dir: dir, Env: environ}).Command(name, args...) +} + +// Command constructs a command using the context environment and directory. +func (c Context) Command(name string, args ...string) *exec.Cmd { cmd := exec.Command(name, args...) - cmd.Dir = dir - if environ == nil { + cmd.Dir = c.Dir + if c.Env == nil { return cmd } - cmd.Env = slices.Clone(environ) - path, err := LookPath(environ, dir, name) + cmd.Env = slices.Clone(c.Env) + path, err := c.LookPath(name) cmd.Path = path cmd.Err = err return cmd @@ -47,13 +108,18 @@ func Command(environ []string, dir, name string, args ...string) *exec.Cmd { // PATH entry is resolved against dir but returned with exec.ErrDot, matching // the standard library safeguard against executing from a working directory. func LookPath(environ []string, dir, file string) (string, error) { - if environ == nil { + return (Context{Dir: dir, Env: environ}).LookPath(file) +} + +// LookPath searches file using PATH from the context environment. +func (c Context) LookPath(file string) (string, error) { + if c.Env == nil { return exec.LookPath(file) } if strings.ContainsRune(file, os.PathSeparator) { path := file - if !filepath.IsAbs(path) && dir != "" { - path = filepath.Join(dir, path) + if !filepath.IsAbs(path) && c.Dir != "" { + path = filepath.Join(c.Dir, path) } if executable(path) { return path, nil @@ -62,17 +128,17 @@ func LookPath(environ []string, dir, file string) (string, error) { } extensions := []string{""} if runtime.GOOS == "windows" && filepath.Ext(file) == "" { - if pathExt := Get(environ, "PATHEXT"); pathExt != "" { + if pathExt := c.Get("PATHEXT"); pathExt != "" { extensions = filepath.SplitList(strings.ToLower(pathExt)) } } - for _, pathDir := range filepath.SplitList(Get(environ, "PATH")) { + for _, pathDir := range filepath.SplitList(c.Get("PATH")) { if pathDir == "" { pathDir = "." } relative := !filepath.IsAbs(pathDir) - if relative && dir != "" { - pathDir = filepath.Join(dir, pathDir) + if relative && c.Dir != "" { + pathDir = filepath.Join(c.Dir, pathDir) } for _, extension := range extensions { candidate := filepath.Join(pathDir, file+extension) diff --git a/internal/processenv/processenv_test.go b/internal/processenv/processenv_test.go index ffe4fc4941..253b86468c 100644 --- a/internal/processenv/processenv_test.go +++ b/internal/processenv/processenv_test.go @@ -11,6 +11,54 @@ import ( "testing" ) +func TestCaptureClonesExplicitInputs(t *testing.T) { + workDir := t.TempDir() + environ := []string{"KEY=value"} + process, err := Capture(workDir, environ) + if err != nil { + t.Fatal(err) + } + environ[0] = "KEY=changed" + if got := process.Get("KEY"); got != "value" { + t.Fatalf("Get(KEY) = %q, want value", got) + } + if got := process.Abs("out.o"); got != filepath.Join(workDir, "out.o") { + t.Fatalf("Abs(out.o) = %q", got) + } + + clone := process.Clone() + clone.Env[0] = "KEY=clone" + if got := process.Get("KEY"); got != "value" { + t.Fatalf("clone changed source context: Get(KEY) = %q", got) + } +} + +func TestCaptureSnapshotsAmbientInputs(t *testing.T) { + t.Setenv("PROCESSENV_CAPTURE_TEST", "before") + process, err := Capture("", nil) + if err != nil { + t.Fatal(err) + } + t.Setenv("PROCESSENV_CAPTURE_TEST", "after") + if got := process.Get("PROCESSENV_CAPTURE_TEST"); got != "before" { + t.Fatalf("Get(PROCESSENV_CAPTURE_TEST) = %q, want before", got) + } + if process.Dir == "" || !filepath.IsAbs(process.Dir) { + t.Fatalf("Capture directory = %q, want absolute working directory", process.Dir) + } +} + +func TestZeroContextUsesAmbientEnvironment(t *testing.T) { + t.Setenv("PROCESSENV_CONTEXT_TEST", "ambient") + var process Context + if got := process.Get("PROCESSENV_CONTEXT_TEST"); got != "ambient" { + t.Fatalf("Get(PROCESSENV_CONTEXT_TEST) = %q, want ambient", got) + } + if got, ok := process.Lookup("PROCESSENV_CONTEXT_TEST"); !ok || got != "ambient" { + t.Fatalf("Lookup(PROCESSENV_CONTEXT_TEST) = %q, %v, want ambient, true", got, ok) + } +} + func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { workDir := t.TempDir() binDir := filepath.Join(workDir, "bin") @@ -22,10 +70,14 @@ func TestCommandUsesSnapshotPathEnvironmentAndDir(t *testing.T) { t.Fatal(err) } - cmd := Command([]string{ - "PATH=" + binDir, - "REQUEST_VALUE=snapshot", - }, workDir, "snapshot-tool") + process := Context{ + Dir: workDir, + Env: []string{ + "PATH=" + binDir, + "REQUEST_VALUE=snapshot", + }, + } + cmd := process.Command("snapshot-tool") out, err := cmd.Output() if err != nil { t.Fatal(err) diff --git a/xtool/env/llvm/llvm.go b/xtool/env/llvm/llvm.go index 9b3c1f1a93..678dbd0559 100644 --- a/xtool/env/llvm/llvm.go +++ b/xtool/env/llvm/llvm.go @@ -22,7 +22,6 @@ import ( "os" "os/exec" "path/filepath" - "slices" "sort" "strings" @@ -47,16 +46,20 @@ const ( // checks the LLVM_CONFIG environment variable first, then searches in PATH. If // not found, it returns [ldLLVMConfigBin] as a last resort. func defaultLLVMConfigBin(environ []string, dir string) string { - bin := getenv(environ, "LLVM_CONFIG") + return defaultLLVMConfigBinWithContext(processenv.Context{Env: environ, Dir: dir}) +} + +func defaultLLVMConfigBinWithContext(process processenv.Context) string { + bin := process.Get("LLVM_CONFIG") if bin != "" { return bin } - bin, err := processenv.LookPath(environ, dir, "llvm-config") + bin, err := process.LookPath("llvm-config") if err == nil { return bin } - llgoRoot := env.LLGoROOTWithEnv(environ) + llgoRoot := env.LLGoROOTWithEnv(process.Env) // Check LLGO_ROOT/crosscompile/clang for llvm-config crossLLVMConfigBin := filepath.Join(llgoRoot, CrosscompileClangPath, "bin", "llvm-config") if _, err := os.Stat(crossLLVMConfigBin); err == nil { @@ -70,8 +73,7 @@ func defaultLLVMConfigBin(environ []string, dir string) string { // Env represents an LLVM installation. type Env struct { binDir string - environ []string - dir string + process processenv.Context } // New creates a new [Env] instance. @@ -88,16 +90,21 @@ func NewWithEnv(llvmConfigBin string, environ []string, dirs ...string) *Env { if len(dirs) != 0 { dir = dirs[0] } + return NewWithContext(llvmConfigBin, processenv.Context{Env: environ, Dir: dir}) +} + +// NewWithContext creates an Env using one request execution context. +func NewWithContext(llvmConfigBin string, process processenv.Context) *Env { if llvmConfigBin == "" { - llvmConfigBin = defaultLLVMConfigBin(environ, dir) + llvmConfigBin = defaultLLVMConfigBinWithContext(process) } // Note that an empty binDir is acceptable. In this case, LLVM // executables are assumed to be in PATH. - cmd := processenv.Command(environ, dir, llvmConfigBin, "--bindir") + cmd := process.Command(llvmConfigBin, "--bindir") binDir, _ := cmd.Output() - e := &Env{binDir: strings.TrimSpace(string(binDir)), environ: slices.Clone(environ), dir: dir} + e := &Env{binDir: strings.TrimSpace(string(binDir)), process: process.Clone()} return e } @@ -147,15 +154,15 @@ func (e *Env) Readelf(args ...string) (*exec.Cmd, error) { } func (e *Env) toolPath(base string) (string, error) { - if tool := searchTool(resolveDir(e.binDir, e.dir), base); tool != "" { + if tool := searchTool(resolveDir(e.binDir, e.process.Dir), base); tool != "" { return tool, nil } - if tool, err := processenv.LookPath(e.environ, e.dir, base); err == nil { + if tool, err := e.process.LookPath(base); err == nil { return tool, nil } else if errors.Is(err, exec.ErrDot) { return "", err } - if tool, err := searchToolInPath(e.environ, e.dir, base); tool != "" || err != nil { + if tool, err := searchToolInPath(e.process, base); tool != "" || err != nil { return tool, err } return "", fmt.Errorf("%s not found", base) @@ -180,13 +187,13 @@ func searchTool(dir, base string) string { return "" } -func searchToolInPath(environ []string, workingDir, base string) (string, error) { - for _, dir := range filepath.SplitList(getenv(environ, "PATH")) { +func searchToolInPath(process processenv.Context, base string) (string, error) { + for _, dir := range filepath.SplitList(process.Get("PATH")) { if dir == "" { dir = "." } relative := !filepath.IsAbs(dir) - if tool := searchTool(resolveDir(dir, workingDir), base); tool != "" { + if tool := searchTool(resolveDir(dir, process.Dir), base); tool != "" { if relative { return tool, exec.ErrDot } @@ -197,7 +204,7 @@ func searchToolInPath(environ []string, workingDir, base string) (string, error) } func (e *Env) command(path string, args ...string) *exec.Cmd { - return processenv.Command(e.environ, e.dir, path, args...) + return e.process.Command(path, args...) } func resolveDir(dir, workingDir string) string { @@ -207,13 +214,6 @@ func resolveDir(dir, workingDir string) string { return filepath.Join(workingDir, dir) } -func getenv(environ []string, key string) string { - if environ == nil { - return os.Getenv(key) - } - return processenv.Get(environ, key) -} - func isExecutable(path string) bool { if path == "" { return false From a84578ecaf5ac7053f345fdf35fc07d0b119d2bf Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Tue, 28 Jul 2026 23:31:44 +0800 Subject: [PATCH 7/9] test: cover request process boundaries --- internal/build/build_test.go | 62 ++++++++++++++++++++++ internal/build/cgo_test.go | 71 ++++++++++++++++++++++++++ internal/build/run_test.go | 54 ++++++++++++++++++++ internal/crosscompile/crosscompile.go | 9 ---- internal/processenv/processenv_test.go | 9 ++++ 5 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 internal/build/run_test.go diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 6df41bd01f..95754bd6f0 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -81,6 +81,68 @@ func TestResolveBuildConfigDoesNotAliasInput(t *testing.T) { } } +func TestResolveBuildConfigDefaultsAndValidation(t *testing.T) { + if got := (*Config)(nil).clone(); got != nil { + t.Fatalf("nil Config clone = %#v", got) + } + if _, err := resolveBuildConfig(nil); err == nil { + t.Fatal("nil build config succeeded") + } + + 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 got := (*Config)(nil).llgoRuntimeDir(); got != "" { + t.Fatalf("nil runtime dir = %q", got) + } + if got := (&Config{}).llgoRuntimeDir(); got != "" { + t.Fatalf("empty-root runtime dir = %q", got) + } +} + +func TestBuildHelpersUseResolvedRequestConfiguration(t *testing.T) { + conf := &Config{environment: []string{llgoWasmRuntime + "=request-wasm"}} + if got := WasmRuntimeForConfig(conf); got != "request-wasm" { + t.Fatalf("WasmRuntimeForConfig = %q", got) + } + if got := WasmRuntimeForConfig(&Config{environment: []string{}}); got != defaultWasmRuntime { + t.Fatalf("default WasmRuntimeForConfig = %q", got) + } + + root := t.TempDir() + ctx := &context{ + buildConf: &Config{llgoRoot: root}, + crossCompile: crosscompile.Export{ExtraFiles: []string{"missing.c"}}, + } + if _, err := compileExtraFiles(ctx, false); err == nil { + t.Fatal("missing extra file succeeded") + } + + parent := filepath.Join(t.TempDir(), "not-a-directory") + if err := os.WriteFile(parent, nil, 0o644); err != nil { + t.Fatal(err) + } + ctx.buildConf.BuildMode = BuildModeExe + if err := linkObjFiles(ctx, filepath.Join(parent, "app"), nil, nil, false); err == nil { + t.Fatal("output below regular file succeeded") + } +} + func TestResolveBuildConfigUsesExplicitEnvironment(t *testing.T) { t.Setenv(llgoFuncInfo, "0") t.Setenv(llgoTrace, "0") diff --git a/internal/build/cgo_test.go b/internal/build/cgo_test.go index 3f835ac8f6..474db90146 100644 --- a/internal/build/cgo_test.go +++ b/internal/build/cgo_test.go @@ -11,10 +11,12 @@ import ( "os" "path/filepath" "reflect" + "runtime" "strings" "testing" "github.com/goplus/llgo/internal/packages" + "github.com/goplus/llgo/internal/processenv" ) func TestParseCgoDeclFlags(t *testing.T) { @@ -76,6 +78,75 @@ func TestParseCgoDeclFlags(t *testing.T) { } } +func TestCgoToolsUseRequestContext(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test tools use POSIX shell scripts") + } + dir := t.TempDir() + binDir := filepath.Join(dir, "bin") + if err := os.Mkdir(binDir, 0o755); err != nil { + t.Fatal(err) + } + clang := filepath.Join(binDir, "clang") + clangScript := `#!/bin/sh +case " $* " in + *" -dM "*) printf '#define REQUEST_MACRO 1\n' ;; + *) printf '{"inner":[{"kind":"FunctionDecl","name":"request_func"}]}' ;; +esac +` + if err := os.WriteFile(clang, []byte(clangScript), 0o755); err != nil { + t.Fatal(err) + } + pkgConfig := filepath.Join(binDir, "pkg-config") + pkgConfigScript := `#!/bin/sh +case "$1" in + --libs) printf '%s' '-lrequest' ;; + --cflags) printf '%s' '-I/request/include' ;; + *) exit 2 ;; +esac +` + if err := os.WriteFile(pkgConfig, []byte(pkgConfigScript), 0o755); err != nil { + t.Fatal(err) + } + process := processenv.Context{Dir: dir, Env: []string{"PATH=" + binDir}} + + macros := map[string]bool{} + if err := getMacroNames(process, "request.c", nil, macros, false); err != nil { + t.Fatal(err) + } + if !macros["REQUEST_MACRO"] { + t.Fatalf("macro names = %#v", macros) + } + funcs := map[string]bool{} + if err := getFuncNames(process, "request.c", nil, funcs, false); err != nil { + t.Fatal(err) + } + if !funcs["request_func"] { + t.Fatalf("function names = %#v", funcs) + } + externs, err := genExternDeclsByClang(process, nil, "int request_value;", nil, map[string]string{}, false) + if err != nil { + t.Fatal(err) + } + if externs != "" { + t.Fatalf("unexpected extern declarations: %q", externs) + } + + decls, err := parseCgoDeclWithContext(process, "#cgo pkg-config: request") + if err != nil { + t.Fatal(err) + } + if len(decls) != 1 || + !reflect.DeepEqual(decls[0].cflags, []string{"-I/request/include"}) || + !reflect.DeepEqual(decls[0].ldflags, []string{"-lrequest"}) { + t.Fatalf("pkg-config declarations = %#v", decls) + } + + if _, _, err := parseCgoPreamble(token.Position{Filename: "request.go", Line: 1}, "int value;"); err != nil { + t.Fatal(err) + } +} + func TestCollectCgoSymbolsStripsPackagePrefix(t *testing.T) { externs := []string{ "command-line-arguments._cgo_96608f8de8c8_Cfunc_fputs", diff --git a/internal/build/run_test.go b/internal/build/run_test.go new file mode 100644 index 0000000000..ac34fc0d54 --- /dev/null +++ b/internal/build/run_test.go @@ -0,0 +1,54 @@ +//go:build !llgo && !windows + +package build + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/goplus/llgo/internal/processenv" +) + +func TestRunInEmulatorValidationAndRequestContext(t *testing.T) { + process := processenv.Context{ + Dir: t.TempDir(), + Env: []string{"PATH=" + filepath.Dir("/bin/sh")}, + } + ctx := &context{process: process} + + if err := runInEmulator(ctx, "", nil, "", "", &Config{CompileOnly: true}, ModeRun, false); err != nil { + t.Fatalf("compile-only emulator run failed: %v", err) + } + if err := runInEmulator(ctx, "", nil, "", "", &Config{Target: "demo"}, ModeRun, false); err == nil { + t.Fatal("missing emulator succeeded") + } + for _, mode := range []Mode{ModeRun, ModeTest} { + err := runInEmulator(ctx, "/bin/sh -c 'exit 7'", nil, "", "", &Config{}, mode, false) + if err == nil { + t.Fatalf("emulator mode %v succeeded", mode) + } + } + + if err := runEmuCmd(process, nil, "'", nil, false, false); err == nil || !strings.Contains(err.Error(), "parse") { + t.Fatalf("malformed emulator command error = %v", err) + } + if err := runEmuCmd(process, nil, " ", nil, false, false); err == nil || !strings.Contains(err.Error(), "empty") { + t.Fatalf("empty emulator command error = %v", err) + } +} + +func TestRunNativeWasmUsesConfigEnvironment(t *testing.T) { + conf := &Config{ + Goos: "wasip1", + environment: []string{ + llgoWasmRuntime + "=$REQUEST_WASM_RUNTIME", + "REQUEST_WASM_RUNTIME=/definitely/missing/wasm-runtime", + }, + } + ctx := &context{process: processenv.Context{Dir: t.TempDir(), Env: os.Environ()}} + if err := runNative(ctx, "app.wasm", "", "", conf, ModeRun); err == nil { + t.Fatal("missing configured wasm runtime succeeded") + } +} diff --git a/internal/crosscompile/crosscompile.go b/internal/crosscompile/crosscompile.go index 8e1ba61f3b..1b1b38bfb0 100644 --- a/internal/crosscompile/crosscompile.go +++ b/internal/crosscompile/crosscompile.go @@ -725,15 +725,6 @@ func Use(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level return UseWithContext(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, processenv.Context{}, env.LLGoROOT()) } -// UseWithEnv selects a toolchain using explicit process inputs. Download and -// cache operations remain protected by their existing cross-process locks. -func UseWithEnv(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, environ []string, dir, llgoRoot string) (export Export, err error) { - return UseWithContext(goos, goarch, targetName, wasiThreads, forceEspClang, level, ltoMode, goGlobalDCE, processenv.Context{ - Env: environ, - Dir: dir, - }, llgoRoot) -} - // UseWithContext selects a toolchain using explicit process context. Download // and cache operations remain protected by their existing cross-process locks. func UseWithContext(goos, goarch, targetName string, wasiThreads, forceEspClang bool, level optlevel.Level, ltoMode lto.Mode, goGlobalDCE bool, process processenv.Context, llgoRoot string) (export Export, err error) { diff --git a/internal/processenv/processenv_test.go b/internal/processenv/processenv_test.go index 253b86468c..5effae93ac 100644 --- a/internal/processenv/processenv_test.go +++ b/internal/processenv/processenv_test.go @@ -25,6 +25,15 @@ func TestCaptureClonesExplicitInputs(t *testing.T) { if got := process.Abs("out.o"); got != filepath.Join(workDir, "out.o") { t.Fatalf("Abs(out.o) = %q", got) } + if got := process.Abs(""); got != "" { + t.Fatalf("Abs(empty) = %q", got) + } + if got := process.Abs(workDir); got != workDir { + t.Fatalf("Abs(absolute) = %q", got) + } + if got, ok := process.Lookup("KEY"); !ok || got != "value" { + t.Fatalf("Lookup(KEY) = %q, %v, want value, true", got, ok) + } clone := process.Clone() clone.Env[0] = "KEY=clone" From 522176e6a1707385aea2c61c006a358d5530e572 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Wed, 29 Jul 2026 14:53:38 +0800 Subject: [PATCH 8/9] build: preserve isolation across main updates --- internal/build/build.go | 25 ++++++++++++++++++++++--- internal/build/build_test.go | 19 +++++++++++++++++++ ssa/eh.go | 3 +++ ssa/eh_defer_test.go | 9 ++++++++- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/internal/build/build.go b/internal/build/build.go index cf863ee615..888fd53328 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -376,7 +376,26 @@ const ( loadSyntax = loadTypes | packages.NeedSyntax | packages.NeedTypesInfo ) -var llssaInitOnce sync.Once +var ( + llssaInitOnce sync.Once + rewriteMainPrefixMu sync.RWMutex +) + +// lockRewriteMainPrefix contains the legacy process-global ABI switch until it +// can become a Program option. Ordinary builds share the default false state; +// a rewrite-enabled build has exclusive ownership and restores the default. +func lockRewriteMainPrefix(enabled bool) func() { + if !enabled { + rewriteMainPrefixMu.RLock() + return rewriteMainPrefixMu.RUnlock + } + rewriteMainPrefixMu.Lock() + abi.SetRewriteMainPrefix(true) + return func() { + abi.SetRewriteMainPrefix(false) + rewriteMainPrefixMu.Unlock() + } +} func Do(args []string, conf *Config) ([]Package, error) { return Build(BuildRequest{Args: args, Config: conf}) @@ -394,6 +413,8 @@ func Build(req BuildRequest) ([]Package, error) { if err != nil { return nil, err } + unlockRewriteMainPrefix := lockRewriteMainPrefix(conf.RewriteMainPrefix) + defer unlockRewriteMainPrefix() // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" export, err := crosscompile.UseWithContext(conf.Goos, conf.Goarch, conf.Target, isEnvOnConfig(conf, llgoWasiThreads, false), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), process, conf.llgoRoot) @@ -442,8 +463,6 @@ func Build(req BuildRequest) ([]Package, error) { if conf.Mode == ModeTest { cfg.Mode |= packages.NeedForTest } - abi.SetRewriteMainPrefix(conf.RewriteMainPrefix) - emitDebugInfo := shouldEmitDebugInfo(conf, &export) frontendOptions := cl.Options{ Debug: emitDebugInfo, diff --git a/internal/build/build_test.go b/internal/build/build_test.go index 95754bd6f0..f96f59efa4 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -32,6 +32,7 @@ import ( "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" llssa "github.com/goplus/llgo/ssa" + "github.com/goplus/llgo/ssa/abi" "github.com/xgo-dev/llvm" ) @@ -45,6 +46,24 @@ func TestMain(m *testing.M) { os.Exit(code) } +func TestLockRewriteMainPrefixRestoresDefault(t *testing.T) { + pkg := types.NewPackage("example.com/main", "main") + func() { + unlock := lockRewriteMainPrefix(true) + defer unlock() + if got := abi.PathOf(pkg); got != "main" { + t.Fatalf("PathOf() with rewrite enabled = %q, want main", got) + } + }() + func() { + unlock := lockRewriteMainPrefix(false) + defer unlock() + if got := abi.PathOf(pkg); got != "example.com/main" { + t.Fatalf("PathOf() after rewrite release = %q, want example.com/main", got) + } + }() +} + func TestResolveBuildConfigDoesNotAliasInput(t *testing.T) { input := &Config{ RunArgs: []string{"run"}, diff --git a/ssa/eh.go b/ssa/eh.go index b8ead4eb64..2991fdef67 100644 --- a/ssa/eh.go +++ b/ssa/eh.go @@ -591,6 +591,9 @@ func (p Function) endDefer(b Builder) { b.Jump(rethNext) } } + if n == 0 { + b.SetBlockEx(procBlk, AtEnd, true) + } link := b.getField(b.Load(self.data), deferLink) b.Call(b.Pkg.rtFunc("SetThreadDefer"), link) b.IndirectJump(b.Load(rundPtr), nexts) diff --git a/ssa/eh_defer_test.go b/ssa/eh_defer_test.go index 5f99729b1e..bcfbed33ee 100644 --- a/ssa/eh_defer_test.go +++ b/ssa/eh_defer_test.go @@ -9,6 +9,7 @@ import ( "github.com/goplus/llgo/ssa" "github.com/goplus/llgo/ssa/ssatest" + "github.com/xgo-dev/llvm" ) func TestExplicitDeferStackIR(t *testing.T) { @@ -73,7 +74,10 @@ func TestExplicitDeferStackDrainWithoutLoopCases(t *testing.T) { fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo) b := fn.MakeBody(1) - fn.SetRecover(fn.MakeBlock()) + recoverBlock := fn.MakeBlock() + fn.SetRecover(recoverBlock) + b.SetBlock(recoverBlock).Return() + b.SetBlock(fn.Block(0)) _ = b.BuiltinCall("ssa:deferstack") b.DeferStackDrain() @@ -88,6 +92,9 @@ func TestExplicitDeferStackDrainWithoutLoopCases(t *testing.T) { if !strings.Contains(ir, "sigsetjmp") && !strings.Contains(ir, "setjmp") { t.Fatalf("expected defer stack setup with recover, got:\n%s", ir) } + if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("explicit defer stack without loop cases produced invalid IR: %v\n%s", err, ir) + } } func TestExplicitDeferStackDrainWithoutRecoverNoop(t *testing.T) { From 8cc5d13593afc06ebc749232d6ce4a8faa0df4f4 Mon Sep 17 00:00:00 2001 From: ZhouGuangyuan Date: Wed, 29 Jul 2026 16:40:08 +0800 Subject: [PATCH 9/9] ssa: scope main prefix rewriting to programs --- cl/compile.go | 16 +++---- cl/import.go | 29 ++++++++---- cl/instr.go | 2 +- cl/locality.go | 8 ++-- cl/locality_lower.go | 14 +++--- cl/static_init.go | 2 +- internal/build/build.go | 32 +++---------- internal/build/build_test.go | 72 +++++++++++++++++++++-------- ssa/abi/abi.go | 90 +++++++++++++++++++++--------------- ssa/abi/abi_test.go | 8 ++-- ssa/abi/type.go | 12 +++-- ssa/abitype.go | 20 ++++---- ssa/decl.go | 2 +- ssa/globaldce.go | 14 ++++-- ssa/interface.go | 4 +- ssa/package.go | 5 +- ssa/ssa_test.go | 33 +++++++++++++ ssa/target.go | 11 +++-- ssa/type.go | 43 ++++++++++++++--- 19 files changed, 270 insertions(+), 147 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index f1c104cca5..fb6d413c14 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -390,7 +390,7 @@ func (p *context) compileType(pkg llssa.Package, t *ssa.Type) { } tnName := tn.Name() typ := tn.Type() - name := llssa.FullName(tn.Pkg(), tnName) + name := p.prog.FullName(tn.Pkg(), tnName) dbgInstrln("==> NewType", name, typ) p.compileMethods(pkg, typ) p.compileMethods(pkg, types.NewPointer(typ)) @@ -611,7 +611,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun if p.prog.FuncInfoMetadataEnabled() { goName := fn.Name() if pkgTypes != nil { - goName = funcName(pkgTypes, f, false) + goName = funcNameWithProgram(p.prog, pkgTypes, f, false) } pos := p.funcInfoPosition(f) pkg.EmitFuncInfo(fn.Name(), funcInfoDisplayName(pkgTypes, goName), pos.Filename, pos.Line, pos.Column) @@ -2119,7 +2119,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes - pkgName, pkgPath := pkgTypes.Name(), llssa.PathOf(pkgTypes) + pkgName, pkgPath := pkgTypes.Name(), prog.PathOf(pkgTypes) patch, hasPatch := patches[pkgPath] if hasPatch { pkgTypes = patch.Types @@ -2129,7 +2129,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri if err = ParsePkgSyntax(prog, pkgProg.Fset, pkgTypes, files); err != nil { return nil, nil, err } - if err = prog.ValidateLocalities(llssa.PathOf(pkgTypes)); err != nil { + if err = prog.ValidateLocalities(prog.PathOf(pkgTypes)); err != nil { return nil, nil, err } if err = validateLocalInitializers(prog, pkgTypes); err != nil { @@ -2452,7 +2452,7 @@ func (p *context) typeArgName(t types.Type) string { case *types.Named: name := p.localNamedName(t, p.isLocalType(t.Obj())) if pkg := t.Obj().Pkg(); pkg != nil { - return reflectTypeArgPkgPath(pkg) + "." + name + return p.reflectTypeArgPkgPath(pkg) + "." + name } return name case *types.Pointer: @@ -2473,7 +2473,7 @@ func (p *context) typeArgName(t types.Type) string { } return fmt.Sprintf("%s %s", s, elem) default: - return types.TypeString(t, reflectTypeArgPkgPath) + return types.TypeString(t, p.reflectTypeArgPkgPath) } } @@ -2490,14 +2490,14 @@ func chanDirName(dir types.ChanDir) string { } } -func reflectTypeArgPkgPath(pkg *types.Package) string { +func (p *context) reflectTypeArgPkgPath(pkg *types.Package) string { if pkg == nil { return "" } if pkg.Path() == "command-line-arguments" && pkg.Name() != "" { return pkg.Name() } - return llssa.PathOf(pkg) + return p.prog.PathOf(pkg) } func (p *context) isGenericLocalType(obj types.Object) bool { diff --git a/cl/import.go b/cl/import.go index 907cf4d881..0960b0296d 100644 --- a/cl/import.go +++ b/cl/import.go @@ -131,7 +131,7 @@ func pkgKindByScope(scope *types.Scope) (int, string) { } func (p *context) importPkg(pkg *types.Package, i *pkgInfo) { - pkgPath := llssa.PathOf(pkg) + pkgPath := p.prog.PathOf(pkg) scope := pkg.Scope() kind, _ := pkgKindByScope(scope) if kind == PkgNormal { @@ -446,6 +446,10 @@ func typesFuncName(pkgPath string, fn *types.Func) (fullName, inPkgName string) // - func: pkg.name // - method: pkg.(T).name, pkg.(*T).name func funcName(pkg *types.Package, fn *ssa.Function, org bool) string { + return funcNameWithProgram(nil, pkg, fn, org) +} + +func funcNameWithProgram(prog llssa.Program, pkg *types.Package, fn *ssa.Function, org bool) string { // Closures in methods can be nested (closure inside closure inside method). // Walking only one Parent() loses the receiver for deeper nests, producing // names like "pkg.marshal$1$1" that can collide across receiver types. @@ -472,11 +476,18 @@ func funcName(pkg *types.Package, fn *ssa.Function, org bool) string { if org := fn.Origin(); org != nil { fnName = org.Name() if fn.Signature.Recv() == nil { - fnName += llssa.TypeArgs(fn.TypeArgs()) + if prog != nil { + fnName += prog.TypeArgs(fn.TypeArgs()) + } else { + fnName += llssa.TypeArgs(fn.TypeArgs()) + } } } else { fnName = fn.Name() } + if prog != nil { + return prog.FuncName(pkg, fnName, recv, org) + } return llssa.FuncName(pkg, fnName, recv, org) } @@ -621,7 +632,7 @@ func (p *context) funcName(fn *ssa.Function) (*types.Package, string, int) { if origin := fn.Origin(); origin != nil { pkg = origin.Pkg.Pkg p.ensureLoaded(pkg) - orgName = funcName(pkg, origin, true) + orgName = funcNameWithProgram(p.prog, pkg, origin, true) } else { fname := fn.Name() if checkCgo(fname) && !cgoIgnored(fname) { @@ -647,7 +658,7 @@ func (p *context) funcName(fn *ssa.Function) (*types.Package, string, int) { pkg = p.goTyps } p.ensureLoaded(pkg) - orgName = funcName(pkg, fn, false) + orgName = funcNameWithProgram(p.prog, pkg, fn, false) } if v, ok := p.prog.Linkname(orgName); ok { if strings.HasPrefix(v, "C.") { @@ -672,7 +683,7 @@ func (p *context) funcName(fn *ssa.Function) (*types.Package, string, int) { if instr, ok := syncAtomicIntrinsicMap[orgName]; ok { return nil, instr, llgoInstr } - return pkg, funcName(pkg, fn, false), goFunc + return pkg, funcNameWithProgram(p.prog, pkg, fn, false), goFunc } const ( @@ -683,12 +694,12 @@ const ( ) func (p *context) varName(pkg *types.Package, v *ssa.Global) (vName string, vtype int, define bool) { - name := llssa.FullName(pkg, v.Name()) + name := p.prog.FullName(pkg, v.Name()) // TODO(lijie): need a bettery way to process linkname (maybe alias) if !isCgoCfpvar(v.Name()) && !isCgoVar(v.Name()) { if v, ok := p.prog.Linkname(name); ok { if strings.HasPrefix(v, "go:") { - if llssa.PathOf(pkg) == "runtime" { + if p.prog.PathOf(pkg) == "runtime" { return v, goVar, true } return v, goVar, false @@ -761,7 +772,7 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, return nil } ctx := &context{prog: prog} - pkgPath := llssa.PathOf(pkg) + pkgPath := prog.PathOf(pkg) for _, file := range files { for _, decl := range file.Decls { switch decl := decl.(type) { @@ -788,7 +799,7 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package, return err } for _, variable := range vars { - prog.SetLocalityInfo(llssa.FullName(pkg, variable.Name), variable.Info) + prog.SetLocalityInfo(prog.FullName(pkg, variable.Name), variable.Info) } continue } diff --git a/cl/instr.go b/cl/instr.go index 7e091eee82..fa139f2126 100644 --- a/cl/instr.go +++ b/cl/instr.go @@ -1473,7 +1473,7 @@ func (p *context) runtimeCallerFrameName() string { return "" } if p.goFn != nil && p.goFn.Pkg != nil && p.goFn.Pkg.Pkg != nil { - return runtimeFrameName(funcName(p.goFn.Pkg.Pkg, p.goFn, false)) + return runtimeFrameName(funcNameWithProgram(p.prog, p.goFn.Pkg.Pkg, p.goFn, false)) } if p.fn != nil { return runtimeFrameName(p.fn.Name()) diff --git a/cl/locality.go b/cl/locality.go index d9c561ae39..65664ad79d 100644 --- a/cl/locality.go +++ b/cl/locality.go @@ -35,13 +35,13 @@ func PrepareLocalVariables(prog llssa.Program, fset *token.FileSet, pkg *types.P if pkg == nil || info == nil { return nil } - path := llssa.PathOf(pkg) + path := prog.PathOf(pkg) prepared, err := locality.Prepare(fset, path, pkg, info, files, packageLocalities(prog, path)) if err != nil { return err } for name, local := range prepared { - prog.SetLocalityInfo(llssa.FullName(pkg, name), local) + prog.SetLocalityInfo(prog.FullName(pkg, name), local) } for fullName := range prog.PackageLocalities(path) { name := strings.TrimPrefix(fullName, path+".") @@ -60,7 +60,7 @@ func PrepareLocalVariables(prog llssa.Program, fset *token.FileSet, pkg *types.P } func validateLocalInitializers(prog llssa.Program, pkg *types.Package) error { - return locality.ValidatePrepared(llssa.PathOf(pkg), packageLocalities(prog, llssa.PathOf(pkg))) + return locality.ValidatePrepared(prog.PathOf(pkg), packageLocalities(prog, prog.PathOf(pkg))) } func packageLocalities(prog llssa.Program, pkgPath string) map[string]locality.Info { @@ -76,7 +76,7 @@ func planLocalPackage(prog llssa.Program, pkg *types.Package) (localitylayout.Pa if pkg == nil { return localitylayout.Package{}, nil } - path := llssa.PathOf(pkg) + path := prog.PathOf(pkg) prefix := path + "." decls := prog.PackageLocalities(path) input := make([]localitylayout.Declaration, 0, len(decls)) diff --git a/cl/locality_lower.go b/cl/locality_lower.go index 7fbf599e64..6fdf3d5626 100644 --- a/cl/locality_lower.go +++ b/cl/locality_lower.go @@ -88,7 +88,7 @@ func (p *context) prepareExportedLocalContext(f *ssa.Function) { if !p.prog.NeedsLocalContext() || f == nil || f.Pkg == nil { return } - fullName := funcName(f.Pkg.Pkg, f, false) + fullName := funcNameWithProgram(p.prog, f.Pkg.Pkg, f, false) if _, exported := p.pkg.ExportFuncs()[fullName]; !exported { return } @@ -137,7 +137,7 @@ func (p *context) prepareLocalVariables(pkg llssa.Package, globals []*ssa.Global // plan. Both definitions and references use this path so forbidden linkname // aliases and layout validation cannot diverge between lowering cases. func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, defineCurrent bool) (*localVariable, bool, error) { - fullName := llssa.FullName(global.Pkg.Pkg, global.Name()) + fullName := p.prog.FullName(global.Pkg.Pkg, global.Name()) canonical, info, ok, err := p.prog.ResolveLocality(fullName) if err != nil { return nil, false, err @@ -161,7 +161,7 @@ func (p *context) localVariableFor(pkg llssa.Package, global *ssa.Global, define } func (p *context) localityGlobalStorage(pkg llssa.Package, global *ssa.Global, name string, typ types.Type, bg llssa.Background) (llssa.Global, bool) { - info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())) + info, ok := p.resolveLocality(p.prog.FullName(global.Pkg.Pkg, global.Name())) if !ok || info.Locality == locality.None { return pkg.NewVar(name, typ, bg), false } @@ -185,7 +185,7 @@ func (p *context) localTypesPackage(fullName string) *types.Package { if pkg == nil { return false } - prefix := llssa.PathOf(pkg) + "." + prefix := p.prog.PathOf(pkg) + "." if !strings.HasPrefix(fullName, prefix) { return false } @@ -215,7 +215,7 @@ func (p *context) localPackageFor(typesPkg *types.Package, pkg llssa.Package, de if typesPkg == nil { return nil, nil } - path := llssa.PathOf(typesPkg) + path := p.prog.PathOf(typesPkg) if owner := p.locality.packages[path]; owner != nil { return owner, nil } @@ -352,7 +352,7 @@ func (p *context) localVariableAddr(b llssa.Builder, v *ssa.Global, info llssa.V } func (p *context) localVariableAddress(b llssa.Builder, variable *ssa.Global, name string) (llssa.Expr, bool) { - info, ok := p.resolveLocality(llssa.FullName(variable.Pkg.Pkg, variable.Name())) + info, ok := p.resolveLocality(p.prog.FullName(variable.Pkg.Pkg, variable.Name())) if !ok || info.Locality == locality.None { return llssa.Expr{}, false } @@ -405,7 +405,7 @@ func (p *context) ensureLocalInitializer(b llssa.Builder, owner *localPackage, k } func (p *context) initializeLocalGuards(b llssa.Builder) { - owner := p.locality.packages[llssa.PathOf(p.goTyps)] + owner := p.locality.packages[p.prog.PathOf(p.goTyps)] if owner == nil { return } diff --git a/cl/static_init.go b/cl/static_init.go index fd0f238151..a330389eb7 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -85,7 +85,7 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { if _, rewritten := p.rewriteValue(globalName); rewritten { continue } - if info, ok := p.resolveLocality(llssa.FullName(global.Pkg.Pkg, global.Name())); ok && info.Locality != llssa.LocalityNone { + if info, ok := p.resolveLocality(p.prog.FullName(global.Pkg.Pkg, global.Name())); ok && info.Locality != llssa.LocalityNone { // Local initializers must remain executable so they can populate the // current context rather than a process-wide LLVM initializer. continue diff --git a/internal/build/build.go b/internal/build/build.go index 888fd53328..c663f295b5 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -376,26 +376,7 @@ const ( loadSyntax = loadTypes | packages.NeedSyntax | packages.NeedTypesInfo ) -var ( - llssaInitOnce sync.Once - rewriteMainPrefixMu sync.RWMutex -) - -// lockRewriteMainPrefix contains the legacy process-global ABI switch until it -// can become a Program option. Ordinary builds share the default false state; -// a rewrite-enabled build has exclusive ownership and restores the default. -func lockRewriteMainPrefix(enabled bool) func() { - if !enabled { - rewriteMainPrefixMu.RLock() - return rewriteMainPrefixMu.RUnlock - } - rewriteMainPrefixMu.Lock() - abi.SetRewriteMainPrefix(true) - return func() { - abi.SetRewriteMainPrefix(false) - rewriteMainPrefixMu.Unlock() - } -} +var llssaInitOnce sync.Once func Do(args []string, conf *Config) ([]Package, error) { return Build(BuildRequest{Args: args, Config: conf}) @@ -413,8 +394,6 @@ func Build(req BuildRequest) ([]Package, error) { if err != nil { return nil, err } - unlockRewriteMainPrefix := lockRewriteMainPrefix(conf.RewriteMainPrefix) - defer unlockRewriteMainPrefix() // Handle crosscompile configuration first to set correct GOOS/GOARCH forceEspClang := conf.ForceEspClang || conf.Target != "" export, err := crosscompile.UseWithContext(conf.Goos, conf.Goarch, conf.Target, isEnvOnConfig(conf, llgoWasiThreads, false), forceEspClang, conf.OptLevel, conf.ltoMode(), conf.goGlobalDCEEnabled(), process, conf.llgoRoot) @@ -476,10 +455,11 @@ func Build(req BuildRequest) ([]Package, error) { }) target := &llssa.Target{ - GOOS: conf.Goos, - GOARCH: conf.Goarch, - Target: conf.Target, - OptLevel: conf.OptLevel, + GOOS: conf.Goos, + GOARCH: conf.Goarch, + Target: conf.Target, + OptLevel: conf.OptLevel, + RewriteMainPrefix: conf.RewriteMainPrefix, } prog := llssa.NewProgram(target) diff --git a/internal/build/build_test.go b/internal/build/build_test.go index f96f59efa4..e72a6b7478 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -32,7 +32,6 @@ import ( "github.com/goplus/llgo/internal/mockable" "github.com/goplus/llgo/internal/packages" llssa "github.com/goplus/llgo/ssa" - "github.com/goplus/llgo/ssa/abi" "github.com/xgo-dev/llvm" ) @@ -46,24 +45,6 @@ func TestMain(m *testing.M) { os.Exit(code) } -func TestLockRewriteMainPrefixRestoresDefault(t *testing.T) { - pkg := types.NewPackage("example.com/main", "main") - func() { - unlock := lockRewriteMainPrefix(true) - defer unlock() - if got := abi.PathOf(pkg); got != "main" { - t.Fatalf("PathOf() with rewrite enabled = %q, want main", got) - } - }() - func() { - unlock := lockRewriteMainPrefix(false) - defer unlock() - if got := abi.PathOf(pkg); got != "example.com/main" { - t.Fatalf("PathOf() after rewrite release = %q, want example.com/main", got) - } - }() -} - func TestResolveBuildConfigDoesNotAliasInput(t *testing.T) { input := &Config{ RunArgs: []string{"run"}, @@ -1384,6 +1365,59 @@ func TestConcurrentBuildRequestsKeepInputsIsolated(t *testing.T) { } } +func TestConcurrentBuildRequestsIsolateRewriteMainPrefix(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/rewrite\n\ngo 1.24\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\n\nfunc F() {}\n"), 0o644); err != nil { + t.Fatal(err) + } + baseEnv := withEnv(os.Environ(), llgoBuildCache+"=0") + + type result struct { + rewrite bool + pkgs []Package + err error + } + results := make(chan result, 2) + for _, rewrite := range []bool{false, true} { + conf := NewDefaultConf(ModeGen) + conf.RewriteMainPrefix = rewrite + go func() { + pkgs, err := Build(BuildRequest{ + Args: []string{"."}, + Config: conf, + Dir: dir, + Env: baseEnv, + }) + results <- result{rewrite: rewrite, pkgs: pkgs, err: err} + }() + } + var built []result + for range 2 { + got := <-results + if got.err != nil { + t.Fatal(got.err) + } + if len(got.pkgs) != 1 || got.pkgs[0].LPkg == nil { + t.Fatalf("Build returned packages = %+v, want one compiled package", got.pkgs) + } + t.Cleanup(got.pkgs[0].LPkg.Prog.Dispose) + built = append(built, got) + } + for _, got := range built { + ir := got.pkgs[0].LPkg.String() + want, notWant := "example.com/rewrite.F", "main.F" + if got.rewrite { + want, notWant = notWant, want + } + if !strings.Contains(ir, want) || strings.Contains(ir, notWant) { + t.Fatalf("RewriteMainPrefix=%v produced unexpected symbols; want %q and not %q:\n%s", got.rewrite, want, notWant, ir) + } + } +} + func TestBuildRequestUsesExplicitWorkingDirectory(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 { diff --git a/ssa/abi/abi.go b/ssa/abi/abi.go index f178cecb54..633f792828 100644 --- a/ssa/abi/abi.go +++ b/ssa/abi/abi.go @@ -135,6 +135,7 @@ type Builder struct { buf []byte PtrSize uintptr Sizes types.Sizes + Namer Namer } // New creates a new ABI type Builder. @@ -172,7 +173,7 @@ func (b *Builder) TypeName(t types.Type) (ret string, pub bool) { o := t.Obj() pkg := o.Pkg() ids := scopeIndices(o) - return "_llgo_" + FullName(pkg, NamedName(t)+ids), (pkg == nil || o.Exported() && ids == "") + return "_llgo_" + b.Namer.FullName(pkg, b.Namer.NamedName(t)+ids), (pkg == nil || o.Exported() && ids == "") case *types.Interface: if t.Empty() { return "_llgo_any", true @@ -201,33 +202,47 @@ func (b *Builder) TypeName(t types.Type) (ret string, pub bool) { return } -func NamedName(t *types.Named) string { +// Namer owns the package-path policy used to construct symbols for one +// compilation. +type Namer struct { + RewriteMainPrefix bool +} + +func (n Namer) NamedName(t *types.Named) string { if targs := t.TypeArgs(); targs != nil { - n := targs.Len() - infos := make([]string, n) - for i := 0; i < n; i++ { - infos[i] = typeArgString(targs.At(i)) + count := targs.Len() + infos := make([]string, count) + for i := 0; i < count; i++ { + infos[i] = n.typeArgString(targs.At(i)) } return t.Obj().Name() + "[" + strings.Join(infos, ",") + "]" } return t.Obj().Name() } -func TypeArgs(typeArgs []types.Type) string { +func NamedName(t *types.Named) string { + return (Namer{}).NamedName(t) +} + +func (n Namer) TypeArgs(typeArgs []types.Type) string { targs := make([]string, len(typeArgs)) for i, t := range typeArgs { - targs[i] = typeArgString(t) + targs[i] = n.typeArgString(t) } return "[" + strings.Join(targs, ",") + "]" } -func namedLikeTypeArgString(obj types.Object, targs *types.TypeList) string { +func TypeArgs(typeArgs []types.Type) string { + return (Namer{}).TypeArgs(typeArgs) +} + +func (n Namer) namedLikeTypeArgString(obj types.Object, targs *types.TypeList) string { name := obj.Name() if targs != nil { - n := targs.Len() - infos := make([]string, n) - for i := 0; i < n; i++ { - infos[i] = typeArgString(targs.At(i)) + count := targs.Len() + infos := make([]string, count) + for i := 0; i < count; i++ { + infos[i] = n.typeArgString(targs.At(i)) } name += "[" + strings.Join(infos, ",") + "]" } @@ -235,31 +250,35 @@ func namedLikeTypeArgString(obj types.Object, targs *types.TypeList) string { // tests). Disambiguate them in symbol names using stable scope indices. name += scopeIndices(obj) if pkg := obj.Pkg(); pkg != nil { - return PathOf(pkg) + "." + name + return n.PathOf(pkg) + "." + name } return name } -func typeArgString(t types.Type) string { +func namedLikeTypeArgString(obj types.Object, targs *types.TypeList) string { + return (Namer{}).namedLikeTypeArgString(obj, targs) +} + +func (n Namer) typeArgString(t types.Type) string { switch t := t.(type) { case *types.Alias: - return typeArgString(types.Unalias(t)) + return n.typeArgString(types.Unalias(t)) case *types.Basic: return t.String() case *types.Named: - return namedLikeTypeArgString(t.Obj(), t.TypeArgs()) + return n.namedLikeTypeArgString(t.Obj(), t.TypeArgs()) case *types.Pointer: - return "*" + typeArgString(t.Elem()) + return "*" + n.typeArgString(t.Elem()) case *types.Slice: - return "[]" + typeArgString(t.Elem()) + return "[]" + n.typeArgString(t.Elem()) case *types.Array: - return fmt.Sprintf("[%v]%s", t.Len(), typeArgString(t.Elem())) + return fmt.Sprintf("[%v]%s", t.Len(), n.typeArgString(t.Elem())) case *types.Map: - return fmt.Sprintf("map[%s]%s", typeArgString(t.Key()), typeArgString(t.Elem())) + return fmt.Sprintf("map[%s]%s", n.typeArgString(t.Key()), n.typeArgString(t.Elem())) case *types.Chan: _, s := ChanDir(t.Dir()) elem := t.Elem() - elemStr := typeArgString(elem) + elemStr := n.typeArgString(elem) // Keep canonical channel formatting for nested directional channels. // Example: chan (<-chan int), not "chan <-chan int" (ambiguous). if t.Dir() == types.SendRecv { @@ -271,7 +290,7 @@ func typeArgString(t types.Type) string { default: // Fallback for rare type arguments (e.g. signature/interface/struct). // Collisions are mainly caused by local named types, handled above. - return types.TypeString(t, PathOf) + return types.TypeString(t, n.PathOf) } } @@ -279,32 +298,31 @@ const ( PatchPathPrefix = env.LLGoRuntimePkg + "/internal/lib/" ) -// SetRewriteMainPrefix controls whether symbols in the main package -// use "main." as their package path prefix instead of the actual -// import path. When true, pkgpath.sym is rewritten to main.sym. -func SetRewriteMainPrefix(b bool) { - rewriteMainPrefix = b -} - -var rewriteMainPrefix bool - // PathOf returns the package path of the specified package. -func PathOf(pkg *types.Package) string { +func (n Namer) PathOf(pkg *types.Package) string { if pkg == nil { return "" } - if rewriteMainPrefix && pkg.Name() == "main" { + if n.RewriteMainPrefix && pkg.Name() == "main" { return "main" } return strings.TrimPrefix(pkg.Path(), PatchPathPrefix) } +func PathOf(pkg *types.Package) string { + return (Namer{}).PathOf(pkg) +} + // FullName returns the full name of a package member. -func FullName(pkg *types.Package, name string) string { +func (n Namer) FullName(pkg *types.Package, name string) string { if pkg == nil { return name } - return PathOf(pkg) + "." + name + return n.PathOf(pkg) + "." + name +} + +func FullName(pkg *types.Package, name string) string { + return (Namer{}).FullName(pkg, name) } // BasicName returns the ABI type name for the specified basic type. diff --git a/ssa/abi/abi_test.go b/ssa/abi/abi_test.go index bc0a0b93ea..0ba6035fd1 100644 --- a/ssa/abi/abi_test.go +++ b/ssa/abi/abi_test.go @@ -413,9 +413,11 @@ func TestRewriteMainPrefix(t *testing.T) { if path := abi.PathOf(pkg); path != "example.com/foo/pkg" { t.Fatalf("error %v", path) } - abi.SetRewriteMainPrefix(true) - if path := abi.PathOf(pkg); path != "main" { + namer := abi.Namer{RewriteMainPrefix: true} + if path := namer.PathOf(pkg); path != "main" { t.Fatalf("error %v", path) } - abi.SetRewriteMainPrefix(false) + if path := abi.PathOf(pkg); path != "example.com/foo/pkg" { + t.Fatalf("default namer changed to %v", path) + } } diff --git a/ssa/abi/type.go b/ssa/abi/type.go index d3b072b82b..100c7fa590 100644 --- a/ssa/abi/type.go +++ b/ssa/abi/type.go @@ -120,7 +120,7 @@ func (b *Builder) reflectTypeArgBaseString(t types.Type) string { case *types.Named: name := b.namedStr(t) if pkg := t.Obj().Pkg(); pkg != nil { - return reflectTypeArgPkgPath(pkg) + "." + name + return b.reflectTypeArgPkgPath(pkg) + "." + name } return name case *types.Interface: @@ -141,17 +141,21 @@ func (b *Builder) reflectTypeArgBaseString(t types.Type) string { _, s := ChanDir(t.Dir()) return s + " " + b.reflectTypeArgString(t.Elem()) } - return types.TypeString(t, reflectTypeArgPkgPath) + return types.TypeString(t, b.reflectTypeArgPkgPath) } -func reflectTypeArgPkgPath(pkg *types.Package) string { +func (b *Builder) reflectTypeArgPkgPath(pkg *types.Package) string { if pkg == nil { return "" } if pkg.Path() == "command-line-arguments" && pkg.Name() != "" { return pkg.Name() } - return PathOf(pkg) + return b.Namer.PathOf(pkg) +} + +func reflectTypeArgPkgPath(pkg *types.Package) string { + return (&Builder{}).reflectTypeArgPkgPath(pkg) } func (b *Builder) structStr(t *types.Struct) string { diff --git a/ssa/abitype.go b/ssa/abitype.go index 8d5e728c59..9b1c85bfca 100644 --- a/ssa/abitype.go +++ b/ssa/abitype.go @@ -214,7 +214,7 @@ func (b Builder) abiInterfaceImethods(t *types.Interface, name string) llvm.Valu var values []llvm.Value name := f.Name() if !token.IsExported(name) { - name = abi.FullName(f.Pkg(), name) + name = prog.FullName(f.Pkg(), name) } values = append(values, b.Str(name).impl) ftyp := funcType(prog, f.Type()) @@ -320,7 +320,7 @@ func (b Builder) abiExtendedFields(t types.Type, name string, global llvm.Value) for i := 0; i < n; i++ { if f := t.Field(i); !f.Exported() { if pkg := f.Pkg(); pkg != nil { - pkgPath = reflectPkgPath(pkg) + pkgPath = prog.reflectPkgPath(pkg) break } } @@ -405,19 +405,19 @@ retry: goto retry case *types.Named: pkg := typ.Obj().Pkg() - return pkg, reflectPkgPath(pkg) + return pkg, b.Prog.reflectPkgPath(pkg) } return nil, b.Pkg.Path() } -func reflectPkgPath(pkg *types.Package) string { +func (p Program) reflectPkgPath(pkg *types.Package) string { if pkg == nil { return "" } if pkg.Path() == "command-line-arguments" && pkg.Name() != "" { return pkg.Name() } - return abi.PathOf(pkg) + return p.PathOf(pkg) } func (b Builder) abiUncommonMethodSet(t types.Type) (mset *types.MethodSet, ok bool) { @@ -498,7 +498,7 @@ func (b Builder) abiUncommonMethods(t types.Type, methods []*types.Selection) ll m := methods[i] obj := m.Obj() mName := obj.Name() - fullName := abiMethodName(obj) + fullName := prog.abiMethodName(obj) name := b.Str(fullName).impl mSig := m.Type().(*types.Signature) var tfn, ifn llvm.Value @@ -546,12 +546,12 @@ func funcType(prog Program, typ types.Type) types.Type { return ftyp.raw.Type.(*types.Struct).Field(0).Type() } -func abiMethodName(obj types.Object) string { +func (p Program) abiMethodName(obj types.Object) string { name := obj.Name() if token.IsExported(name) { return name } - return abi.FullName(obj.Pkg(), name) + return p.FullName(obj.Pkg(), name) } func methodExprSignature(sig *types.Signature) *types.Signature { @@ -572,9 +572,9 @@ func methodExprSignature(sig *types.Signature) *types.Signature { func (b Builder) abiMethodFunc(anonymous bool, mPkg *types.Package, mName string, mSig *types.Signature) Function { var fullName string if anonymous { - fullName = b.Pkg.Path() + "." + types.TypeString(mSig.Recv().Type(), abi.PathOf) + "." + mName + fullName = b.Pkg.Path() + "." + types.TypeString(mSig.Recv().Type(), b.Prog.PathOf) + "." + mName } else { - fullName = FuncName(mPkg, mName, mSig.Recv(), false) + fullName = b.Prog.FuncName(mPkg, mName, mSig.Recv(), false) } if b.Pkg.fnlink != nil { fullName = b.Pkg.fnlink(fullName) diff --git a/ssa/decl.go b/ssa/decl.go index a575dd1bee..d4a7bb383c 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -142,7 +142,7 @@ func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { } if rt != nil { // Do not redirect the runtime's own zero-sized allocation sentinel. - zeroName := FullName(rt, runtimeZeroSizedAllocSymbol) + zeroName := p.Prog.FullName(rt, runtimeZeroSizedAllocSymbol) if name != zeroName && p.ownsGlobal(name) { zero := p.moduleZeroSizedAlloc(p.Prog.Elem(t)) alias := llvm.AddAlias(p.mod, typ, 0, zero.impl, name) diff --git a/ssa/globaldce.go b/ssa/globaldce.go index cb83c2a2bc..a0ea9f66c7 100644 --- a/ssa/globaldce.go +++ b/ssa/globaldce.go @@ -30,12 +30,16 @@ type ReflectMethodCheck struct { } func methodCapabilitySig(sig *types.Signature) string { + return methodCapabilitySigWithPath(sig, PathOf) +} + +func methodCapabilitySigWithPath(sig *types.Signature, pathOf func(*types.Package) string) string { canon := types.NewSignatureType(nil, nil, nil, methodCapabilityTuple(sig.Params()), methodCapabilityTuple(sig.Results()), sig.Variadic()) return types.TypeString(canon, func(pkg *types.Package) string { if pkg == nil { return "" } - return PathOf(pkg) + return pathOf(pkg) }) } @@ -113,7 +117,11 @@ func methodCapabilityType(t types.Type) types.Type { } func methodCapabilityKey(method *types.Func) string { - return "go.method." + methodCapabilityName(method) + ":" + methodCapabilitySig(method.Type().(*types.Signature)) + return methodCapabilityKeyWithPath(method, PathOf) +} + +func methodCapabilityKeyWithPath(method *types.Func, pathOf func(*types.Package) string) string { + return "go.method." + methodCapabilityName(method) + ":" + methodCapabilitySigWithPath(method.Type().(*types.Signature), pathOf) } func methodCapabilityName(method *types.Func) string { @@ -289,7 +297,7 @@ func (p Program) addMethodTypeMetadata(global llvm.Value, fullType Type, methods methodStride := p.SizeOf(methodType) for i, sel := range methods { baseOffset := methodArrayOffset + uint64(i)*methodStride - p.addTypeMetadata(global, baseOffset+ifnOffset, methodCapabilityKey(sel.Obj().(*types.Func))) + p.addTypeMetadata(global, baseOffset+ifnOffset, methodCapabilityKeyWithPath(sel.Obj().(*types.Func), p.PathOf)) if sel.Obj().Exported() { name := sel.Obj().Name() p.addTypeMetadata(global, baseOffset+ifnOffset, reflectValueMethodTypeID) diff --git a/ssa/interface.go b/ssa/interface.go index 253df5c64b..f9aee874c6 100644 --- a/ssa/interface.go +++ b/ssa/interface.go @@ -91,7 +91,7 @@ func (b Builder) Imethod(intf Expr, method *types.Func) Expr { if prog.enableGoGlobalDCE { fnType := prog.Elem(pfn.Type) fn = Expr{ - prog.methodCheckedLoad(b.impl, pfn.impl, methodCapabilityKey(method)), + prog.methodCheckedLoad(b.impl, pfn.impl, methodCapabilityKeyWithPath(method, prog.PathOf)), fnType, } } else { @@ -212,7 +212,7 @@ func (b Builder) recordInterfaceInfo(t *types.Interface, typeName string) { for i := 0; i < t.NumMethods(); i++ { f := t.Method(i) ftypName, _ := prog.abi.TypeName(funcType(prog, f.Type())) - mb.AddIfaceMethod(intfSym, abiMethodName(f), mb.Sym(ftypName)) + mb.AddIfaceMethod(intfSym, prog.abiMethodName(f), mb.Sym(ftypName)) } } diff --git a/ssa/package.go b/ssa/package.go index 64263ea9fc..b9ab53bdc7 100644 --- a/ssa/package.go +++ b/ssa/package.go @@ -324,6 +324,7 @@ func NewProgram(target *Target) Program { debugInfoOptimized: target.effectiveOptLevel() != optlevel.O0, } prog.abi.Init(uintptr(prog.ptrSize), (*goProgram)(unsafe.Pointer(prog))) + prog.abi.Namer.RewriteMainPrefix = target.RewriteMainPrefix return prog } @@ -397,7 +398,7 @@ func (p Program) isNoInterfaceMethod(fn *types.Func) bool { if !ok || sig.Recv() == nil { return false } - _, ok = p.noInterface[FuncName(fn.Pkg(), fn.Name(), sig.Recv(), true)] + _, ok = p.noInterface[p.FuncName(fn.Pkg(), fn.Name(), sig.Recv(), true)] return ok } @@ -896,7 +897,7 @@ func (p Package) MaterializePreserveSyms() { func (p Package) rtFunc(fnName string) Expr { p.NeedRuntime = true fn := p.Prog.runtime().Scope().Lookup(fnName).(*types.Func) - name := FullName(fn.Pkg(), fnName) + name := p.Prog.FullName(fn.Pkg(), fnName) if p.fnlink != nil { name = p.fnlink(name) } diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 38085a627b..0e944979d6 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -38,6 +38,39 @@ import ( "github.com/xgo-dev/llvm" ) +func TestProgramRewriteMainPrefixIsRequestScoped(t *testing.T) { + pkg := types.NewPackage("example.com/rewrite", "main") + typeName := types.NewTypeName(token.NoPos, pkg, "T", nil) + named := types.NewNamed(typeName, types.Typ[types.Int], nil) + + plain := NewProgram(nil) + defer plain.Dispose() + rewritten := NewProgram(&Target{RewriteMainPrefix: true}) + defer rewritten.Dispose() + + if got := plain.FullName(pkg, "F"); got != "example.com/rewrite.F" { + t.Fatalf("plain FullName = %q, want example.com/rewrite.F", got) + } + if got := rewritten.FullName(pkg, "F"); got != "main.F" { + t.Fatalf("rewritten FullName = %q, want main.F", got) + } + if got := plain.NameOf(named); got != "example.com/rewrite.T" { + t.Fatalf("plain NameOf = %q, want example.com/rewrite.T", got) + } + if got := rewritten.NameOf(named); got != "main.T" { + t.Fatalf("rewritten NameOf = %q, want main.T", got) + } + if got, _ := plain.abi.TypeName(named); got != "_llgo_example.com/rewrite.T" { + t.Fatalf("plain ABI TypeName = %q, want _llgo_example.com/rewrite.T", got) + } + if got, _ := rewritten.abi.TypeName(named); got != "_llgo_main.T" { + t.Fatalf("rewritten ABI TypeName = %q, want _llgo_main.T", got) + } + if got := FullName(pkg, "F"); got != "example.com/rewrite.F" { + t.Fatalf("default FullName changed to %q", got) + } +} + func TestEndDefer(t *testing.T) { prog := NewProgram(nil) pkg := prog.NewPackage("foo", "foo") diff --git a/ssa/target.go b/ssa/target.go index a352b477fd..2e3f767cbc 100644 --- a/ssa/target.go +++ b/ssa/target.go @@ -27,11 +27,12 @@ import ( // ----------------------------------------------------------------------------- type Target struct { - GOOS string - GOARCH string - GOARM string // "5", "6", "7" (default) - Target string // target name from -target flag (e.g., "esp32", "arm7tdmi", "wasi") - OptLevel optlevel.Level + GOOS string + GOARCH string + GOARM string // "5", "6", "7" (default) + Target string // target name from -target flag (e.g., "esp32", "arm7tdmi", "wasi") + OptLevel optlevel.Level + RewriteMainPrefix bool // use "main" as the symbol prefix for packages named main } func (p *Target) targetInfo() (llvm.TargetData, llvm.TargetMachine) { diff --git a/ssa/type.go b/ssa/type.go index 98c9fc4848..5f5c5c351d 100644 --- a/ssa/type.go +++ b/ssa/type.go @@ -521,7 +521,7 @@ func (p Program) retType(raw *types.Signature) Type { } func (p Program) llvmNameOf(named *types.Named) (name string) { - name = NameOf(named) + name = p.NameOf(named) if obj := named.Obj(); obj != nil { parent := obj.Parent() pkg := obj.Pkg() @@ -634,20 +634,41 @@ func NameOf(typ *types.Named) string { return abi.FullName(typ.Obj().Pkg(), abi.NamedName(typ)) } +func (p Program) NameOf(typ *types.Named) string { + namer := p.namer() + return namer.FullName(typ.Obj().Pkg(), namer.NamedName(typ)) +} + // FullName returns the full name of a package member. func FullName(pkg *types.Package, name string) string { return abi.FullName(pkg, name) } +func (p Program) FullName(pkg *types.Package, name string) string { + return p.namer().FullName(pkg, name) +} + // PathOf returns the package path of the specified package. func PathOf(pkg *types.Package) string { return abi.PathOf(pkg) } +func (p Program) PathOf(pkg *types.Package) string { + return p.namer().PathOf(pkg) +} + // FuncName: // - func: pkg.name // - method: pkg.T.name, pkg.(*T).name func FuncName(pkg *types.Package, name string, recv *types.Var, org bool) string { + return funcName(abi.Namer{}, pkg, name, recv, org) +} + +func (p Program) FuncName(pkg *types.Package, name string, recv *types.Var, org bool) string { + return funcName(p.namer(), pkg, name, recv, org) +} + +func funcName(namer abi.Namer, pkg *types.Package, name string, recv *types.Var, org bool) string { if recv != nil { named, ptr := recvNamed(recv.Type()) var tName string @@ -655,18 +676,17 @@ func FuncName(pkg *types.Package, name string, recv *types.Var, org bool) string if org { tName = named.Obj().Name() } else { - tName = abi.NamedName(named) + tName = namer.NamedName(named) } if ptr { tName = "(*" + tName + ")" } } else { - tName = types.TypeString(recv.Type(), PathOf) + tName = types.TypeString(recv.Type(), namer.PathOf) } - return PathOf(pkg) + "." + tName + "." + name + return namer.PathOf(pkg) + "." + tName + "." + name } - ret := FullName(pkg, name) - return ret + return namer.FullName(pkg, name) } func recvNamed(t types.Type) (typ *types.Named, ptr bool) { @@ -689,4 +709,15 @@ func TypeArgs(typeArgs []types.Type) string { return abi.TypeArgs(typeArgs) } +func (p Program) TypeArgs(typeArgs []types.Type) string { + return p.namer().TypeArgs(typeArgs) +} + +func (p Program) namer() abi.Namer { + if p == nil { + return abi.Namer{} + } + return p.abi.Namer +} + // -----------------------------------------------------------------------------