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