1// Copyright 2016 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 bootstrap 16 17import ( 18 "bytes" 19 "fmt" 20 "hash/fnv" 21 "io" 22 "io/ioutil" 23 "path/filepath" 24 "strconv" 25 "strings" 26 27 "github.com/google/blueprint" 28 "github.com/google/blueprint/pathtools" 29) 30 31// This file supports globbing source files in Blueprints files. 32// 33// The build.ninja file needs to be regenerated any time a file matching the glob is added 34// or removed. The naive solution is to have the build.ninja file depend on all the 35// traversed directories, but this will cause the regeneration step to run every time a 36// non-matching file is added to a traversed directory, including backup files created by 37// editors. 38// 39// The solution implemented here optimizes out regenerations when the directory modifications 40// don't match the glob by having the build.ninja file depend on an intermedate file that 41// is only updated when a file matching the glob is added or removed. The intermediate file 42// depends on the traversed directories via a depfile. The depfile is used to avoid build 43// errors if a directory is deleted - a direct dependency on the deleted directory would result 44// in a build failure with a "missing and no known rule to make it" error. 45 46var ( 47 _ = pctx.VariableFunc("globCmd", func(ctx blueprint.VariableFuncContext, config interface{}) (string, error) { 48 return filepath.Join(config.(BootstrapConfig).SoongOutDir(), "bpglob"), nil 49 }) 50 51 // globRule rule traverses directories to produce a list of files that match $glob 52 // and writes it to $out if it has changed, and writes the directories to $out.d 53 GlobRule = pctx.StaticRule("GlobRule", 54 blueprint.RuleParams{ 55 Command: "$globCmd -o $out $args", 56 CommandDeps: []string{"$globCmd"}, 57 Description: "glob", 58 59 Restat: true, 60 Deps: blueprint.DepsGCC, 61 Depfile: "$out.d", 62 }, 63 "args") 64) 65 66// GlobFileContext is the subset of ModuleContext and SingletonContext needed by GlobFile 67type GlobFileContext interface { 68 Config() interface{} 69 Build(pctx blueprint.PackageContext, params blueprint.BuildParams) 70} 71 72// GlobFile creates a rule to write to fileListFile a list of the files that match the specified 73// pattern but do not match any of the patterns specified in excludes. The file will include 74// appropriate dependencies to regenerate the file if and only if the list of matching files has 75// changed. 76func GlobFile(ctx GlobFileContext, pattern string, excludes []string, fileListFile string) { 77 args := `-p "` + pattern + `"` 78 if len(excludes) > 0 { 79 args += " " + joinWithPrefixAndQuote(excludes, "-e ") 80 } 81 ctx.Build(pctx, blueprint.BuildParams{ 82 Rule: GlobRule, 83 Outputs: []string{fileListFile}, 84 Args: map[string]string{ 85 "args": args, 86 }, 87 Description: "glob " + pattern, 88 }) 89} 90 91// multipleGlobFilesRule creates a rule to write to fileListFile a list of the files that match the specified 92// pattern but do not match any of the patterns specified in excludes. The file will include 93// appropriate dependencies to regenerate the file if and only if the list of matching files has 94// changed. 95func multipleGlobFilesRule(ctx GlobFileContext, fileListFile string, shard int, globs pathtools.MultipleGlobResults) { 96 args := strings.Builder{} 97 98 for i, glob := range globs { 99 if i != 0 { 100 args.WriteString(" ") 101 } 102 args.WriteString(`-p "`) 103 args.WriteString(glob.Pattern) 104 args.WriteString(`"`) 105 for _, exclude := range glob.Excludes { 106 args.WriteString(` -e "`) 107 args.WriteString(exclude) 108 args.WriteString(`"`) 109 } 110 } 111 112 ctx.Build(pctx, blueprint.BuildParams{ 113 Rule: GlobRule, 114 Outputs: []string{fileListFile}, 115 Args: map[string]string{ 116 "args": args.String(), 117 }, 118 Description: fmt.Sprintf("regenerate globs shard %d of %d", shard, numGlobBuckets), 119 }) 120} 121 122func joinWithPrefixAndQuote(strs []string, prefix string) string { 123 if len(strs) == 0 { 124 return "" 125 } 126 127 if len(strs) == 1 { 128 return prefix + `"` + strs[0] + `"` 129 } 130 131 n := len(" ") * (len(strs) - 1) 132 for _, s := range strs { 133 n += len(prefix) + len(s) + len(`""`) 134 } 135 136 ret := make([]byte, 0, n) 137 for i, s := range strs { 138 if i != 0 { 139 ret = append(ret, ' ') 140 } 141 ret = append(ret, prefix...) 142 ret = append(ret, '"') 143 ret = append(ret, s...) 144 ret = append(ret, '"') 145 } 146 return string(ret) 147} 148 149// GlobSingleton collects any glob patterns that were seen by Context and writes out rules to 150// re-evaluate them whenever the contents of the searched directories change, and retrigger the 151// primary builder if the results change. 152type GlobSingleton struct { 153 // A function that returns the glob results of individual glob buckets 154 GlobLister func() pathtools.MultipleGlobResults 155 156 // Ninja file that contains instructions for validating the glob list files 157 GlobFile string 158 159 // Directory containing the glob list files 160 GlobDir string 161 162 // The source directory 163 SrcDir string 164} 165 166func globBucketName(globDir string, globBucket int) string { 167 return filepath.Join(globDir, strconv.Itoa(globBucket)) 168} 169 170// Returns the directory where glob list files live 171func GlobDirectory(buildDir, globListDir string) string { 172 return filepath.Join(buildDir, "globs", globListDir) 173} 174 175func (s *GlobSingleton) GenerateBuildActions(ctx blueprint.SingletonContext) { 176 // Sort the list of globs into buckets. A hash function is used instead of sharding so that 177 // adding a new glob doesn't force rerunning all the buckets by shifting them all by 1. 178 globBuckets := make([]pathtools.MultipleGlobResults, numGlobBuckets) 179 for _, g := range s.GlobLister() { 180 bucket := globToBucket(g) 181 globBuckets[bucket] = append(globBuckets[bucket], g) 182 } 183 184 for i, globs := range globBuckets { 185 fileListFile := globBucketName(s.GlobDir, i) 186 187 // Called from generateGlobNinjaFile. Write out the file list to disk, and add a ninja 188 // rule to run bpglob if any of the dependencies (usually directories that contain 189 // globbed files) have changed. The file list produced by bpglob should match exactly 190 // with the file written here so that restat can prevent rerunning the primary builder. 191 // 192 // We need to write the file list here so that it has an older modified date 193 // than the build.ninja (otherwise we'd run the primary builder twice on 194 // every new glob) 195 // 196 // We don't need to write the depfile because we're guaranteed that ninja 197 // will run the command at least once (to record it into the ninja_log), so 198 // the depfile will be loaded from that execution. 199 absoluteFileListFile := blueprint.JoinPath(s.SrcDir, fileListFile) 200 err := pathtools.WriteFileIfChanged(absoluteFileListFile, globs.FileList(), 0666) 201 if err != nil { 202 panic(fmt.Errorf("error writing %s: %s", fileListFile, err)) 203 } 204 205 // Write out the ninja rule to run bpglob. 206 multipleGlobFilesRule(ctx, fileListFile, i, globs) 207 } 208} 209 210// Writes a .ninja file that contains instructions for regenerating the glob 211// files that contain the results of every glob that was run. The list of files 212// is available as the result of GlobFileListFiles(). 213func WriteBuildGlobsNinjaFile(glob *GlobSingleton, config interface{}) error { 214 buffer, errs := generateGlobNinjaFile(glob, config) 215 if len(errs) > 0 { 216 return fatalErrors(errs) 217 } 218 219 const outFilePermissions = 0666 220 err := ioutil.WriteFile(blueprint.JoinPath(glob.SrcDir, glob.GlobFile), buffer, outFilePermissions) 221 if err != nil { 222 return fmt.Errorf("error writing %s: %s", glob.GlobFile, err) 223 } 224 225 return nil 226} 227 228func generateGlobNinjaFile(glob *GlobSingleton, config interface{}) ([]byte, []error) { 229 230 ctx := blueprint.NewContext() 231 ctx.RegisterSingletonType("glob", func() blueprint.Singleton { 232 return glob 233 }, false) 234 235 extraDeps, errs := ctx.ResolveDependencies(config) 236 if len(extraDeps) > 0 { 237 return nil, []error{fmt.Errorf("shouldn't have extra deps")} 238 } 239 if len(errs) > 0 { 240 return nil, errs 241 } 242 243 // PrepareBuildActions() will write $OUTDIR/soong/globs/$m/$i files 244 // where $m=bp2build|build and $i=0..numGlobBuckets 245 extraDeps, errs = ctx.PrepareBuildActions(config) 246 if len(extraDeps) > 0 { 247 return nil, []error{fmt.Errorf("shouldn't have extra deps")} 248 } 249 if len(errs) > 0 { 250 return nil, errs 251 } 252 253 buf := bytes.NewBuffer(nil) 254 err := ctx.WriteBuildFile(buf, false, "") 255 if err != nil { 256 return nil, []error{err} 257 } 258 259 return buf.Bytes(), nil 260} 261 262// GlobFileListFiles returns the list of files that contain the result of globs 263// in the build. It is suitable for inclusion in build.ninja.d (so that 264// build.ninja is regenerated if the globs change). The instructions to 265// regenerate these files are written by WriteBuildGlobsNinjaFile(). 266func GlobFileListFiles(globDir string) []string { 267 var fileListFiles []string 268 for i := 0; i < numGlobBuckets; i++ { 269 fileListFile := globBucketName(globDir, i) 270 fileListFiles = append(fileListFiles, fileListFile) 271 } 272 return fileListFiles 273} 274 275const numGlobBuckets = 1024 276 277// globToBucket converts a pathtools.GlobResult into a hashed bucket number in the range 278// [0, numGlobBuckets). 279func globToBucket(g pathtools.GlobResult) int { 280 hash := fnv.New32a() 281 io.WriteString(hash, g.Pattern) 282 for _, e := range g.Excludes { 283 io.WriteString(hash, e) 284 } 285 return int(hash.Sum32() % numGlobBuckets) 286} 287