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 }