1
15
16 package main
17
18 import (
19 "bytes"
20 "errors"
21 "flag"
22 "fmt"
23 "log"
24 "os"
25 "path"
26 "path/filepath"
27 "regexp"
28 "strings"
29
30 "github.com/bazelbuild/bazel-gazelle/internal/wspace"
31 "github.com/bazelbuild/bazel-gazelle/label"
32 "github.com/bazelbuild/bazel-gazelle/pathtools"
33 "github.com/bazelbuild/buildtools/build"
34 )
35
36 const usageMessage = `usage: move_labels [-repo_root=root] [-from=dir] -to=dir
37
38 move_labels updates Bazel labels in a tree containing build files after the
39 tree has been moved to a new location. This is useful for vendoring
40 repositories that already have Bazel build files.
41
42 `
43
44 func main() {
45 log.SetPrefix("move_labels: ")
46 log.SetFlags(0)
47 if err := run(os.Args[1:]); err != nil {
48 log.Fatal(err)
49 }
50 }
51
52 func run(args []string) error {
53 c, err := newConfiguration(args)
54 if err != nil {
55 return err
56 }
57
58 files, err := moveLabelsInDir(c)
59 if err != nil {
60 return err
61 }
62
63 var errs errorList
64 for _, file := range files {
65 content := build.Format(file)
66 if err := os.WriteFile(file.Path, content, 0o666); err != nil {
67 errs = append(errs, err)
68 }
69 }
70 if len(errs) > 0 {
71 return errs
72 }
73 return nil
74 }
75
76 func moveLabelsInDir(c *configuration) ([]*build.File, error) {
77 toRel, err := filepath.Rel(c.repoRoot, c.to)
78 if err != nil {
79 return nil, err
80 }
81 toRel = filepath.ToSlash(toRel)
82
83 var files []*build.File
84 var errors errorList
85 err = filepath.Walk(c.to, func(path string, info os.FileInfo, err error) error {
86 if err != nil {
87 return err
88 }
89 if name := info.Name(); name != "BUILD" && name != "BUILD.bazel" {
90 return nil
91 }
92 content, err := os.ReadFile(path)
93 if err != nil {
94 errors = append(errors, err)
95 return nil
96 }
97 file, err := build.Parse(path, content)
98 if err != nil {
99 errors = append(errors, err)
100 return nil
101 }
102 moveLabelsInFile(file, c.from, toRel)
103 files = append(files, file)
104 return nil
105 })
106 if err != nil {
107 return nil, err
108 }
109 if len(errors) > 0 {
110 return nil, errors
111 }
112 return files, nil
113 }
114
115 func moveLabelsInFile(file *build.File, from, to string) {
116 build.Edit(file, func(x build.Expr, _ []build.Expr) build.Expr {
117 str, ok := x.(*build.StringExpr)
118 if !ok {
119 return nil
120 }
121 label := str.Value
122 var moved string
123 if strings.Contains(label, "$(location") {
124 moved = moveLocations(from, to, label)
125 } else {
126 moved = moveLabel(from, to, label)
127 }
128 if moved == label {
129 return nil
130 }
131 return &build.StringExpr{Value: moved}
132 })
133 }
134
135 func moveLabel(from, to, str string) string {
136 l, err := label.Parse(str)
137 if err != nil {
138 return str
139 }
140 if l.Relative || l.Repo != "" ||
141 l.Pkg == "visibility" || l.Pkg == "conditions" ||
142 pathtools.HasPrefix(l.Pkg, to) || !pathtools.HasPrefix(l.Pkg, from) {
143 return str
144 }
145 l.Pkg = path.Join(to, pathtools.TrimPrefix(l.Pkg, from))
146 return l.String()
147 }
148
149 var locationsRegexp = regexp.MustCompile(`\$\(locations?\s*([^)]*)\)`)
150
151
152 func moveLocations(from, to, str string) string {
153 matches := locationsRegexp.FindAllStringSubmatchIndex(str, -1)
154 buf := new(bytes.Buffer)
155 pos := 0
156 for _, match := range matches {
157 buf.WriteString(str[pos:match[2]])
158 label := str[match[2]:match[3]]
159 moved := moveLabel(from, to, label)
160 buf.WriteString(moved)
161 buf.WriteString(str[match[3]:match[1]])
162 pos = match[1]
163 }
164 buf.WriteString(str[pos:])
165 return buf.String()
166 }
167
168 type configuration struct {
169
170
171 repoRoot string
172
173
174
175
176 from string
177
178
179
180 to string
181 }
182
183 func newConfiguration(args []string) (*configuration, error) {
184 var err error
185 c := &configuration{}
186 fs := flag.NewFlagSet("move_labels", flag.ContinueOnError)
187 fs.Usage = func() {}
188 fs.StringVar(&c.repoRoot, "repo_root", "", "repository root directory; inferred to be parent directory containing WORKSPACE file")
189 fs.StringVar(&c.from, "from", "", "original location of build files, formatted as a slash-separated relative path from the original repository root")
190 fs.StringVar(&c.to, "to", "", "new location of build files, formatted as a file system path")
191 if err := fs.Parse(args); err != nil {
192 if err == flag.ErrHelp {
193 fmt.Fprint(os.Stderr, usageMessage)
194 fs.PrintDefaults()
195 os.Exit(0)
196 }
197
198 return nil, errors.New("Try -help for more information")
199 }
200
201 if c.repoRoot == "" {
202 c.repoRoot, err = findRepoRoot()
203 if err != nil {
204 return nil, err
205 }
206 }
207 c.repoRoot, err = filepath.Abs(c.repoRoot)
208 if err != nil {
209 return nil, err
210 }
211
212 if c.to == "" {
213 return nil, errors.New("-to must be specified. Try -help for more information.")
214 }
215 c.to, err = filepath.Abs(c.to)
216 if err != nil {
217 return nil, err
218 }
219
220 if len(fs.Args()) != 0 {
221 return nil, errors.New("No positional arguments expected. Try -help for more information.")
222 }
223
224 return c, nil
225 }
226
227 func findRepoRoot() (string, error) {
228 dir, err := os.Getwd()
229 if err != nil {
230 return "", err
231 }
232 root, err := wspace.FindRepoRoot(dir)
233 if err != nil {
234 return "", fmt.Errorf("could not find WORKSPACE file. -repo_root must be set explicitly")
235 }
236 return root, nil
237 }
238
239 type errorList []error
240
241 func (e errorList) Error() string {
242 buf := new(bytes.Buffer)
243 for _, err := range e {
244 fmt.Fprintln(buf, err.Error())
245 }
246 return buf.String()
247 }
248
View as plain text