// Copyright 2019 Google LLC. // SPDX-License-Identifier: Apache-2.0 // This tool analyses external (i.e. not built with `buildGo.nix`) Go // packages to determine a build plan that Nix can import. package main import ( "encoding/json" "flag" "fmt" "go/build" "io/ioutil" "log" "os" "path" "path/filepath" "strings" ) // Path to a JSON file describing all standard library import paths. // This file is generated and set here by Nix during the build // process. var stdlibList string // pkg describes a single Go package within the specified source // directory. // // Return information includes the local (relative from project root) // and external (none-stdlib) dependencies of this package. type pkg struct { Name string `json:"name"` PackageName string `json:"packageName"` Locator []string `json:"locator"` Files []string `json:"files"` CgoFiles []string `json:"cgofiles"` SFiles []string `json:"sfiles"` CFiles []string `json:"cfiles"` CXXFiles []string `json:"cxxfiles"` LocalDeps [][]string `json:"localDeps"` ForeignDeps []foreignDep `json:"foreignDeps"` IsCommand bool `json:"isCommand"` CgoCFLAGS []string `json:"cgocflags"` CgoLDFLAGS []string `json:"cgoldflags"` } type foreignDep struct { Path string `json:"path"` // filename, column and line number of the import, if known Position string `json:"position"` } // findGoDirs returns a filepath.WalkFunc that identifies all // directories that contain Go source code in a certain tree. func findGoDirs(at string) ([]string, error) { dirSet := make(map[string]bool) err := filepath.Walk(at, func(path string, info os.FileInfo, err error) error { if err != nil { return err } name := info.Name() // Skip folders that are guaranteed to not be relevant if info.IsDir() && (name == "testdata" || name == ".git") { return filepath.SkipDir } // If the current file is a Go file, then the directory is popped // (i.e. marked as a Go directory). if !info.IsDir() && strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") { dirSet[filepath.Dir(path)] = true } return nil }) if err != nil { return nil, err } goDirs := []string{} for k, _ := range dirSet { goDirs = append(goDirs, k) } return goDirs, nil } func forceEmpty[T any](m []T) []T { if m == nil { return make([]T, 0) } return m } // analysePackage loads and analyses the imports of a single Go // package, returning the data that is required by the Nix code to // generate a derivation for this package. func analysePackage(root, source, importpath string, stdlib map[string]bool, tags []string, cgo bool) (pkg, error) { ctx := build.Default ctx.CgoEnabled = cgo ctx.BuildTags = tags p, err := ctx.ImportDir(source, build.IgnoreVendor) if err != nil { return pkg{}, err } local := [][]string{} foreign := []foreignDep{} for _, i := range p.Imports { if stdlib[i] { continue } if i == importpath { local = append(local, []string{}) } else if strings.HasPrefix(i, importpath+"/") { local = append(local, strings.Split(strings.TrimPrefix(i, importpath+"/"), "/")) } else { // The import positions is a map keyed on the import name. // The value is a list, presumably because an import can appear // multiple times in a package. Let’s just take the first one, // should be enough for a good error message. firstPos := p.ImportPos[i][0].String() foreign = append(foreign, foreignDep{Path: i, Position: firstPos}) } } prefix := strings.TrimPrefix(source, root+"/") locator := []string{} if len(prefix) != len(source) { locator = strings.Split(prefix, "/") } else { // Otherwise, the locator is empty since its the root package and // no prefix should be added to files. prefix = "" } prependPrefix := func(fs []string) []string { out := make([]string, len(fs)) for n, f := range fs { out[n] = path.Join(prefix, f) } return out } return pkg{ Name: path.Join(importpath, prefix), PackageName: p.Name, Locator: locator, Files: prependPrefix(p.GoFiles), CgoFiles: prependPrefix(p.CgoFiles), SFiles: prependPrefix(p.SFiles), CFiles: prependPrefix(p.CFiles), CXXFiles: prependPrefix(p.CXXFiles), LocalDeps: local, ForeignDeps: foreign, IsCommand: p.IsCommand(), CgoCFLAGS: forceEmpty(p.CgoCFLAGS), CgoLDFLAGS: forceEmpty(p.CgoLDFLAGS), }, nil } func loadStdlibPkgs(from string) (pkgs map[string]bool, err error) { f, err := ioutil.ReadFile(from) if err != nil { return } err = json.Unmarshal(f, &pkgs) return } func main() { source := flag.String("source", "", "path to directory with sources to process") path := flag.String("path", "", "import path for the package") tagsStr := flag.String("tags", "", "tags, space-separated") cgo := flag.Bool("cgo", false, "cgo") flag.Parse() if *source == "" { log.Fatalf("-source flag must be specified") } var tags []string if len(*tagsStr) > 0 { tags = strings.Split(*tagsStr, " ") } stdlibPkgs, err := loadStdlibPkgs(stdlibList) if err != nil { log.Fatalf("failed to load standard library index from %q: %s\n", stdlibList, err) } goDirs, err := findGoDirs(*source) if err != nil { log.Fatalf("failed to walk source directory '%s': %s", *source, err) } all := []pkg{} for _, d := range goDirs { analysed, err := analysePackage(*source, d, *path, stdlibPkgs, tags, *cgo) // If the Go source analysis returned "no buildable Go files", // that directory should be skipped. // // This might be due to `+build` flags on the platform and other // reasons (such as test files). if _, ok := err.(*build.NoGoError); ok { continue } if err != nil { log.Fatalf("failed to analyse package at %q: %s", d, err) } all = append(all, analysed) } j, _ := json.Marshal(all) fmt.Println(string(j)) }