blob: 727b7253ccbe5e1f2b15b9d6bb34c93ecd8213cf [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 Cross7bac3c62015-04-23 15:45:25 -070018 "errors"
Colin Crossced59ee2016-11-05 08:37:33 -070019 "fmt"
Colin Cross127d2ea2016-11-01 11:10:51 -070020 "io/ioutil"
Colin Cross4b793e52015-04-23 13:29:28 -070021 "os"
Jamie Gennisdebef532014-10-24 10:42:57 -070022 "path/filepath"
23 "strings"
Colin Cross127d2ea2016-11-01 11:10:51 -070024
25 "github.com/google/blueprint/deptools"
Jamie Gennisdebef532014-10-24 10:42:57 -070026)
27
Colin Crossa26ae892019-03-25 16:14:37 -070028var GlobMultipleRecursiveErr = errors.New("pattern contains multiple '**'")
29var GlobLastRecursiveErr = errors.New("pattern has '**' as last path element")
30var GlobInvalidRecursiveErr = errors.New("pattern contains other characters between '**' and path separator")
Colin Cross7bac3c62015-04-23 15:45:25 -070031
Dan Willemsenb6c90232018-02-23 14:49:45 -080032// Glob returns the list of files and directories that match the given pattern
33// but do not match the given exclude patterns, along with the list of
34// directories and other dependencies that were searched to construct the file
35// list. The supported glob and exclude patterns are equivalent to
36// filepath.Glob, with an extension that recursive glob (** matching zero or
37// more complete path entries) is supported. Any directories in the matches
38// list will have a '/' suffix.
Colin Cross127d2ea2016-11-01 11:10:51 -070039//
40// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps
41// should be used instead, as they will automatically set up dependencies
42// to rerun the primary builder when the list of matching files changes.
Colin Crosse98d0822018-09-21 15:30:13 -070043func Glob(pattern string, excludes []string, follow ShouldFollowSymlinks) (matches, deps []string, err error) {
44 return startGlob(OsFs, pattern, excludes, follow)
Colin Crossb519a7e2017-02-01 13:21:35 -080045}
46
Colin Crosse98d0822018-09-21 15:30:13 -070047func startGlob(fs FileSystem, pattern string, excludes []string,
48 follow ShouldFollowSymlinks) (matches, deps []string, err error) {
49
Colin Cross7d2ee0d2015-06-18 10:48:15 -070050 if filepath.Base(pattern) == "**" {
Colin Cross7bac3c62015-04-23 15:45:25 -070051 return nil, nil, GlobLastRecursiveErr
52 } else {
Colin Crosse98d0822018-09-21 15:30:13 -070053 matches, deps, err = glob(fs, pattern, false, follow)
Colin Cross7bac3c62015-04-23 15:45:25 -070054 }
55
Colin Cross5d6d4c72015-04-23 17:56:11 -070056 if err != nil {
57 return nil, nil, err
58 }
59
60 matches, err = filterExcludes(matches, excludes)
61 if err != nil {
62 return nil, nil, err
63 }
64
Dan Willemsen75fec882017-06-22 17:01:24 -070065 // If the pattern has wildcards, we added dependencies on the
66 // containing directories to know about changes.
67 //
68 // If the pattern didn't have wildcards, and didn't find matches, the
69 // most specific found directories were added.
70 //
71 // But if it didn't have wildcards, and did find a match, no
72 // dependencies were added, so add the match itself to detect when it
73 // is removed.
74 if !isWild(pattern) {
75 deps = append(deps, matches...)
76 }
77
Dan Willemsenb6c90232018-02-23 14:49:45 -080078 for i, match := range matches {
Colin Crosse98d0822018-09-21 15:30:13 -070079 isSymlink, err := fs.IsSymlink(match)
Colin Crosse3b7ec32018-09-20 14:36:10 -070080 if err != nil {
Colin Crosse98d0822018-09-21 15:30:13 -070081 return nil, nil, err
Colin Crosse3b7ec32018-09-20 14:36:10 -070082 }
Colin Crosse98d0822018-09-21 15:30:13 -070083 if !(isSymlink && follow == DontFollowSymlinks) {
84 isDir, err := fs.IsDir(match)
85 if os.IsNotExist(err) {
86 if isSymlink {
87 return nil, nil, fmt.Errorf("%s: dangling symlink", match)
88 }
89 }
90 if err != nil {
91 return nil, nil, fmt.Errorf("%s: %s", match, err.Error())
92 }
Colin Crosse3b7ec32018-09-20 14:36:10 -070093
Colin Crosse98d0822018-09-21 15:30:13 -070094 if isDir {
95 matches[i] = match + "/"
96 }
Dan Willemsenb6c90232018-02-23 14:49:45 -080097 }
98 }
99
Dan Willemsen75fec882017-06-22 17:01:24 -0700100 return matches, deps, nil
Colin Cross7bac3c62015-04-23 15:45:25 -0700101}
102
103// glob is a recursive helper function to handle globbing each level of the pattern individually,
104// allowing searched directories to be tracked. Also handles the recursive glob pattern, **.
Colin Crosse98d0822018-09-21 15:30:13 -0700105func glob(fs FileSystem, pattern string, hasRecursive bool,
106 follow ShouldFollowSymlinks) (matches, dirs []string, err error) {
107
Colin Cross7bac3c62015-04-23 15:45:25 -0700108 if !isWild(pattern) {
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700109 // If there are no wilds in the pattern, check whether the file exists or not.
110 // Uses filepath.Glob instead of manually statting to get consistent results.
111 pattern = filepath.Clean(pattern)
Colin Crossb519a7e2017-02-01 13:21:35 -0800112 matches, err = fs.glob(pattern)
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700113 if err != nil {
114 return matches, dirs, err
115 }
116
117 if len(matches) == 0 {
118 // Some part of the non-wild pattern didn't exist. Add the last existing directory
119 // as a dependency.
120 var matchDirs []string
121 for len(matchDirs) == 0 {
Colin Crossa26ae892019-03-25 16:14:37 -0700122 pattern = filepath.Dir(pattern)
Colin Crossb519a7e2017-02-01 13:21:35 -0800123 matchDirs, err = fs.glob(pattern)
Colin Cross7d2ee0d2015-06-18 10:48:15 -0700124 if err != nil {
125 return matches, dirs, err
126 }
127 }
128 dirs = append(dirs, matchDirs...)
129 }
Colin Cross4b793e52015-04-23 13:29:28 -0700130 return matches, dirs, err
Jamie Gennisdebef532014-10-24 10:42:57 -0700131 }
132
Colin Cross4b793e52015-04-23 13:29:28 -0700133 dir, file := saneSplit(pattern)
Colin Cross7bac3c62015-04-23 15:45:25 -0700134
135 if file == "**" {
136 if hasRecursive {
137 return matches, dirs, GlobMultipleRecursiveErr
138 }
139 hasRecursive = true
Colin Crossa26ae892019-03-25 16:14:37 -0700140 } else if strings.Contains(file, "**") {
141 return matches, dirs, GlobInvalidRecursiveErr
Colin Cross7bac3c62015-04-23 15:45:25 -0700142 }
143
Colin Crosse98d0822018-09-21 15:30:13 -0700144 dirMatches, dirs, err := glob(fs, dir, hasRecursive, follow)
Colin Cross7bac3c62015-04-23 15:45:25 -0700145 if err != nil {
146 return nil, nil, err
147 }
148
Colin Cross4b793e52015-04-23 13:29:28 -0700149 for _, m := range dirMatches {
Colin Crosse3b7ec32018-09-20 14:36:10 -0700150 isDir, err := fs.IsDir(m)
151 if os.IsNotExist(err) {
152 if isSymlink, _ := fs.IsSymlink(m); isSymlink {
153 return nil, nil, fmt.Errorf("dangling symlink: %s", m)
154 }
155 }
156 if err != nil {
Colin Crossced59ee2016-11-05 08:37:33 -0700157 return nil, nil, fmt.Errorf("unexpected error after glob: %s", err)
Colin Crosse3b7ec32018-09-20 14:36:10 -0700158 }
159
160 if isDir {
Colin Cross7bac3c62015-04-23 15:45:25 -0700161 if file == "**" {
Colin Crosse98d0822018-09-21 15:30:13 -0700162 recurseDirs, err := fs.ListDirsRecursive(m, follow)
Colin Cross7bac3c62015-04-23 15:45:25 -0700163 if err != nil {
164 return nil, nil, err
165 }
166 matches = append(matches, recurseDirs...)
167 } else {
168 dirs = append(dirs, m)
Colin Crossa4706122018-09-19 13:36:42 -0700169 newMatches, err := fs.glob(filepath.Join(MatchEscape(m), file))
Colin Cross7bac3c62015-04-23 15:45:25 -0700170 if err != nil {
171 return nil, nil, err
172 }
Dan Willemsen53f49502016-07-25 18:40:02 -0700173 if file[0] != '.' {
174 newMatches = filterDotFiles(newMatches)
175 }
Colin Cross7bac3c62015-04-23 15:45:25 -0700176 matches = append(matches, newMatches...)
Jamie Gennisdebef532014-10-24 10:42:57 -0700177 }
178 }
179 }
180
Colin Cross4b793e52015-04-23 13:29:28 -0700181 return matches, dirs, nil
Jamie Gennisdebef532014-10-24 10:42:57 -0700182}
183
Colin Cross7bac3c62015-04-23 15:45:25 -0700184// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
Colin Cross4b793e52015-04-23 13:29:28 -0700185// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
Colin Cross7bac3c62015-04-23 15:45:25 -0700186// not "/". Returns ".", "" if path is "."
Colin Cross4b793e52015-04-23 13:29:28 -0700187func saneSplit(path string) (dir, file string) {
Colin Cross7bac3c62015-04-23 15:45:25 -0700188 if path == "." {
189 return ".", ""
190 }
Colin Cross4b793e52015-04-23 13:29:28 -0700191 dir, file = filepath.Split(path)
192 switch dir {
193 case "":
194 dir = "."
195 case "/":
196 // Nothing
197 default:
198 dir = dir[:len(dir)-1]
Jamie Gennisdebef532014-10-24 10:42:57 -0700199 }
Colin Cross4b793e52015-04-23 13:29:28 -0700200 return dir, file
Jamie Gennisdebef532014-10-24 10:42:57 -0700201}
202
203func isWild(pattern string) bool {
204 return strings.ContainsAny(pattern, "*?[")
205}
Jean-Francois Dupuis418d5c52015-02-05 20:01:49 -0800206
Colin Cross5d6d4c72015-04-23 17:56:11 -0700207// Filters the strings in matches based on the glob patterns in excludes. Hierarchical (a/*) and
208// recursive (**) glob patterns are supported.
209func filterExcludes(matches []string, excludes []string) ([]string, error) {
210 if len(excludes) == 0 {
211 return matches, nil
212 }
213
214 var ret []string
215matchLoop:
216 for _, m := range matches {
217 for _, e := range excludes {
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800218 exclude, err := Match(e, m)
Colin Cross5d6d4c72015-04-23 17:56:11 -0700219 if err != nil {
220 return nil, err
221 }
222 if exclude {
223 continue matchLoop
224 }
225 }
226 ret = append(ret, m)
227 }
228
229 return ret, nil
230}
231
Dan Willemsen53f49502016-07-25 18:40:02 -0700232// filterDotFiles filters out files that start with '.'
233func filterDotFiles(matches []string) []string {
234 ret := make([]string, 0, len(matches))
235
236 for _, match := range matches {
237 _, name := filepath.Split(match)
238 if name[0] == '.' {
239 continue
240 }
241 ret = append(ret, match)
242 }
243
244 return ret
245}
246
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800247// Match returns true if name matches pattern using the same rules as filepath.Match, but supporting
Colin Crossa26ae892019-03-25 16:14:37 -0700248// recursive globs (**).
Colin Crossf9c2e8c2017-11-22 12:50:21 -0800249func Match(pattern, name string) (bool, error) {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700250 if filepath.Base(pattern) == "**" {
251 return false, GlobLastRecursiveErr
252 }
253
Colin Crossc0c3b0f2018-07-13 21:17:48 -0700254 patternDir := pattern[len(pattern)-1] == '/'
255 nameDir := name[len(name)-1] == '/'
256
257 if patternDir != nameDir {
258 return false, nil
259 }
260
261 if nameDir {
262 name = name[:len(name)-1]
263 pattern = pattern[:len(pattern)-1]
264 }
265
Colin Cross5d6d4c72015-04-23 17:56:11 -0700266 for {
267 var patternFile, nameFile string
Colin Crossa26ae892019-03-25 16:14:37 -0700268 pattern, patternFile = filepath.Dir(pattern), filepath.Base(pattern)
Colin Cross5d6d4c72015-04-23 17:56:11 -0700269
270 if patternFile == "**" {
Colin Crossa26ae892019-03-25 16:14:37 -0700271 if strings.Contains(pattern, "**") {
272 return false, GlobMultipleRecursiveErr
273 }
274 // Test if the any prefix of name matches the part of the pattern before **
275 for {
276 if name == "." || name == "/" {
277 return name == pattern, nil
278 }
279 if match, err := filepath.Match(pattern, name); err != nil {
280 return false, err
281 } else if match {
282 return true, nil
283 }
284 name = filepath.Dir(name)
285 }
286 } else if strings.Contains(patternFile, "**") {
287 return false, GlobInvalidRecursiveErr
Colin Cross5d6d4c72015-04-23 17:56:11 -0700288 }
289
Colin Crossa26ae892019-03-25 16:14:37 -0700290 name, nameFile = filepath.Dir(name), filepath.Base(name)
291
292 if nameFile == "." && patternFile == "." {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700293 return true, nil
Colin Crossa26ae892019-03-25 16:14:37 -0700294 } else if nameFile == "/" && patternFile == "/" {
295 return true, nil
296 } else if nameFile == "." || patternFile == "." || nameFile == "/" || patternFile == "/" {
Colin Cross5d6d4c72015-04-23 17:56:11 -0700297 return false, nil
298 }
299
300 match, err := filepath.Match(patternFile, nameFile)
301 if err != nil || !match {
302 return match, err
303 }
304 }
305}
306
Jean-Francois Dupuis418d5c52015-02-05 20:01:49 -0800307func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
308 var (
309 matches []string
Colin Cross63d5d4d2015-04-20 16:41:55 -0700310 deps []string
Jean-Francois Dupuis418d5c52015-02-05 20:01:49 -0800311 )
312
313 globedList = make([]string, 0)
314 depDirs = make([]string, 0)
315
316 for _, pattern := range patterns {
317 if isWild(pattern) {
Colin Crosse98d0822018-09-21 15:30:13 -0700318 matches, deps, err = Glob(filepath.Join(prefix, pattern), nil, FollowSymlinks)
Jean-Francois Dupuis418d5c52015-02-05 20:01:49 -0800319 if err != nil {
320 return nil, nil, err
321 }
322 globedList = append(globedList, matches...)
323 depDirs = append(depDirs, deps...)
324 } else {
325 globedList = append(globedList, filepath.Join(prefix, pattern))
326 }
327 }
328 return globedList, depDirs, nil
329}
Colin Cross127d2ea2016-11-01 11:10:51 -0700330
331// IsGlob returns true if the pattern contains any glob characters (*, ?, or [).
332func IsGlob(pattern string) bool {
333 return strings.IndexAny(pattern, "*?[") >= 0
334}
335
336// HasGlob returns true if any string in the list contains any glob characters (*, ?, or [).
337func HasGlob(in []string) bool {
338 for _, s := range in {
339 if IsGlob(s) {
340 return true
341 }
342 }
343
344 return false
345}
346
Dan Willemsenb6c90232018-02-23 14:49:45 -0800347// GlobWithDepFile finds all files and directories that match glob. Directories
348// will have a trailing '/'. It compares the list of matches against the
349// contents of fileListFile, and rewrites fileListFile if it has changed. It
350// also writes all of the the directories it traversed as dependencies on
351// fileListFile to depFile.
Colin Cross127d2ea2016-11-01 11:10:51 -0700352//
Dan Willemsenb6c90232018-02-23 14:49:45 -0800353// The format of glob is either path/*.ext for a single directory glob, or
354// path/**/*.ext for a recursive glob.
Colin Cross127d2ea2016-11-01 11:10:51 -0700355//
356// Returns a list of file paths, and an error.
357//
358// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps
359// should be used instead, as they will automatically set up dependencies
360// to rerun the primary builder when the list of matching files changes.
361func GlobWithDepFile(glob, fileListFile, depFile string, excludes []string) (files []string, err error) {
Colin Crosse98d0822018-09-21 15:30:13 -0700362 files, deps, err := Glob(glob, excludes, FollowSymlinks)
Colin Cross127d2ea2016-11-01 11:10:51 -0700363 if err != nil {
364 return nil, err
365 }
366
367 fileList := strings.Join(files, "\n") + "\n"
368
369 WriteFileIfChanged(fileListFile, []byte(fileList), 0666)
Dan Willemsen75fec882017-06-22 17:01:24 -0700370 deptools.WriteDepFile(depFile, fileListFile, deps)
Colin Cross127d2ea2016-11-01 11:10:51 -0700371
372 return
373}
374
375// WriteFileIfChanged wraps ioutil.WriteFile, but only writes the file if
376// the files does not already exist with identical contents. This can be used
377// along with ninja restat rules to skip rebuilding downstream rules if no
378// changes were made by a rule.
379func WriteFileIfChanged(filename string, data []byte, perm os.FileMode) error {
380 var isChanged bool
381
382 dir := filepath.Dir(filename)
383 err := os.MkdirAll(dir, 0777)
384 if err != nil {
385 return err
386 }
387
388 info, err := os.Stat(filename)
389 if err != nil {
390 if os.IsNotExist(err) {
391 // The file does not exist yet.
392 isChanged = true
393 } else {
394 return err
395 }
396 } else {
397 if info.Size() != int64(len(data)) {
398 isChanged = true
399 } else {
400 oldData, err := ioutil.ReadFile(filename)
401 if err != nil {
402 return err
403 }
404
405 if len(oldData) != len(data) {
406 isChanged = true
407 } else {
408 for i := range data {
409 if oldData[i] != data[i] {
410 isChanged = true
411 break
412 }
413 }
414 }
415 }
416 }
417
418 if isChanged {
419 err = ioutil.WriteFile(filename, data, perm)
420 if err != nil {
421 return err
422 }
423 }
424
425 return nil
426}
Colin Crossa4706122018-09-19 13:36:42 -0700427
428var matchEscaper = strings.NewReplacer(
429 `*`, `\*`,
430 `?`, `\?`,
431 `[`, `\[`,
432 `]`, `\]`,
433)
434
435// MatchEscape returns its inputs with characters that would be interpreted by
436func MatchEscape(s string) string {
437 return matchEscaper.Replace(s)
438}