Saturday, June 13, 2015

Using build constraints to skip entire hierarchies in go

This is a nice change to let go apply build constraints to prune a hierarchy of packages. After the change, if there is a file skip.go in a package, then the build constraint for the file refers to the entire package and not just to the file. Also, if the package is not selected by the constraint, any sub-directory is also left out.

This is an example file, src/net/internal/socktest/skip.go, which I use to avoid compiling this package for Clive:

// +build !clive 

package socktest


Once this file is in place, running "go install std", or whatever happens to mention that package, will print a line

skip: socktest

and skip that package (and any directories within it).

In src/go/build/build.go, you add a new flag NotToBuild to the Package:

--- a/src/go/build/build.go
+++ b/src/go/build/build.go
@@ -360,6 +360,8 @@ type Package struct {
  AllTags       []string // tags that can influence file selection in this directory
  ConflictDir   string   // this directory shadows Dir in $GOPATH
 
+ NotToBuild bool // the skip.go file indicates not to build this in this context
+
  // Source files
  GoFiles        []string // .go source files (excluding CgoFiles, TestGoFiles, XTestGoFiles)
  CgoFiles       []string // .go source files that import "C"

and then set NotToBuild in the package if skip.go is there and does not match the context.
In that case, all files are added to the ignored list and also we return an error in case the caller
might be tempted to do anything with the package that we want to discard.

We must look for the skip file before processing any other file because we might have a skip in place because other files do not even compile, for example.

--- a/src/go/build/build.go
+++ b/src/go/build/build.go
@@ -610,6 +612,20 @@ Found:
  return p, err
  }
 
+ for i, d := range dirs {
+ name := d.Name()
+ if name != "skip.go" {
+ continue
+ }
+ dirs[i] = dirs[len(dirs)-1]
+ dirs = dirs[:len(dirs)-1]
+ match, _, _, _ := ctxt.matchFile(p.Dir, name, true, make(map[string]bool))
+ if !match {
+ fmt.Fprintf(os.Stderr, "skip: %s\n", p.Dir)
+ p.NotToBuild = true
+ }
+ break
+ }
  var Sfiles []string // files with ".S" (capital S)
  var firstFile, firstCommentFile string
  imported := make(map[string][]token.Position)
@@ -670,6 +686,10 @@ Found:
  continue
  }
 
+ if p.NotToBuild {
+ p.IgnoredGoFiles = append(p.IgnoredGoFiles, name)
+ continue
+ }
  pf, err := parser.ParseFile(fset, filename, data, parser.ImportsOnly|parser.ParseComments)
  if err != nil {
  return p, err
@@ -777,7 +797,9 @@ Found:
  if len(p.GoFiles)+len(p.CgoFiles)+len(p.TestGoFiles)+len(p.XTestGoFiles) == 0 {
  return p, &NoGoError{p.Dir}
  }
-
+ if p.NotToBuild {
+ return p, &NoGoError{p.Dir}
+ }
  for tag := range allTags {
  p.AllTags = append(p.AllTags, tag)
  }

Then, the flag is copied into the Package structure used by the go command.

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -95,6 +95,8 @@ type Package struct {
  coverMode    string               // preprocess Go source files with the coverage tool in this mode
  coverVars    map[string]*CoverVar // variables created by coverage analysis
  omitDWARF    bool                 // tell linker not to write DWARF information
+
+ NotToBuild bool `json:",omitempty"` // package is a skip
 }
 
 // CoverVar holds the name of the generated coverage variables targeting the named file.
@@ -137,6 +139,7 @@ func (p *Package) copyBuild(pp *build.Package) {
  p.TestImports = pp.TestImports
  p.XTestGoFiles = pp.XTestGoFiles
  p.XTestImports = pp.XTestImports
+ p.NotToBuild = pp.NotToBuild 
 }

And finally we make that command do the discard. The list of packages matched is adjusted
to remove those NotToBuild and children of NotToBuild.

--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -516,7 +516,7 @@ func matchPackages(pattern string) []string {
  have["runtime/cgo"] = true // ignore during walk
  }
  var pkgs []string
-
+ var skip []string
  for _, src := range buildContext.SrcDirs() {
  if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
  continue
@@ -554,12 +554,23 @@ func matchPackages(pattern string) []string {
  if !match(name) {
  return nil
  }
- _, err = buildContext.ImportDir(path, 0)
+ var p *build.Package
+ p, err = buildContext.ImportDir(path, 0)
  if err != nil {
+ if p != nil && p.NotToBuild {
+ skip = append(skip, path+"/")
+ return nil
+ }
  if _, noGo := err.(*build.NoGoError); noGo {
  return nil
  }
  }
+ for _, s := range skip {
+ if strings.HasPrefix(path, s) {
+ // fmt.Fprintf(os.Stderr, "skip child %s\n", path)
+ return nil
+ }
+ }
  pkgs = append(pkgs, name)
  return nil
  })
@@ -597,6 +608,7 @@ func matchPackagesInFS(pattern string) []string {
  match := matchPattern(pattern)
 
  var pkgs []string
+ var skip []string
  filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
  if err != nil || !fi.IsDir() {
  return nil
@@ -624,12 +636,23 @@ func matchPackagesInFS(pattern string) []string {
  if !match(name) {
  return nil
  }
- if _, err = build.ImportDir(path, 0); err != nil {
+ var p *build.Package
+ if p, err = build.ImportDir(path, 0); err != nil {
+ if p != nil && p.NotToBuild {
+ skip = append(skip, path+"/")
+ return nil
+ }
  if _, noGo := err.(*build.NoGoError); !noGo {
  log.Print(err)
  }
  return nil
  }
+ for _, s := range skip {
+ if strings.HasPrefix(path, s) {
+ // fmt.Fprintf(os.Stderr, "skip child %s\n", path)
+ return nil
+ }
+ }
  pkgs = append(pkgs, name)
  return nil
  })