• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// 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
15package pathtools
16
17import (
18	"errors"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"path/filepath"
23	"strings"
24
25	"github.com/google/blueprint/deptools"
26)
27
28var GlobMultipleRecursiveErr = errors.New("pattern contains multiple **")
29var GlobLastRecursiveErr = errors.New("pattern ** as last path element")
30
31// Glob returns the list of files that match the given pattern but
32// do not match the given exclude patterns, along with the list of directories
33// that were searched to construct the file list.  The supported glob and
34// exclude patterns are equivalent to filepath.Glob, with an extension that
35// recursive glob (** matching zero or more complete path entries) is supported.
36// Glob also returns a list of directories that were searched.
37//
38// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps
39// should be used instead, as they will automatically set up dependencies
40// to rerun the primary builder when the list of matching files changes.
41func Glob(pattern string, excludes []string) (matches, dirs []string, err error) {
42	return startGlob(OsFs, pattern, excludes)
43}
44
45func startGlob(fs FileSystem, pattern string, excludes []string) (matches, dirs []string, err error) {
46	if filepath.Base(pattern) == "**" {
47		return nil, nil, GlobLastRecursiveErr
48	} else {
49		matches, dirs, err = glob(fs, pattern, false)
50	}
51
52	if err != nil {
53		return nil, nil, err
54	}
55
56	matches, err = filterExcludes(matches, excludes)
57	if err != nil {
58		return nil, nil, err
59	}
60
61	return matches, dirs, nil
62}
63
64// glob is a recursive helper function to handle globbing each level of the pattern individually,
65// allowing searched directories to be tracked.  Also handles the recursive glob pattern, **.
66func glob(fs FileSystem, pattern string, hasRecursive bool) (matches, dirs []string, err error) {
67	if !isWild(pattern) {
68		// If there are no wilds in the pattern, check whether the file exists or not.
69		// Uses filepath.Glob instead of manually statting to get consistent results.
70		pattern = filepath.Clean(pattern)
71		matches, err = fs.glob(pattern)
72		if err != nil {
73			return matches, dirs, err
74		}
75
76		if len(matches) == 0 {
77			// Some part of the non-wild pattern didn't exist.  Add the last existing directory
78			// as a dependency.
79			var matchDirs []string
80			for len(matchDirs) == 0 {
81				pattern, _ = saneSplit(pattern)
82				matchDirs, err = fs.glob(pattern)
83				if err != nil {
84					return matches, dirs, err
85				}
86			}
87			dirs = append(dirs, matchDirs...)
88		}
89		return matches, dirs, err
90	}
91
92	dir, file := saneSplit(pattern)
93
94	if file == "**" {
95		if hasRecursive {
96			return matches, dirs, GlobMultipleRecursiveErr
97		}
98		hasRecursive = true
99	}
100
101	dirMatches, dirs, err := glob(fs, dir, hasRecursive)
102	if err != nil {
103		return nil, nil, err
104	}
105
106	for _, m := range dirMatches {
107		if isDir, err := fs.IsDir(m); err != nil {
108			return nil, nil, fmt.Errorf("unexpected error after glob: %s", err)
109		} else if isDir {
110			if file == "**" {
111				recurseDirs, err := walkAllDirs(m)
112				if err != nil {
113					return nil, nil, err
114				}
115				matches = append(matches, recurseDirs...)
116			} else {
117				dirs = append(dirs, m)
118				newMatches, err := fs.glob(filepath.Join(m, file))
119				if err != nil {
120					return nil, nil, err
121				}
122				if file[0] != '.' {
123					newMatches = filterDotFiles(newMatches)
124				}
125				matches = append(matches, newMatches...)
126			}
127		}
128	}
129
130	return matches, dirs, nil
131}
132
133// Faster version of dir, file := filepath.Dir(path), filepath.File(path) with no allocations
134// Similar to filepath.Split, but returns "." if dir is empty and trims trailing slash if dir is
135// not "/".  Returns ".", "" if path is "."
136func saneSplit(path string) (dir, file string) {
137	if path == "." {
138		return ".", ""
139	}
140	dir, file = filepath.Split(path)
141	switch dir {
142	case "":
143		dir = "."
144	case "/":
145		// Nothing
146	default:
147		dir = dir[:len(dir)-1]
148	}
149	return dir, file
150}
151
152func isWild(pattern string) bool {
153	return strings.ContainsAny(pattern, "*?[")
154}
155
156// Returns a list of all directories under dir
157func walkAllDirs(dir string) (dirs []string, err error) {
158	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
159		if err != nil {
160			return err
161		}
162
163		if info.Mode().IsDir() {
164			name := info.Name()
165			if name[0] == '.' && name != "." {
166				return filepath.SkipDir
167			}
168
169			dirs = append(dirs, path)
170		}
171		return nil
172	})
173
174	return dirs, err
175}
176
177// Filters the strings in matches based on the glob patterns in excludes.  Hierarchical (a/*) and
178// recursive (**) glob patterns are supported.
179func filterExcludes(matches []string, excludes []string) ([]string, error) {
180	if len(excludes) == 0 {
181		return matches, nil
182	}
183
184	var ret []string
185matchLoop:
186	for _, m := range matches {
187		for _, e := range excludes {
188			exclude, err := match(e, m)
189			if err != nil {
190				return nil, err
191			}
192			if exclude {
193				continue matchLoop
194			}
195		}
196		ret = append(ret, m)
197	}
198
199	return ret, nil
200}
201
202// filterDotFiles filters out files that start with '.'
203func filterDotFiles(matches []string) []string {
204	ret := make([]string, 0, len(matches))
205
206	for _, match := range matches {
207		_, name := filepath.Split(match)
208		if name[0] == '.' {
209			continue
210		}
211		ret = append(ret, match)
212	}
213
214	return ret
215}
216
217// match returns true if name matches pattern using the same rules as filepath.Match, but supporting
218// hierarchical patterns (a/*) and recursive globs (**).
219func match(pattern, name string) (bool, error) {
220	if filepath.Base(pattern) == "**" {
221		return false, GlobLastRecursiveErr
222	}
223
224	for {
225		var patternFile, nameFile string
226		pattern, patternFile = saneSplit(pattern)
227		name, nameFile = saneSplit(name)
228
229		if patternFile == "**" {
230			return matchPrefix(pattern, filepath.Join(name, nameFile))
231		}
232
233		if nameFile == "" && patternFile == "" {
234			return true, nil
235		} else if nameFile == "" || patternFile == "" {
236			return false, nil
237		}
238
239		match, err := filepath.Match(patternFile, nameFile)
240		if err != nil || !match {
241			return match, err
242		}
243	}
244}
245
246// matchPrefix returns true if the beginning of name matches pattern using the same rules as
247// filepath.Match, but supporting hierarchical patterns (a/*).  Recursive globs (**) are not
248// supported, they should have been handled in match().
249func matchPrefix(pattern, name string) (bool, error) {
250	if len(pattern) > 0 && pattern[0] == '/' {
251		if len(name) > 0 && name[0] == '/' {
252			pattern = pattern[1:]
253			name = name[1:]
254		} else {
255			return false, nil
256		}
257	}
258
259	for {
260		var patternElem, nameElem string
261		patternElem, pattern = saneSplitFirst(pattern)
262		nameElem, name = saneSplitFirst(name)
263
264		if patternElem == "." {
265			patternElem = ""
266		}
267		if nameElem == "." {
268			nameElem = ""
269		}
270
271		if patternElem == "**" {
272			return false, GlobMultipleRecursiveErr
273		}
274
275		if patternElem == "" {
276			return true, nil
277		} else if nameElem == "" {
278			return false, nil
279		}
280
281		match, err := filepath.Match(patternElem, nameElem)
282		if err != nil || !match {
283			return match, err
284		}
285	}
286}
287
288func saneSplitFirst(path string) (string, string) {
289	i := strings.IndexRune(path, filepath.Separator)
290	if i < 0 {
291		return path, ""
292	}
293	return path[:i], path[i+1:]
294}
295
296func GlobPatternList(patterns []string, prefix string) (globedList []string, depDirs []string, err error) {
297	var (
298		matches []string
299		deps    []string
300	)
301
302	globedList = make([]string, 0)
303	depDirs = make([]string, 0)
304
305	for _, pattern := range patterns {
306		if isWild(pattern) {
307			matches, deps, err = Glob(filepath.Join(prefix, pattern), nil)
308			if err != nil {
309				return nil, nil, err
310			}
311			globedList = append(globedList, matches...)
312			depDirs = append(depDirs, deps...)
313		} else {
314			globedList = append(globedList, filepath.Join(prefix, pattern))
315		}
316	}
317	return globedList, depDirs, nil
318}
319
320// IsGlob returns true if the pattern contains any glob characters (*, ?, or [).
321func IsGlob(pattern string) bool {
322	return strings.IndexAny(pattern, "*?[") >= 0
323}
324
325// HasGlob returns true if any string in the list contains any glob characters (*, ?, or [).
326func HasGlob(in []string) bool {
327	for _, s := range in {
328		if IsGlob(s) {
329			return true
330		}
331	}
332
333	return false
334}
335
336// GlobWithDepFile finds all files that match glob.  It compares the list of files
337// against the contents of fileListFile, and rewrites fileListFile if it has changed.  It also
338// writes all of the the directories it traversed as a depenencies on fileListFile to depFile.
339//
340// The format of glob is either path/*.ext for a single directory glob, or path/**/*.ext
341// for a recursive glob.
342//
343// Returns a list of file paths, and an error.
344//
345// In general ModuleContext.GlobWithDeps or SingletonContext.GlobWithDeps
346// should be used instead, as they will automatically set up dependencies
347// to rerun the primary builder when the list of matching files changes.
348func GlobWithDepFile(glob, fileListFile, depFile string, excludes []string) (files []string, err error) {
349	files, dirs, err := Glob(glob, excludes)
350	if err != nil {
351		return nil, err
352	}
353
354	fileList := strings.Join(files, "\n") + "\n"
355
356	WriteFileIfChanged(fileListFile, []byte(fileList), 0666)
357	deptools.WriteDepFile(depFile, fileListFile, dirs)
358
359	return
360}
361
362// WriteFileIfChanged wraps ioutil.WriteFile, but only writes the file if
363// the files does not already exist with identical contents.  This can be used
364// along with ninja restat rules to skip rebuilding downstream rules if no
365// changes were made by a rule.
366func WriteFileIfChanged(filename string, data []byte, perm os.FileMode) error {
367	var isChanged bool
368
369	dir := filepath.Dir(filename)
370	err := os.MkdirAll(dir, 0777)
371	if err != nil {
372		return err
373	}
374
375	info, err := os.Stat(filename)
376	if err != nil {
377		if os.IsNotExist(err) {
378			// The file does not exist yet.
379			isChanged = true
380		} else {
381			return err
382		}
383	} else {
384		if info.Size() != int64(len(data)) {
385			isChanged = true
386		} else {
387			oldData, err := ioutil.ReadFile(filename)
388			if err != nil {
389				return err
390			}
391
392			if len(oldData) != len(data) {
393				isChanged = true
394			} else {
395				for i := range data {
396					if oldData[i] != data[i] {
397						isChanged = true
398						break
399					}
400				}
401			}
402		}
403	}
404
405	if isChanged {
406		err = ioutil.WriteFile(filename, data, perm)
407		if err != nil {
408			return err
409		}
410	}
411
412	return nil
413}
414