blob: 14cdacfcddf9d360d341e02c4a09248d8119ad13 [file] [log] [blame]
Colin Cross8e0c5112015-01-23 14:15:10 -08001// Copyright 2014 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Jamie Gennisdebef532014-10-24 10:42:57 -070015package pathtools
16
17import (
Colin Cross25236982021-04-05 17:20:34 -070018 "encoding/json"
Colin Cross7bac3c62015-04-23 15:45:25 -070019 "errors"
Colin Crossced59ee2016-11-05 08:37:33 -070020 "fmt"
Colin Cross127d2ea2016-11-01 11:10:51 -070021 "io/ioutil"
Colin Cross4b793e52015-04-23 13:29:28 -070022 "os"
Jamie Gennisdebef532014-10-24 10:42:57 -070023 "path/filepath"
24 "strings"
25)
26
Colin Cross7e6f6b72021-04-12 18:46:31 -070027// BPGlobArgumentVersion is used to abort argument parsing early when the bpglob argument format
28// has changed but soong_build hasn't had a chance to rerun yet to update build-globs.ninja.
29// Increment it manually when changing the bpglob argument format. It is located here because
30// pathtools is the only package that is shared between bpglob and bootstrap.
Colin Cross25236982021-04-05 17:20:34 -070031const BPGlobArgumentVersion = 2
Colin Cross7e6f6b72021-04-12 18:46:31 -070032
Colin Crossa26ae892019-03-25 16:14:37 -070033var GlobMultipleRecursiveErr = errors.New("pattern contains multiple '**'")
34var GlobLastRecursiveErr = errors.New("pattern has '**' as last path element")
35var GlobInvalidRecursiveErr = errors.New("pattern contains other characters between '**' and path separator")
Colin Cross7bac3c62015-04-23 15:45:25 -070036
Colin Cross67c99252021-04-07 14:28:13 -070037// GlobResult is a container holding the results of a call to Glob.
38type GlobResult struct {
39 // Pattern is the pattern that was passed to Glob.
40 Pattern string
41 // Excludes is the list of excludes that were passed to Glob.
42 Excludes []string
43
44 // Matches is the list of files or directories that matched the pattern but not the excludes.
45 Matches []string
46
47 // Deps is the list of files or directories that must be depended on to regenerate the glob.
48 Deps []string
49}
50
51// FileList returns the list of files matched by a glob for writing to an output file.
52func (result GlobResult) FileList() []byte {
53 return []byte(strings.Join(result.Matches, "\n") + "\n")
54}
55
Colin Cross25236982021-04-05 17:20:34 -070056// MultipleGlobResults is a list of GlobResult structs.
57type MultipleGlobResults []GlobResult
58
59// FileList returns the list of files matched by a list of multiple globs for writing to an output file.
60func (results MultipleGlobResults) FileList() []byte {
61 multipleMatches := make([][]string, len(results))
62 for i, result := range results {
63 multipleMatches[i] = result.Matches
64 }
65 buf, err := json.Marshal(multipleMatches)
66 if err != nil {
67 panic(fmt.Errorf("failed to marshal glob results to json: %w", err))
68 }
69 return buf
70}
71
72// Deps returns the deps from all of the GlobResults.
73func (results MultipleGlobResults) Deps() []string {
74 var deps []string
75 for _, result := range results {
76 deps = append(deps, result.Deps...)
77 }
78 return deps
79}
80
Dan Willemsenb6c90232018-02-23 14:49:45 -080081// Glob returns the list of files and directories that match the given pattern
82// but do not match the given exclude patterns, along with the list of
83// directories and other dependencies that were searched to construct the file
84// list. The supported glob and exclude patterns are equivalent to
85// filepath.Glob, with an extension that recursive glob (** matching zero or
86// more complete path entries) is supported. Any directories in the matches
87// list will have a '/' suffix.
Colin Cross127d2ea2016-11-01 11:10:51 -070088//
89// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps
90// should be used instead, as they will automatically set up dependencies
91// to rerun the primary builder when the list of matching files changes.
Colin Cross67c99252021-04-07 14:28:13 -070092func Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (GlobResult, error) {
Colin Crosse98d0822018-09-21 15:30:13 -070093 return startGlob(OsFs, pattern, excludes, follow)
Colin Crossb519a7e2017-02-01 13:21:35 -080094}
95
Colin Crosse98d0822018-09-21 15:30:13 -070096func startGlob(fs FileSystem, pattern string, excludes []string,
Colin Cross67c99252021-04-07 14:28:13 -070097 follow ShouldFollowSymlinks) (GlobResult, error) {
Colin Crosse98d0822018-09-21 15:30:13 -070098
Colin Cross7d2ee0d2015-06-18 10:48:15 -070099 if filepath.Base(pattern) == "**" {
Colin Cross67c99252021-04-07 14:28:13 -0700100 return GlobResult{}, GlobLastRecursiveErr
Colin Cross7bac3c62015-04-23 15:45:25 -0700101 }
102
Colin Cross67c99252021-04-07 14:28:13 -0700103 matches, deps, err := glob(fs, pattern, false, follow)
104
Colin Cross5d6d4c72015-04-23 17:56:11 -0700105 if err != nil {
Colin Cross67c99252021-04-07 14:28:13 -0700106 return GlobResult{}, err
Colin Cross5d6d4c72015-04-23 17:56:11 -0700107 }
108
109 matches, err = filterExcludes(matches, excludes)
110 if err != nil {
Colin Cross67c99252021-04-07 14:28:13 -0700111 return GlobResult{}, err
Colin Cross5d6d4c72015-04-23 17:56:11 -0700112 }
113
Dan Willemsen75fec882017-06-22 17:01:24 -0700114 // If the pattern has wildcards, we added dependencies on the
115 // containing directories to know about changes.
116 //
117 // If the pattern didn't have wildcards, and didn't find matches, the
118 // most specific found directories were added.
119 //
120 // But if it didn't have wildcards, and did find a match, no
121 // dependencies were added, so add the match itself to detect when it
122 // is removed.
123 if !isWild(pattern) {
124 deps = append(deps, matches...)
125 }
126
Dan Willemsenb6c90232018-02-23 14:49:45 -0800127 for i, match := range matches {
Colin Crosse535c972021-01-20 10:57:58 -0800128 var info os.FileInfo
129 if follow == DontFollowSymlinks {
130 info, err = fs.Lstat(match)
131 } else {
132 info, err = fs.Stat(match)
133 }
Colin Crosse3b7ec32018-09-20 14:36:10 -0700134 if err != nil {
Colin Cross67c99252021-04-07 14:28:13 -0700135 return GlobResult{}, err
Colin Crosse3b7ec32018-09-20 14:36:10 -0700136 }
137
Colin Cross4604a812021-01-20 13:44:53 -0800138 if info.IsDir() {
Colin Crosse535c972021-01-20 10:57:58 -0800139 matches[i] = match + "/"
Dan Willemsenb6c90232018-02-23 14:49:45 -0800140 }
141 }
142
Colin Cross67c99252021-04-07 14:28:13 -0700143 return GlobResult{
144 Pattern: pattern,
145 Excludes: excludes,
146 Matches: matches,
147 Deps: deps,
148 }, nil
Colin Cross7bac3c62015-04-23 15:45:25 -0700149}
150
151// glob is a recursive helper function to handle globbing each level of the pattern individually,
152// allowing searched directories to be tracked. Also handles the recursive glob pattern, **.
Colin Crosse98d0822018-09-21 15:30:13 -0700153func glob(fs FileSystem, pattern string, hasRecursive bool,
154 follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
155
Colin Cross7bac3c62015-04-23 15:45:25 -0700156 if !isWild(pattern) {
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700157 // If there are no wilds in the pattern, check whether the file exists or not.
158 // Uses filepath.Glob instead of manually statting to get consistent results.
159 pattern = filepath.Clean(pattern)
Colin Crossb519a7e2017-02-01 13:21:35 -0800160 matches, err = fs.glob(pattern)
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700161 if err != nil {
162 return matches, dirs, err
163 }
164
165 if len(matches) == 0 {
166 // Some part of the non-wild pattern didn't exist. Add the last existing directory
167 // as a dependency.
168 var matchDirs []string
169 for len(matchDirs) == 0 {
Colin Crossa26ae892019-03-25 16:14:37 -0700170 pattern = filepath.Dir(pattern)
Colin Crossb519a7e2017-02-01 13:21:35 -0800171 matchDirs, err = fs.glob(pattern)
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700172 if err != nil {
173 return matches, dirs, err
174 }
175 }
176 dirs = append(dirs, matchDirs...)
177 }
Colin Cross4b793e52015-04-23 13:29:28 -0700178 return matches, dirs, err
Jamie Gennisdebef532014-10-24 10:42:57 -0700179 }
180
Colin Cross4b793e52015-04-23 13:29:28 -0700181 dir, file := saneSplit(pattern)
Colin Cross7bac3c62015-04-23 15:45:25 -0700182
183 if file == "**" {
184 if hasRecursive {
185 return matches, dirs, GlobMultipleRecursiveErr
186 }
187 hasRecursive = true
Colin Crossa26ae892019-03-25 16:14:37 -0700188 } else if strings.Contains(file, "**") {
189 return matches, dirs, GlobInvalidRecursiveErr
Colin Cross7bac3c62015-04-23 15:45:25 -0700190 }
191
Colin Crosse98d0822018-09-21 15:30:13 -0700192 dirMatches, dirs, err := glob(fs, dir, hasRecursive, follow)
Colin Cross7bac3c62015-04-23 15:45:25 -0700193 if err != nil {
194 return nil, nil, err
195 }
196
Colin Cross4b793e52015-04-23 13:29:28 -0700197 for _, m := range dirMatches {
Colin Crosse3b7ec32018-09-20 14:36:10 -0700198 isDir, err := fs.IsDir(m)
199 if os.IsNotExist(err) {
200 if isSymlink, _ := fs.IsSymlink(m); isSymlink {
201 return nil, nil, fmt.Errorf("dangling symlink: %s", m)
202 }
203 }
204 if err != nil {
Colin Crossced59ee2016-11-05 08:37:33 -0700205 return nil, nil, fmt.Errorf("unexpected error after glob: %s", err)
Colin Crosse3b7ec32018-09-20 14:36:10 -0700206 }
207
208 if isDir {
Colin Cross7bac3c62015-04-23 15:45:25 -0700209 if file == "**" {
Colin Crosse98d0822018-09-21 15:30:13 -0700210 recurseDirs, err := fs.ListDirsRecursive(m, follow)
Colin Cross7bac3c62015-04-23 15:45:25 -0700211 if err != nil {
212 return nil, nil, err
213 }
214 matches = append(matches, recurseDirs...)
215 } else {
216 dirs = append(dirs, m)
Colin Crossa4706122018-09-19 13:36:42 -0700217 newMatches, err := fs.glob(filepath.Join(MatchEscape(m), file))
Colin Cross7bac3c62015-04-23 15:45:25 -0700218 if err != nil {
219 return nil, nil, err
220 }
Dan Willemsen53f49502016-07-25 18:40:02 -0700221 if file[0] != '.' {
222 newMatches = filterDotFiles(newMatches)
223 }
Colin Cross7bac3c62015-04-23 15:45:25 -0700224 matches = append(matches, newMatches...)
Jamie Gennisdebef532014-10-24 10:42:57 -0700225 }
226 }
227 }
228
Colin Cross4b793e52015-04-23 13:29:28 -0700229 return matches, dirs, nil
Jamie Gennisdebef532014-10-24 10:42:57 -0700230}
231
Colin Cross7bac3c62015-04-23 15:45:25 -0700232// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
Colin Cross4b793e52015-04-23 13:29:28 -0700233// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
Colin Cross7bac3c62015-04-23 15:45:25 -0700234// not "/". Returns ".", "" if path is "."
Colin Cross4b793e52015-04-23 13:29:28 -0700235func saneSplit(path string) (dir, file string) {
Colin Cross7bac3c62015-04-23 15:45:25 -0700236 if path == "." {
237 return ".", ""
238 }
Colin Cross4b793e52015-04-23 13:29:28 -0700239 dir, file = filepath.Split(path)
240 switch dir {
241 case "":
242 dir = "."
243 case "/":
244 // Nothing
245 default:
246 dir = dir[:len(dir)-1]
Jamie Gennisdebef532014-10-24 10:42:57 -0700247 }
Colin Cross4b793e52015-04-23 13:29:28 -0700248 return dir, file
Jamie Gennisdebef532014-10-24 10:42:57 -0700249}
250
251func isWild(pattern string) bool {
252 return strings.ContainsAny(pattern, "*?[")
253}
Jean-Francois Dupuis418d5c52015-02-05 20:01:49 -0800254
Colin Cross5d6d4c72015-04-23 17:56:11 -0700255// Filters the strings in matches based on the glob patterns in excludes. Hierarchical (a/*) and
256// recursive (**) glob patterns are supported.
257func filterExcludes(matches []string, excludes []string) ([]string, error) {
258 if len(excludes) == 0 {
259 return matches, nil
260 }
261
262 var ret []string
263matchLoop:
264 for _, m := range matches {
265 for _, e := range excludes {
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800266 exclude, err := Match(e, m)
Colin Cross5d6d4c72015-04-23 17:56:11 -0700267 if err != nil {
268 return nil, err
269 }
270 if exclude {
271 continue matchLoop
272 }
273 }
274 ret = append(ret, m)
275 }
276
277 return ret, nil
278}
279
Dan Willemsen53f49502016-07-25 18:40:02 -0700280// filterDotFiles filters out files that start with '.'
281func filterDotFiles(matches []string) []string {
282 ret := make([]string, 0, len(matches))
283
284 for _, match := range matches {
285 _, name := filepath.Split(match)
286 if name[0] == '.' {
287 continue
288 }
289 ret = append(ret, match)
290 }
291
292 return ret
293}
294
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800295// Match returns true if name matches pattern using the same rules as filepath.Match, but supporting
Colin Crossa26ae892019-03-25 16:14:37 -0700296// recursive globs (**).
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800297func Match(pattern, name string) (bool, error) {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700298 if filepath.Base(pattern) == "**" {
299 return false, GlobLastRecursiveErr
300 }
301
Colin Crossc0c3b0f2018-07-13 21:17:48 -0700302 patternDir := pattern[len(pattern)-1] == '/'
303 nameDir := name[len(name)-1] == '/'
304
305 if patternDir != nameDir {
306 return false, nil
307 }
308
309 if nameDir {
310 name = name[:len(name)-1]
311 pattern = pattern[:len(pattern)-1]
312 }
313
Colin Cross5d6d4c72015-04-23 17:56:11 -0700314 for {
315 var patternFile, nameFile string
Colin Crossa26ae892019-03-25 16:14:37 -0700316 pattern, patternFile = filepath.Dir(pattern), filepath.Base(pattern)
Colin Cross5d6d4c72015-04-23 17:56:11 -0700317
318 if patternFile == "**" {
Colin Crossa26ae892019-03-25 16:14:37 -0700319 if strings.Contains(pattern, "**") {
320 return false, GlobMultipleRecursiveErr
321 }
322 // Test if the any prefix of name matches the part of the pattern before **
323 for {
324 if name == "." || name == "/" {
325 return name == pattern, nil
326 }
327 if match, err := filepath.Match(pattern, name); err != nil {
328 return false, err
329 } else if match {
330 return true, nil
331 }
332 name = filepath.Dir(name)
333 }
334 } else if strings.Contains(patternFile, "**") {
335 return false, GlobInvalidRecursiveErr
Colin Cross5d6d4c72015-04-23 17:56:11 -0700336 }
337
Colin Crossa26ae892019-03-25 16:14:37 -0700338 name, nameFile = filepath.Dir(name), filepath.Base(name)
339
340 if nameFile == "." && patternFile == "." {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700341 return true, nil
Colin Crossa26ae892019-03-25 16:14:37 -0700342 } else if nameFile == "/" && patternFile == "/" {
343 return true, nil
344 } else if nameFile == "." || patternFile == "." || nameFile == "/" || patternFile == "/" {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700345 return false, nil
346 }
347
348 match, err := filepath.Match(patternFile, nameFile)
349 if err != nil || !match {
350 return match, err
351 }
352 }
353}
354
Colin Cross127d2ea2016-11-01 11:10:51 -0700355// IsGlob returns true if the pattern contains any glob characters (*, ?, or [).
356func IsGlob(pattern string) bool {
357 return strings.IndexAny(pattern, "*?[") >= 0
358}
359
360// HasGlob returns true if any string in the list contains any glob characters (*, ?, or [).
361func HasGlob(in []string) bool {
362 for _, s := range in {
363 if IsGlob(s) {
364 return true
365 }
366 }
367
368 return false
369}
370
Colin Cross127d2ea2016-11-01 11:10:51 -0700371// WriteFileIfChanged wraps ioutil.WriteFile, but only writes the file if
372// the files does not already exist with identical contents. This can be used
373// along with ninja restat rules to skip rebuilding downstream rules if no
374// changes were made by a rule.
375func WriteFileIfChanged(filename string, data []byte, perm os.FileMode) error {
376 var isChanged bool
377
378 dir := filepath.Dir(filename)
379 err := os.MkdirAll(dir, 0777)
380 if err != nil {
381 return err
382 }
383
384 info, err := os.Stat(filename)
385 if err != nil {
386 if os.IsNotExist(err) {
387 // The file does not exist yet.
388 isChanged = true
389 } else {
390 return err
391 }
392 } else {
393 if info.Size() != int64(len(data)) {
394 isChanged = true
395 } else {
396 oldData, err := ioutil.ReadFile(filename)
397 if err != nil {
398 return err
399 }
400
401 if len(oldData) != len(data) {
402 isChanged = true
403 } else {
404 for i := range data {
405 if oldData[i] != data[i] {
406 isChanged = true
407 break
408 }
409 }
410 }
411 }
412 }
413
414 if isChanged {
415 err = ioutil.WriteFile(filename, data, perm)
416 if err != nil {
417 return err
418 }
419 }
420
421 return nil
422}
Colin Crossa4706122018-09-19 13:36:42 -0700423
424var matchEscaper = strings.NewReplacer(
425 `*`, `\*`,
426 `?`, `\?`,
427 `[`, `\[`,
428 `]`, `\]`,
429)
430
431// MatchEscape returns its inputs with characters that would be interpreted by
432func MatchEscape(s string) string {
433 return matchEscaper.Replace(s)
434}