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 }