github.com/please-build/go-rules/tools/please_go@v0.0.0-20240319165128-ea27d6f5caba/modinfo/modinfo.go (about) 1 // Package modinfo produces modinfo records for Go's importconfig which are consumed by 2 // `go tool link` and later accessible from runtime/debug.ReadBuildInfo() 3 package modinfo 4 5 import ( 6 "encoding/hex" 7 "fmt" 8 "io/fs" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime/debug" 13 "sort" 14 "strings" 15 16 "golang.org/x/mod/module" 17 ) 18 19 // WriteModInfo writes mod info to the given output file 20 func WriteModInfo(goTool, modulePath, pkgPath, buildMode, cgoEnabled, goos, goarch, outputFile string) error { 21 if buildMode == "" { 22 buildMode = "exe" 23 } 24 // Nab the Go version from the tool 25 out, err := exec.Command(goTool, "version").CombinedOutput() 26 if err != nil { 27 return fmt.Errorf("failed to exec %s: %w", goTool, err) 28 } 29 bi := debug.BuildInfo{ 30 GoVersion: strings.TrimSpace(string(out)), 31 Path: pkgPath, 32 Main: debug.Module{ 33 Path: modulePath, 34 // We don't have a concept of a module version here 35 }, 36 Settings: []debug.BuildSetting{ 37 {Key: "-buildmode", Value: buildMode}, 38 {Key: "CGO_ENABLED", Value: cgoEnabled}, 39 {Key: "GOARCH", Value: goarch}, 40 {Key: "GOOS", Value: goos}, 41 }, 42 } 43 seen := map[string]struct{}{} 44 if err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { 45 if err != nil || d.IsDir() || !strings.HasSuffix(path, ".modinfo") { 46 return err 47 } 48 contents, err := os.ReadFile(path) 49 if err != nil { 50 return err 51 } 52 mod := strings.TrimSpace(string(contents)) 53 if _, present := seen[mod]; !present { 54 seen[mod] = struct{}{} 55 if module, version, found := strings.Cut(mod, "@"); found { 56 bi.Deps = append(bi.Deps, &debug.Module{ 57 Path: module, 58 Version: version, 59 }) 60 } 61 } 62 return nil 63 }); err != nil { 64 return fmt.Errorf("failed to walk modinfo files: %w", err) 65 } 66 sort.Slice(bi.Deps, func(i, j int) bool { return bi.Deps[i].Path < bi.Deps[j].Path }) 67 return os.WriteFile(outputFile, []byte(fmt.Sprintf("modinfo %q\n", modInfoData(bi.String()))), 0644) 68 } 69 70 // modInfoData wraps the given string in Go's modinfo. This mimics what go build does in order 71 // for `go version` to be able to find this lot later on. 72 func modInfoData(modinfo string) string { 73 // These are not exported from the stdlib (they're in cmd/go/internal/modload) so we must duplicate :( 74 start, _ := hex.DecodeString("3077af0c9274080241e1c107e6d618e6") 75 end, _ := hex.DecodeString("f932433186182072008242104116d8f2") 76 return string(start) + modinfo + string(end) 77 } 78 79 // WriteModuleVersion generates a module version file for a third-party Go module. 80 // 81 // If validate is true, WriteModuleVersion additionally checks that the given module path and version are valid 82 // according to Go's module version numbering standard. 83 func WriteModuleVersion(modulePath, version string, validate bool, outputFile string) error { 84 if validate { 85 if err := module.Check(modulePath, version); err != nil { 86 return fmt.Errorf("invalid module path/version: %v", err) 87 } 88 } 89 canonicalVersion := module.CanonicalVersion(version) 90 return os.WriteFile(outputFile, []byte(fmt.Sprintf("%s@%s", modulePath, canonicalVersion)), 0644) 91 }