github.com/please-build/go-rules/tools/please_go@v0.0.0-20240319165128-ea27d6f5caba/packageinfo/packageinfo.go (about)

     1  // Package packageinfo writes information about Go packages in a JSON format.
     2  // This is created at build time and intended to be consumed by the gopackagedriver binary.
     3  package packageinfo
     4  
     5  import (
     6  	"encoding/json"
     7  	"fmt"
     8  	"go/build"
     9  	"io"
    10  	"io/fs"
    11  	"log"
    12  	"os"
    13  	"path/filepath"
    14  	"runtime"
    15  	"slices"
    16  	"sort"
    17  	"strings"
    18  
    19  	"golang.org/x/tools/go/packages"
    20  )
    21  
    22  // WritePackageInfo writes a series of package info files to the given file.
    23  func WritePackageInfo(importPath string, srcRoot, importconfig string, imports map[string]string, installPkgs []string, subrepo, module string, w io.Writer) error {
    24  	// Discover all Go files in the module
    25  	goFiles := map[string][]string{}
    26  	module = modulePath(module, importPath)
    27  
    28  	walkDirFunc := func(path string, d fs.DirEntry, err error) error {
    29  		if err != nil {
    30  			return err
    31  		} else if name := d.Name(); name == "testdata" {
    32  			return filepath.SkipDir // Don't descend into testdata
    33  		} else if strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") {
    34  			dir := filepath.Dir(path)
    35  			goFiles[dir] = append(goFiles[dir], path)
    36  		}
    37  		return nil
    38  	}
    39  	// Check install packages first
    40  	for _, pkg := range installPkgs {
    41  		if strings.Contains(pkg, "...") {
    42  			pkg = strings.TrimSuffix(pkg, "...")
    43  			if err := filepath.WalkDir(filepath.Join(srcRoot, pkg), walkDirFunc); err != nil {
    44  				return fmt.Errorf("failed to read module dir: %w", err)
    45  			}
    46  		} else {
    47  			dir := filepath.Join(srcRoot, pkg)
    48  			goFiles[dir] = append(goFiles[dir], filepath.Join(srcRoot, pkg))
    49  		}
    50  	}
    51  	if len(installPkgs) == 0 {
    52  		if err := filepath.WalkDir(srcRoot, walkDirFunc); err != nil {
    53  			return fmt.Errorf("failed to read module dir: %w", err)
    54  		}
    55  	}
    56  	if importconfig != "" {
    57  		m, err := loadImportConfig(importconfig)
    58  		if err != nil {
    59  			return fmt.Errorf("failed to read importconfig: %w", err)
    60  		}
    61  		imports = m
    62  	}
    63  	pkgs := make([]*packages.Package, 0, len(goFiles))
    64  	for dir := range goFiles {
    65  		pkgDir := strings.TrimPrefix(strings.TrimPrefix(dir, srcRoot), "/")
    66  		pkg, err := createPackage(filepath.Join(importPath, pkgDir), dir, subrepo, module)
    67  		if _, ok := err.(*build.NoGoError); ok {
    68  			continue // Don't really care, this happens sometimes for modules
    69  		} else if err != nil {
    70  			return fmt.Errorf("failed to import directory %s: %w", dir, err)
    71  		}
    72  		if subrepo != "" {
    73  			_, pkgPath, ok := strings.Cut(imports[pkg.PkgPath], pkg.PkgPath)
    74  			if !ok {
    75  				return fmt.Errorf("Cannot determine export file path for package %s from %s", pkg.PkgPath, imports[pkg.PkgPath])
    76  			}
    77  			// This is a really gross hack to sneak both paths through the one field.
    78  			pkg.ExportFile = filepath.Join(subrepo, pkgPath) + "|" + imports[pkg.PkgPath]
    79  		} else {
    80  			pkg.ExportFile = imports[pkg.PkgPath]
    81  		}
    82  		pkgs = append(pkgs, pkg)
    83  	}
    84  	// If we're doing the stdlib, limit it to just things in the importconfig (i.e. no cmd/ packages)
    85  	if importconfig != "" {
    86  		pkgs = slices.DeleteFunc(pkgs, func(pkg *packages.Package) bool {
    87  			_, present := imports[pkg.PkgPath]
    88  			return !present
    89  		})
    90  	}
    91  	// Vendor packages. They aren't identified by the original imports but we know what they are now.
    92  	vendorised := map[string]*packages.Package{}
    93  	for _, pkg := range pkgs {
    94  		if strings.HasPrefix(pkg.PkgPath, "vendor/") {
    95  			vendorised[strings.TrimPrefix(pkg.PkgPath, "vendor/")] = pkg
    96  		}
    97  	}
    98  	for _, pkg := range pkgs {
    99  		for k := range pkg.Imports {
   100  			if v, present := vendorised[k]; present {
   101  				pkg.Imports[k] = v
   102  			}
   103  		}
   104  	}
   105  	// Ensure output is deterministic
   106  	sort.Slice(pkgs, func(i, j int) bool {
   107  		return pkgs[i].ID < pkgs[j].ID
   108  	})
   109  	e := json.NewEncoder(w)
   110  	e.SetIndent("", "  ")
   111  	return e.Encode(pkgs)
   112  }
   113  
   114  func createPackage(pkgPath, pkgDir, subrepo, module string) (*packages.Package, error) {
   115  	if pkgDir == "" || pkgDir == "." {
   116  		// This happens when we're in the repo root, ImportDir refuses to read it for some reason.
   117  		path, err := filepath.Abs(pkgDir)
   118  		if err != nil {
   119  			return nil, err
   120  		}
   121  		pkgDir = path
   122  	}
   123  	bpkg, err := build.ImportDir(pkgDir, build.ImportComment)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	bpkg.ImportPath = pkgPath
   128  	return FromBuildPackage(bpkg, subrepo, module), nil
   129  }
   130  
   131  // FromBuildPackage creates a packages Package from a build Package.
   132  func FromBuildPackage(pkg *build.Package, subrepo, module string) *packages.Package {
   133  	p := &packages.Package{
   134  		ID:              pkg.ImportPath,
   135  		Name:            pkg.Name,
   136  		PkgPath:         pkg.ImportPath,
   137  		GoFiles:         make([]string, len(pkg.GoFiles)),
   138  		CompiledGoFiles: make([]string, len(pkg.GoFiles)),
   139  		OtherFiles:      mappend(pkg.CFiles, pkg.CXXFiles, pkg.MFiles, pkg.HFiles, pkg.SFiles, pkg.SwigFiles, pkg.SwigCXXFiles, pkg.SysoFiles),
   140  		EmbedPatterns:   pkg.EmbedPatterns,
   141  		Imports:         make(map[string]*packages.Package, len(pkg.Imports)),
   142  	}
   143  	for i, file := range pkg.GoFiles {
   144  		if subrepo != "" {
   145  			// this is fairly nasty... there must be a better way of getting it without the pkg/ prefix
   146  			log.Printf("here %s | %s | %s | %s", subrepo, pkg.Dir, file, module)
   147  			dir := strings.TrimPrefix(pkg.Dir, "pkg/"+runtime.GOOS+"_"+runtime.GOARCH)
   148  			dir = strings.TrimPrefix(strings.TrimPrefix(dir, "/"), module)
   149  			p.GoFiles[i] = filepath.Join(subrepo, dir, file)
   150  			p.CompiledGoFiles[i] = filepath.Join(pkg.Dir, file) // Stash this here for later
   151  		} else {
   152  			p.GoFiles[i] = filepath.Join(pkg.Dir, file)
   153  			p.CompiledGoFiles[i] = filepath.Join(pkg.Dir, file)
   154  		}
   155  	}
   156  	for _, imp := range pkg.Imports {
   157  		p.Imports[imp] = &packages.Package{ID: imp, PkgPath: imp}
   158  	}
   159  	return p
   160  }
   161  
   162  // mappend appends multiple slices together.
   163  func mappend(s []string, args ...[]string) []string {
   164  	for _, arg := range args {
   165  		s = append(s, arg...)
   166  	}
   167  	return s
   168  }
   169  
   170  // loadImportConfig reads the given importconfig file and produces a map of package name -> export path
   171  func loadImportConfig(filename string) (map[string]string, error) {
   172  	b, err := os.ReadFile(filename)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  	lines := strings.Split(string(b), "\n")
   177  	m := make(map[string]string, len(lines))
   178  	for _, line := range lines {
   179  		if strings.HasPrefix(line, "packagefile ") {
   180  			pkg, exportFile, found := strings.Cut(strings.TrimPrefix(line, "packagefile "), "=")
   181  			if !found {
   182  				return nil, fmt.Errorf("unknown syntax for line: %s", line)
   183  			}
   184  			m[pkg] = exportFile
   185  		}
   186  	}
   187  	return m, nil
   188  }
   189  
   190  // modulePath returns the import path for a module, or the given one if the module isn't set.
   191  func modulePath(module, importPath string) string {
   192  	if module == "" {
   193  		return importPath
   194  	}
   195  	before, _, _ := strings.Cut(module, "@")
   196  	return before
   197  }