123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- /*
- Copyright 2017 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package main
- import (
- "bytes"
- "flag"
- "fmt"
- "io/ioutil"
- "os"
- "path/filepath"
- "reflect"
- "regexp"
- "sort"
- "github.com/bazelbuild/buildtools/build"
- "k8s.io/klog"
- )
- const (
- automanagedTag = "automanaged"
- )
- var (
- root = flag.String("root", ".", "root of go source")
- dryRun = flag.Bool("dry-run", false, "run in dry mode")
- printDiff = flag.Bool("print-diff", false, "print diff to stdout")
- validate = flag.Bool("validate", false, "run in dry mode and exit nonzero if any BUILD files need to be updated")
- cfgPath = flag.String("cfg-path", ".kazelcfg.json", "path to kazel config (relative paths interpreted relative to -repo.")
- )
- func main() {
- flag.Parse()
- flag.Set("alsologtostderr", "true")
- if *root == "" {
- klog.Fatalf("-root argument is required")
- }
- if *validate {
- *dryRun = true
- }
- v, err := newVendorer(*root, *cfgPath, *dryRun)
- if err != nil {
- klog.Fatalf("unable to build vendorer: %v", err)
- }
- if err = os.Chdir(v.root); err != nil {
- klog.Fatalf("cannot chdir into root %q: %v", v.root, err)
- }
- if v.cfg.ManageGoRules {
- klog.Fatalf("kazel no longer supports managing Go rules")
- }
- wroteGenerated := false
- if wroteGenerated, err = v.walkGenerated(); err != nil {
- klog.Fatalf("err walking generated: %v", err)
- }
- if _, err = v.walkSource("."); err != nil {
- klog.Fatalf("err walking source: %v", err)
- }
- written := 0
- if written, err = v.reconcileAllRules(); err != nil {
- klog.Fatalf("err reconciling rules: %v", err)
- }
- if wroteGenerated {
- written++
- }
- if *validate && written > 0 {
- fmt.Fprintf(os.Stderr, "\n%d BUILD files not up-to-date.\n", written)
- os.Exit(1)
- }
- }
- // Vendorer collects context, configuration, and cache while walking the tree.
- type Vendorer struct {
- skippedPaths []*regexp.Regexp
- skippedK8sCodegenPaths []*regexp.Regexp
- dryRun bool
- root string
- cfg *Cfg
- newRules map[string][]*build.Rule // package path -> list of rules to add or update
- managedAttrs []string // which rule attributes kazel will overwrite
- }
- func newVendorer(root, cfgPath string, dryRun bool) (*Vendorer, error) {
- absRoot, err := filepath.Abs(root)
- if err != nil {
- return nil, fmt.Errorf("could not get absolute path: %v", err)
- }
- if !filepath.IsAbs(cfgPath) {
- cfgPath = filepath.Join(absRoot, cfgPath)
- }
- cfg, err := ReadCfg(cfgPath)
- if err != nil {
- return nil, err
- }
- v := Vendorer{
- dryRun: dryRun,
- root: absRoot,
- cfg: cfg,
- newRules: make(map[string][]*build.Rule),
- managedAttrs: []string{"srcs"},
- }
- builtIn, err := compileSkippedPaths([]string{"^\\.git", "^bazel-*"})
- if err != nil {
- return nil, err
- }
- sp, err := compileSkippedPaths(cfg.SkippedPaths)
- if err != nil {
- return nil, err
- }
- sp = append(builtIn, sp...)
- v.skippedPaths = sp
- sop, err := compileSkippedPaths(cfg.SkippedK8sCodegenPaths)
- if err != nil {
- return nil, err
- }
- v.skippedK8sCodegenPaths = append(sop, sp...)
- return &v, nil
- }
- func writeRules(file *build.File, rules []*build.Rule) {
- for _, rule := range rules {
- file.Stmt = append(file.Stmt, rule.Call)
- }
- }
- func (v *Vendorer) addRules(pkgPath string, rules []*build.Rule) {
- cleanPath := filepath.Clean(pkgPath)
- v.newRules[cleanPath] = append(v.newRules[cleanPath], rules...)
- }
- func (v *Vendorer) reconcileAllRules() (int, error) {
- var paths []string
- for path := range v.newRules {
- paths = append(paths, path)
- }
- sort.Strings(paths)
- written := 0
- for _, path := range paths {
- w, err := ReconcileRules(path, v.newRules[path], v.managedAttrs, v.dryRun)
- if w {
- written++
- }
- if err != nil {
- return written, err
- }
- }
- return written, nil
- }
- // addCommentBefore adds a whole-line comment before the provided Expr.
- func addCommentBefore(e build.Expr, comment string) {
- c := e.Comment()
- c.Before = append(c.Before, build.Comment{Token: fmt.Sprintf("# %s", comment)})
- }
- // varExpr creates a variable expression of the form "name = expr".
- // v will be converted into an appropriate Expr using asExpr.
- // The optional description will be included as a comment before the expression.
- func varExpr(name, desc string, v interface{}) build.Expr {
- e := &build.BinaryExpr{
- X: &build.LiteralExpr{Token: name},
- Op: "=",
- Y: asExpr(v),
- }
- if desc != "" {
- addCommentBefore(e, desc)
- }
- return e
- }
- // rvSliceLessFunc returns a function that can be used with sort.Slice() or sort.SliceStable()
- // to sort a slice of reflect.Values.
- // It sorts ints and floats as their native kinds, and everything else as a string.
- func rvSliceLessFunc(k reflect.Kind, vs []reflect.Value) func(int, int) bool {
- switch k {
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
- reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- return func(i, j int) bool { return vs[i].Int() < vs[j].Int() }
- case reflect.Float32, reflect.Float64:
- return func(i, j int) bool { return vs[i].Float() < vs[j].Float() }
- default:
- return func(i, j int) bool {
- return fmt.Sprintf("%v", vs[i]) < fmt.Sprintf("%v", vs[j])
- }
- }
- }
- // asExpr converts a native Go type into the equivalent Starlark expression using reflection.
- // The keys of maps will be sorted for reproducibility.
- func asExpr(e interface{}) build.Expr {
- rv := reflect.ValueOf(e)
- switch rv.Kind() {
- case reflect.Bool:
- return &build.LiteralExpr{Token: fmt.Sprintf("%t", e)}
- case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
- reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
- return &build.LiteralExpr{Token: fmt.Sprintf("%d", e)}
- case reflect.Float32, reflect.Float64:
- return &build.LiteralExpr{Token: fmt.Sprintf("%g", e)}
- case reflect.String:
- return &build.StringExpr{Value: e.(string)}
- case reflect.Slice, reflect.Array:
- var list []build.Expr
- for i := 0; i < rv.Len(); i++ {
- list = append(list, asExpr(rv.Index(i).Interface()))
- }
- return &build.ListExpr{List: list}
- case reflect.Map:
- var list []build.Expr
- keys := rv.MapKeys()
- sort.SliceStable(keys, rvSliceLessFunc(rv.Type().Key().Kind(), keys))
- for _, key := range keys {
- list = append(list, &build.KeyValueExpr{
- Key: asExpr(key.Interface()),
- Value: asExpr(rv.MapIndex(key).Interface()),
- })
- }
- return &build.DictExpr{List: list}
- default:
- klog.Fatalf("unhandled kind: %q for value: %q", rv.Kind(), rv)
- return nil
- }
- }
- func newRule(rt, name string, attrs map[string]build.Expr) *build.Rule {
- rule := &build.Rule{
- Call: &build.CallExpr{
- X: &build.LiteralExpr{Token: rt},
- },
- }
- rule.SetAttr("name", asExpr(name))
- for k, v := range attrs {
- rule.SetAttr(k, v)
- }
- rule.SetAttr("tags", asExpr([]string{automanagedTag}))
- return rule
- }
- // findBuildFile determines the name of a preexisting BUILD file, returning
- // a default if no such file exists.
- func findBuildFile(pkgPath string) (bool, string) {
- options := []string{"BUILD.bazel", "BUILD"}
- for _, b := range options {
- path := filepath.Join(pkgPath, b)
- info, err := os.Stat(path)
- if err == nil && !info.IsDir() {
- return true, path
- }
- }
- return false, filepath.Join(pkgPath, "BUILD.bazel")
- }
- // ReconcileRules reconciles, simplifies, and writes the rules for the specified package, adding
- // additional dependency rules as needed.
- func ReconcileRules(pkgPath string, rules []*build.Rule, managedAttrs []string, dryRun bool) (bool, error) {
- _, path := findBuildFile(pkgPath)
- info, err := os.Stat(path)
- if err != nil && os.IsNotExist(err) {
- f := &build.File{}
- writeRules(f, rules)
- return writeFile(path, f, nil, false, dryRun)
- } else if err != nil {
- return false, err
- }
- if info.IsDir() {
- return false, fmt.Errorf("%q cannot be a directory", path)
- }
- b, err := ioutil.ReadFile(path)
- if err != nil {
- return false, err
- }
- f, err := build.Parse(path, b)
- if err != nil {
- return false, err
- }
- oldRules := make(map[string]*build.Rule)
- for _, r := range f.Rules("") {
- oldRules[r.Name()] = r
- }
- for _, r := range rules {
- o, ok := oldRules[r.Name()]
- if !ok {
- f.Stmt = append(f.Stmt, r.Call)
- continue
- }
- if !RuleIsManaged(o) {
- continue
- }
- reconcileAttr := func(o, n *build.Rule, name string) {
- if e := n.Attr(name); e != nil {
- o.SetAttr(name, e)
- } else {
- o.DelAttr(name)
- }
- }
- for _, attr := range managedAttrs {
- reconcileAttr(o, r, attr)
- }
- delete(oldRules, r.Name())
- }
- for _, r := range oldRules {
- if !RuleIsManaged(r) {
- continue
- }
- f.DelRules(r.Kind(), r.Name())
- }
- return writeFile(path, f, nil, true, dryRun)
- }
- // RuleIsManaged returns whether the provided rule is managed by this tool,
- // based on the tags set on the rule.
- func RuleIsManaged(r *build.Rule) bool {
- for _, tag := range r.AttrStrings("tags") {
- if tag == automanagedTag {
- return true
- }
- }
- return false
- }
- // writeFile writes out f to path, prepending boilerplate to the output.
- // If exists is true, compares against the existing file specified by path,
- // returning false if there are no changes.
- // Otherwise, returns true.
- // If dryRun is false, no files are actually changed; otherwise, the file will be written.
- func writeFile(path string, f *build.File, boilerplate []byte, exists, dryRun bool) (bool, error) {
- var info build.RewriteInfo
- build.Rewrite(f, &info)
- var out []byte
- out = append(out, boilerplate...)
- out = append(out, build.Format(f)...)
- if exists {
- orig, err := ioutil.ReadFile(path)
- if err != nil {
- return false, err
- }
- if bytes.Compare(orig, out) == 0 {
- return false, nil
- }
- if *printDiff {
- Diff(orig, out)
- }
- }
- if dryRun {
- fmt.Fprintf(os.Stderr, "DRY-RUN: wrote %q\n", path)
- return true, nil
- }
- werr := ioutil.WriteFile(path, out, 0644)
- if werr == nil {
- fmt.Fprintf(os.Stderr, "wrote %q\n", path)
- }
- return werr == nil, werr
- }
- func compileSkippedPaths(skippedPaths []string) ([]*regexp.Regexp, error) {
- regexPaths := []*regexp.Regexp{}
- for _, sp := range skippedPaths {
- r, err := regexp.Compile(sp)
- if err != nil {
- return nil, err
- }
- regexPaths = append(regexPaths, r)
- }
- return regexPaths, nil
- }
|