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  }