github.com/thockin/go2make@v0.0.0-20221008213743-c1956c0434a7/go2make.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/spf13/pflag"
    30  	"golang.org/x/tools/go/packages"
    31  )
    32  
    33  var flHelp = pflag.BoolP("help", "h", false, "print help and exit")
    34  var flDbg = pflag.BoolP("debug", "d", false, "enable debugging output")
    35  var flDbgTime = pflag.BoolP("debug-time", "D", false, "enable debugging output with timestamps")
    36  var flOut = pflag.StringP("output", "o", "make", "output format (mainly for debugging): one of make | json)")
    37  var flRoots = pflag.StringSlice("root", nil, "only process packages under specific prefixes (may be specified multiple times)")
    38  var flPrune = pflag.StringSlice("prune", nil, "package prefixes to prune (recursive, may be specified multiple times)")
    39  var flTags = pflag.StringSlice("tag", nil, "build tags to pass to Go (see 'go help build', may be specified multiple times)")
    40  var flRelPath = pflag.String("relative-to", ".", "emit by-path rules for packages relative to this path")
    41  var flImports = pflag.Bool("imports", false, "process all imports of all packages, recursively")
    42  var flStateDir = pflag.String("state-dir", ".go2make", "directory in which to store state used by make")
    43  var flIgnoreErrors = pflag.BoolP("ignore-errors", "e", false, "ignore package errors")
    44  
    45  var lastDebugTime time.Time
    46  
    47  func debug(items ...interface{}) {
    48  	if *flDbg {
    49  		x := []interface{}{}
    50  		if *flDbgTime {
    51  			elapsed := time.Since(lastDebugTime)
    52  			if lastDebugTime.IsZero() {
    53  				elapsed = 0
    54  			}
    55  			lastDebugTime = time.Now()
    56  			x = append(x, fmt.Sprintf("DBG(+%v):", elapsed))
    57  		} else {
    58  			x = append(x, "DBG:")
    59  		}
    60  		x = append(x, items...)
    61  		fmt.Fprintln(os.Stderr, x...)
    62  	}
    63  
    64  }
    65  
    66  type emitter struct {
    67  	roots        []string
    68  	prune        []string
    69  	tags         []string
    70  	ignoreErrors bool
    71  	relPath      string
    72  	imports      bool
    73  	stateDir     string
    74  }
    75  
    76  func main() {
    77  	pflag.Parse()
    78  
    79  	if *flHelp {
    80  		help(os.Stdout)
    81  		os.Exit(0)
    82  	}
    83  
    84  	if *flDbgTime {
    85  		*flDbg = true
    86  	}
    87  
    88  	switch *flOut {
    89  	case "make":
    90  	case "json":
    91  	default:
    92  		fmt.Fprintf(os.Stderr, "unknown output format %q\n", *flOut)
    93  		pflag.Usage()
    94  		os.Exit(1)
    95  	}
    96  
    97  	if *flRelPath == "" {
    98  		fmt.Fprintf(os.Stderr, "error: --relative-to must be defined\n")
    99  		os.Exit(1)
   100  	}
   101  
   102  	if *flStateDir == "" {
   103  		fmt.Fprintf(os.Stderr, "error: --state-dir must be defined\n")
   104  		os.Exit(1)
   105  	}
   106  
   107  	targets := pflag.Args()
   108  	if len(targets) == 0 {
   109  		targets = append(targets, ".")
   110  	}
   111  	debug("targets:", targets)
   112  
   113  	// Gather flag values for easier testing.
   114  	emit := emitter{
   115  		roots:        forEach(*flRoots, dropTrailingSlash),
   116  		prune:        forEach(*flPrune, dropTrailingSlash),
   117  		tags:         *flTags,
   118  		ignoreErrors: *flIgnoreErrors,
   119  		relPath:      dropTrailingSlash(absOrExit(*flRelPath)),
   120  		imports:      *flImports,
   121  		stateDir:     dropTrailingSlash(*flStateDir),
   122  	}
   123  	debug("roots:", emit.roots)
   124  	debug("prune:", emit.prune)
   125  	debug("tags:", emit.tags)
   126  	debug("relative-to:", emit.relPath)
   127  
   128  	pkgs, err := emit.loadPackages(targets...)
   129  	if err != nil {
   130  		fmt.Fprintf(os.Stderr, "error loading packages: %v\n", err)
   131  		os.Exit(1)
   132  	}
   133  
   134  	pkgMap := emit.visitPackages(pkgs)
   135  	if pkgMap == nil {
   136  		os.Exit(1)
   137  	}
   138  
   139  	switch *flOut {
   140  	case "make":
   141  		emit.emitMake(os.Stdout, pkgMap)
   142  	case "json":
   143  		emit.emitJSON(os.Stdout, pkgMap)
   144  	}
   145  }
   146  
   147  func help(out io.Writer) {
   148  	prog := filepath.Base(os.Args[0])
   149  	fmt.Fprintf(out, "Usage: %s [FLAG...] <PKG...>\n", prog)
   150  	fmt.Fprintf(out, "\n")
   151  	fmt.Fprintf(out, "%s calculates all of the dependencies of a set of Go packages and\n", prog)
   152  	fmt.Fprintf(out, "emits a Makfile (unless otherwise specified) which can be used to track dependencies.\n")
   153  	fmt.Fprintf(out, "\n")
   154  	fmt.Fprintf(out, "Package specifications may be simple (e.g. 'example.com/txt/color') or\n")
   155  	fmt.Fprintf(out, "recursive (e.g. 'example.com/txt/...'), and may be Go package names or\n")
   156  	fmt.Fprintf(out, "relative file paths (e.g. './...')\n")
   157  	fmt.Fprintf(out, "\n")
   158  	fmt.Fprintf(out, " Example output:\n")
   159  	fmt.Fprintf(out, "  .go2make/by-pkg/example.com/txt/color/_pkg: .go2make/by-pkg/example.com/txt/color/_files \\\n")
   160  	fmt.Fprintf(out, "    color/color.go \\\n")
   161  	fmt.Fprintf(out, "    .go2make/by-pkg/bytes/_pkg \\\n")
   162  	fmt.Fprintf(out, "    .go2make/by-pkg/example.com/pretty/_pkg\n")
   163  	fmt.Fprintf(out, "          @mkdir -p $(@D)\n")
   164  	fmt.Fprintf(out, "          @touch $@\n")
   165  	fmt.Fprintf(out, "\n")
   166  	fmt.Fprintf(out, "  .go2make/by-path/pkg/example.com/txt/color/_pkg: .go2make/by-pkg/example.com/txt/color/_pkg\n")
   167  	fmt.Fprintf(out, "          @mkdir -p $(@D)\n")
   168  	fmt.Fprintf(out, "          @touch $@\n")
   169  	fmt.Fprintf(out, "\n")
   170  	fmt.Fprintf(out, "User Makefiles can include the generated output and trigger actions when the Go packages need\n")
   171  	fmt.Fprintf(out, "to be rebuilt.  The 'by-pkg/.../_pkg' rules are defined by the Go package name (e.g.\n")
   172  	fmt.Fprintf(out, "example.com/txt/color).  The 'by-path/.../_pkg' rules are defined by the relative path of the\n")
   173  	fmt.Fprintf(out, "Go package when that path is below the value of the --relative-to flag.\n")
   174  	fmt.Fprintf(out, "\n")
   175  	fmt.Fprintf(out, "To make this easier to use, the variables GO2MAKE_BY_PKG and GO2MAKE_BY_PATH are defined.\n")
   176  	fmt.Fprintf(out, "These can be used via make's '$(call)' function.\n")
   177  	fmt.Fprintf(out, " Flags:\n")
   178  
   179  	pflag.PrintDefaults()
   180  }
   181  
   182  func absOrExit(path string) string {
   183  	abs, err := filepath.Abs(path)
   184  	if err != nil {
   185  		fmt.Fprintf(os.Stderr, "error: %v", err)
   186  		os.Exit(1)
   187  	}
   188  	return abs
   189  }
   190  
   191  func dropTrailingSlash(s string) string {
   192  	return strings.TrimRight(s, "/")
   193  }
   194  
   195  func forEach(in []string, fn func(s string) string) []string {
   196  	out := make([]string, 0, len(in))
   197  	for _, s := range in {
   198  		out = append(out, fn(s))
   199  	}
   200  	return out
   201  }
   202  
   203  func (emit emitter) loadPackages(targets ...string) ([]*packages.Package, error) {
   204  	cfg := packages.Config{
   205  		Mode:       packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedModule,
   206  		Tests:      false,
   207  		BuildFlags: []string{"-tags", strings.Join(emit.tags, ",")},
   208  	}
   209  	if emit.imports {
   210  		cfg.Mode |= packages.NeedDeps
   211  	}
   212  	return packages.Load(&cfg, targets...)
   213  }
   214  
   215  func (emit emitter) visitPackages(pkgs []*packages.Package) map[string]*packages.Package {
   216  	pkgMap := map[string]*packages.Package{}
   217  	errs := false
   218  	for _, p := range pkgs {
   219  		ok := emit.visitPackage(p, pkgMap)
   220  		if !ok {
   221  			errs = true
   222  		}
   223  	}
   224  	if errs {
   225  		return nil
   226  	}
   227  	return pkgMap
   228  }
   229  
   230  func (emit emitter) visitPackage(pkg *packages.Package, pkgMap map[string]*packages.Package) bool {
   231  	debug("visiting package", pkg.PkgPath)
   232  	if pkgMap[pkg.PkgPath] == pkg {
   233  		debug("  ", pkg.PkgPath, "was already visited")
   234  		return true
   235  	}
   236  
   237  	if len(emit.roots) > 0 && !rooted(pkg.PkgPath, emit.roots) {
   238  		debug("  ", pkg.PkgPath, "is not under an allowed root")
   239  		return true
   240  	}
   241  
   242  	if len(emit.prune) > 0 && rooted(pkg.PkgPath, emit.prune) {
   243  		debug("  ", pkg.PkgPath, "pruned")
   244  		return true
   245  	}
   246  
   247  	debug("  ", pkg.PkgPath, "is new")
   248  	pkgMap[pkg.PkgPath] = pkg
   249  
   250  	ok := true
   251  	for _, e := range pkg.Errors {
   252  		if emit.ignoreErrors {
   253  			debug("    ignoring error:", e.Msg)
   254  		} else {
   255  			fmt.Fprintf(os.Stderr, "%s\n", e.Msg)
   256  			ok = false
   257  		}
   258  	}
   259  
   260  	// Don't recurse if we have errors already.
   261  	if ok && emit.imports && len(pkg.Imports) > 0 {
   262  		debug("  ", pkg.PkgPath, "has", len(pkg.Imports), "imports")
   263  
   264  		visitEach(pkg.Imports, func(imp *packages.Package) {
   265  			if !emit.visitPackage(imp, pkgMap) {
   266  				ok = false
   267  			}
   268  		})
   269  	}
   270  
   271  	return ok
   272  }
   273  
   274  func rooted(pkg string, list []string) bool {
   275  	for _, s := range list {
   276  		if pkg == s || strings.HasPrefix(pkg, s+"/") {
   277  			return true
   278  		}
   279  	}
   280  	return false
   281  }
   282  
   283  func visitEach(all map[string]*packages.Package, fn func(pkg *packages.Package)) {
   284  	for _, k := range keys(all) {
   285  		fn(all[k])
   286  	}
   287  }
   288  
   289  func keys(m map[string]*packages.Package) []string {
   290  	sl := make([]string, 0, len(m))
   291  	for k := range m {
   292  		sl = append(sl, k)
   293  	}
   294  	sort.Strings(sl)
   295  	return sl
   296  }
   297  
   298  func maybeRelative(path, relativeTo string) (string, bool) {
   299  	if path == relativeTo || strings.HasPrefix(path, relativeTo+"/") {
   300  		return "." + strings.TrimPrefix(path, relativeTo), true
   301  	}
   302  	return path, false
   303  }
   304  
   305  func (emit emitter) emitMake(out io.Writer, pkgMap map[string]*packages.Package) {
   306  	// Emit helpful macros for callers.
   307  	fmt.Fprintf(out, "# This file is autogenerated.\n")
   308  	fmt.Fprintf(out, "\n")
   309  	fmt.Fprintf(out, "# This variable may be used with $(call). It takes a single argument\n")
   310  	fmt.Fprintf(out, "# which is the Go package name, e.g. \"example.com/pkg\".\n")
   311  	fmt.Fprintf(out, "GO2MAKE_BY_PKG = %s/by-pkg/$(1)/_pkg\n", emit.stateDir)
   312  	fmt.Fprintf(out, "\n")
   313  	fmt.Fprintf(out, "# This variable may be used with $(call). It takes a single argument\n")
   314  	fmt.Fprintf(out, "# which is the local package path, e.g. \"path/pkg\" or \"./path/pkg\".\n")
   315  	fmt.Fprintf(out, "GO2MAKE_BY_PATH = %s/by-path/./$(patsubst ./%%,%%,$(1))/_pkg\n", emit.stateDir)
   316  	fmt.Fprintf(out, "\n")
   317  
   318  	// Emit rules for each package.
   319  	visitEach(pkgMap, func(pkg *packages.Package) {
   320  
   321  		codeDir := ""
   322  		isRel := false
   323  		if len(pkg.GoFiles) > 0 {
   324  			codeDir, isRel = maybeRelative(filepath.Dir(pkg.GoFiles[0]), emit.relPath)
   325  			// Emit a rule to represent changes to the directory contents.
   326  			// This rule will be evaluated whenever the code-directory is
   327  			// newer than the saved file-list, but the file-list will only get
   328  			// touched (triggering downstream rebuilds) if the set of files
   329  			// actually changes.
   330  			fmt.Fprintf(out, "%s/by-pkg/%s/_files: %s/\n", emit.stateDir, pkg.PkgPath, codeDir)
   331  			fmt.Fprintf(out, "\t@mkdir -p $(@D)\n")
   332  			fmt.Fprintf(out, "\t@ls $</*.go | LC_ALL=C sort > $@.tmp\n")
   333  			fmt.Fprintf(out, "\t@if ! cmp -s $@.tmp $@; then \\\n")
   334  			fmt.Fprintf(out, "\t    cat $@.tmp > $@; \\\n")
   335  			fmt.Fprintf(out, "\tfi\n")
   336  			fmt.Fprintf(out, "\t@rm -f $@.tmp\n")
   337  			fmt.Fprintf(out, "\n")
   338  		}
   339  
   340  		// Emit a rule to represent the whole package.  This uses a file,
   341  		// rather than the directory itself, to avoid nested dir creation
   342  		// changing the directory's timestamp.
   343  		fmt.Fprintf(out, "%s/by-pkg/%s/_pkg:", emit.stateDir, pkg.PkgPath)
   344  		if len(pkg.GoFiles) > 0 {
   345  			fmt.Fprintf(out, " %s/by-pkg/%s/_files", emit.stateDir, pkg.PkgPath)
   346  		}
   347  		for _, f := range pkg.GoFiles {
   348  			rel, _ := maybeRelative(f, emit.relPath)
   349  			fmt.Fprintf(out, " \\\n  %s", rel)
   350  		}
   351  		for _, imp := range keys(pkg.Imports) {
   352  			if pkgMap[pkg.Imports[imp].PkgPath] != nil {
   353  				fmt.Fprintf(out, " \\\n  %s/by-pkg/%s/_pkg", emit.stateDir, pkg.Imports[imp].PkgPath)
   354  			}
   355  		}
   356  		fmt.Fprintf(out, "\n")
   357  		fmt.Fprintf(out, "\t@mkdir -p $(@D)\n")
   358  		fmt.Fprintf(out, "\t@touch $@\n")
   359  		fmt.Fprintf(out, "\n")
   360  
   361  		if isRel {
   362  			// Emit a rule to represent the package, but by a relative path.  This
   363  			// is useful when you know the path to something but maybe not which Go
   364  			// package it is (e.g. you have a bunch of packages).  Like the by-pkg
   365  			// equivalent, this uses a file, to avoid nested dir creation changing
   366  			// the directory's timestamp.
   367  			fmt.Fprintf(out, "%s/by-path/%s/_pkg: %s/by-pkg/%s/_pkg\n", emit.stateDir, codeDir, emit.stateDir, pkg.PkgPath)
   368  			fmt.Fprintf(out, "\t@mkdir -p $(@D)\n")
   369  			fmt.Fprintf(out, "\t@touch $@\n")
   370  			fmt.Fprintf(out, "\n")
   371  		}
   372  	})
   373  }
   374  
   375  func (emit emitter) emitJSON(out io.Writer, pkgMap map[string]*packages.Package) {
   376  	jb, err := json.Marshal(pkgMap)
   377  	if err != nil {
   378  		fmt.Fprintf(os.Stderr, "JSON error: %v", err)
   379  		os.Exit(1)
   380  	}
   381  	fmt.Fprintln(out, string(jb))
   382  }