1// Copyright 2022 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 bp2build 16 17import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "path/filepath" 22 "regexp" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 "sync/atomic" 28 29 "android/soong/shared" 30 31 "github.com/google/blueprint/pathtools" 32) 33 34// A tree structure that describes what to do at each directory in the created 35// symlink tree. Currently it is used to enumerate which files/directories 36// should be excluded from symlinking. Each instance of "node" represents a file 37// or a directory. If excluded is true, then that file/directory should be 38// excluded from symlinking. Otherwise, the node is not excluded, but one of its 39// descendants is (otherwise the node in question would not exist) 40 41// This is a version int written to a file called symlink_forest_version at the root of the 42// symlink forest. If the version here does not match the version in the file, then we'll 43// clean the whole symlink forest and recreate it. This number can be bumped whenever there's 44// an incompatible change to the forest layout or a bug in incrementality that needs to be fixed 45// on machines that may still have the bug present in their forest. 46const symlinkForestVersion = 1 47 48type instructionsNode struct { 49 name string 50 excluded bool // If false, this is just an intermediate node 51 children map[string]*instructionsNode 52} 53 54type symlinkForestContext struct { 55 verbose bool 56 topdir string // $TOPDIR 57 58 // State 59 wg sync.WaitGroup 60 depCh chan string 61 mkdirCount atomic.Uint64 62 symlinkCount atomic.Uint64 63} 64 65// Ensures that the node for the given path exists in the tree and returns it. 66func ensureNodeExists(root *instructionsNode, path string) *instructionsNode { 67 if path == "" { 68 return root 69 } 70 71 if path[len(path)-1] == '/' { 72 path = path[:len(path)-1] // filepath.Split() leaves a trailing slash 73 } 74 75 dir, base := filepath.Split(path) 76 77 // First compute the parent node... 78 dn := ensureNodeExists(root, dir) 79 80 // then create the requested node as its direct child, if needed. 81 if child, ok := dn.children[base]; ok { 82 return child 83 } else { 84 dn.children[base] = &instructionsNode{base, false, make(map[string]*instructionsNode)} 85 return dn.children[base] 86 } 87} 88 89// Turns a list of paths to be excluded into a tree 90func instructionsFromExcludePathList(paths []string) *instructionsNode { 91 result := &instructionsNode{"", false, make(map[string]*instructionsNode)} 92 93 for _, p := range paths { 94 ensureNodeExists(result, p).excluded = true 95 } 96 97 return result 98} 99 100func mergeBuildFiles(output string, srcBuildFile string, generatedBuildFile string, verbose bool) error { 101 102 srcBuildFileContent, err := os.ReadFile(srcBuildFile) 103 if err != nil { 104 return err 105 } 106 107 generatedBuildFileContent, err := os.ReadFile(generatedBuildFile) 108 if err != nil { 109 return err 110 } 111 112 // There can't be a package() call in both the source and generated BUILD files. 113 // bp2build will generate a package() call for licensing information, but if 114 // there's no licensing information, it will still generate a package() call 115 // that just sets default_visibility=public. If the handcrafted build file 116 // also has a package() call, we'll allow it to override the bp2build 117 // generated one if it doesn't have any licensing information. If the bp2build 118 // one has licensing information and the handcrafted one exists, we'll leave 119 // them both in for bazel to throw an error. 120 packageRegex := regexp.MustCompile(`(?m)^package\s*\(`) 121 packageDefaultVisibilityRegex := regexp.MustCompile(`(?m)^package\s*\(\s*default_visibility\s*=\s*\[\s*"//visibility:public",?\s*]\s*\)`) 122 if packageRegex.Find(srcBuildFileContent) != nil { 123 if verbose && packageDefaultVisibilityRegex.Find(generatedBuildFileContent) != nil { 124 fmt.Fprintf(os.Stderr, "Both '%s' and '%s' have a package() target, removing the first one\n", 125 generatedBuildFile, srcBuildFile) 126 } 127 generatedBuildFileContent = packageDefaultVisibilityRegex.ReplaceAll(generatedBuildFileContent, []byte{}) 128 } 129 130 newContents := generatedBuildFileContent 131 if newContents[len(newContents)-1] != '\n' { 132 newContents = append(newContents, '\n') 133 } 134 newContents = append(newContents, srcBuildFileContent...) 135 136 // Say you run bp2build 4 times: 137 // - The first time there's only an Android.bp file. bp2build will convert it to a build file 138 // under out/soong/bp2build, then symlink from the forest to that generated file 139 // - Then you add a handcrafted BUILD file in the same directory. bp2build will merge this with 140 // the generated one, and write the result to the output file in the forest. But the output 141 // file was a symlink to out/soong/bp2build from the previous step! So we erroneously update 142 // the file in out/soong/bp2build instead. So far this doesn't cause any problems... 143 // - You run a 3rd bp2build with no relevant changes. Everything continues to work. 144 // - You then add a comment to the handcrafted BUILD file. This causes a merge with the 145 // generated file again. But since we wrote to the generated file in step 2, the generated 146 // file has an old copy of the handcrafted file in it! This probably causes duplicate bazel 147 // targets. 148 // To solve this, if we see that the output file is a symlink from a previous build, remove it. 149 stat, err := os.Lstat(output) 150 if err != nil && !os.IsNotExist(err) { 151 return err 152 } else if err == nil { 153 if stat.Mode()&os.ModeSymlink == os.ModeSymlink { 154 if verbose { 155 fmt.Fprintf(os.Stderr, "Removing symlink so that we can replace it with a merged file: %s\n", output) 156 } 157 err = os.Remove(output) 158 if err != nil { 159 return err 160 } 161 } 162 } 163 164 return pathtools.WriteFileIfChanged(output, newContents, 0666) 165} 166 167// Calls readdir() and returns it as a map from the basename of the files in dir 168// to os.FileInfo. 169func readdirToMap(dir string) map[string]os.FileInfo { 170 entryList, err := ioutil.ReadDir(dir) 171 result := make(map[string]os.FileInfo) 172 173 if err != nil { 174 if os.IsNotExist(err) { 175 // It's okay if a directory doesn't exist; it just means that one of the 176 // trees to be merged contains parts the other doesn't 177 return result 178 } else { 179 fmt.Fprintf(os.Stderr, "Cannot readdir '%s': %s\n", dir, err) 180 os.Exit(1) 181 } 182 } 183 184 for _, fi := range entryList { 185 result[fi.Name()] = fi 186 } 187 188 return result 189} 190 191// Creates a symbolic link at dst pointing to src 192func symlinkIntoForest(topdir, dst, src string) uint64 { 193 srcPath := shared.JoinPath(topdir, src) 194 dstPath := shared.JoinPath(topdir, dst) 195 196 // Check if a symlink already exists. 197 if dstInfo, err := os.Lstat(dstPath); err != nil { 198 if !os.IsNotExist(err) { 199 fmt.Fprintf(os.Stderr, "Failed to lstat '%s': %s", dst, err) 200 os.Exit(1) 201 } 202 } else { 203 if dstInfo.Mode()&os.ModeSymlink != 0 { 204 // Assume that the link's target is correct, i.e. no manual tampering. 205 // E.g. OUT_DIR could have been previously used with a different source tree check-out! 206 return 0 207 } else { 208 if err := os.RemoveAll(dstPath); err != nil { 209 fmt.Fprintf(os.Stderr, "Failed to remove '%s': %s", dst, err) 210 os.Exit(1) 211 } 212 } 213 } 214 215 // Create symlink. 216 if err := os.Symlink(srcPath, dstPath); err != nil { 217 fmt.Fprintf(os.Stderr, "Cannot create symlink at '%s' pointing to '%s': %s", dst, src, err) 218 os.Exit(1) 219 } 220 return 1 221} 222 223func isDir(path string, fi os.FileInfo) bool { 224 if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink { 225 return fi.IsDir() 226 } 227 228 fi2, statErr := os.Stat(path) 229 if statErr == nil { 230 return fi2.IsDir() 231 } 232 233 // Check if this is a dangling symlink. If so, treat it like a file, not a dir. 234 _, lstatErr := os.Lstat(path) 235 if lstatErr != nil { 236 fmt.Fprintf(os.Stderr, "Cannot stat or lstat '%s': %s\n%s\n", path, statErr, lstatErr) 237 os.Exit(1) 238 } 239 240 return false 241} 242 243// maybeCleanSymlinkForest will remove the whole symlink forest directory if the version recorded 244// in the symlink_forest_version file is not equal to symlinkForestVersion. 245func maybeCleanSymlinkForest(topdir, forest string, verbose bool) error { 246 versionFilePath := shared.JoinPath(topdir, forest, "symlink_forest_version") 247 versionFileContents, err := os.ReadFile(versionFilePath) 248 if err != nil && !os.IsNotExist(err) { 249 return err 250 } 251 versionFileString := strings.TrimSpace(string(versionFileContents)) 252 symlinkForestVersionString := strconv.Itoa(symlinkForestVersion) 253 if err != nil || versionFileString != symlinkForestVersionString { 254 if verbose { 255 fmt.Fprintf(os.Stderr, "Old symlink_forest_version was %q, current is %q. Cleaning symlink forest before recreating...\n", versionFileString, symlinkForestVersionString) 256 } 257 err = os.RemoveAll(shared.JoinPath(topdir, forest)) 258 if err != nil { 259 return err 260 } 261 } 262 return nil 263} 264 265// maybeWriteVersionFile will write the symlink_forest_version file containing symlinkForestVersion 266// if it doesn't exist already. If it exists we know it must contain symlinkForestVersion because 267// we checked for that already in maybeCleanSymlinkForest 268func maybeWriteVersionFile(topdir, forest string) error { 269 versionFilePath := shared.JoinPath(topdir, forest, "symlink_forest_version") 270 _, err := os.Stat(versionFilePath) 271 if err != nil { 272 if !os.IsNotExist(err) { 273 return err 274 } 275 err = os.WriteFile(versionFilePath, []byte(strconv.Itoa(symlinkForestVersion)+"\n"), 0666) 276 if err != nil { 277 return err 278 } 279 } 280 return nil 281} 282 283// Recursively plants a symlink forest at forestDir. The symlink tree will 284// contain every file in buildFilesDir and srcDir excluding the files in 285// instructions. Collects every directory encountered during the traversal of 286// srcDir . 287func plantSymlinkForestRecursive(context *symlinkForestContext, instructions *instructionsNode, forestDir string, buildFilesDir string, srcDir string) { 288 defer context.wg.Done() 289 290 if instructions != nil && instructions.excluded { 291 // Excluded paths are skipped at the level of the non-excluded parent. 292 fmt.Fprintf(os.Stderr, "may not specify a root-level exclude directory '%s'", srcDir) 293 os.Exit(1) 294 } 295 296 // We don't add buildFilesDir here because the bp2build files marker files is 297 // already a dependency which covers it. If we ever wanted to turn this into 298 // a generic symlink forest creation tool, we'd need to add it, too. 299 context.depCh <- srcDir 300 301 srcDirMap := readdirToMap(shared.JoinPath(context.topdir, srcDir)) 302 buildFilesMap := readdirToMap(shared.JoinPath(context.topdir, buildFilesDir)) 303 304 renamingBuildFile := false 305 if _, ok := srcDirMap["BUILD"]; ok { 306 if _, ok := srcDirMap["BUILD.bazel"]; !ok { 307 if _, ok := buildFilesMap["BUILD.bazel"]; ok { 308 renamingBuildFile = true 309 srcDirMap["BUILD.bazel"] = srcDirMap["BUILD"] 310 delete(srcDirMap, "BUILD") 311 if instructions != nil { 312 if _, ok := instructions.children["BUILD"]; ok { 313 instructions.children["BUILD.bazel"] = instructions.children["BUILD"] 314 delete(instructions.children, "BUILD") 315 } 316 } 317 } 318 } 319 } 320 321 allEntries := make([]string, 0, len(srcDirMap)+len(buildFilesMap)) 322 for n := range srcDirMap { 323 allEntries = append(allEntries, n) 324 } 325 for n := range buildFilesMap { 326 if _, ok := srcDirMap[n]; !ok { 327 allEntries = append(allEntries, n) 328 } 329 } 330 // Tests read the error messages generated, so ensure their order is deterministic 331 sort.Strings(allEntries) 332 333 fullForestPath := shared.JoinPath(context.topdir, forestDir) 334 createForestDir := false 335 if fi, err := os.Lstat(fullForestPath); err != nil { 336 if os.IsNotExist(err) { 337 createForestDir = true 338 } else { 339 fmt.Fprintf(os.Stderr, "Could not read info for '%s': %s\n", forestDir, err) 340 } 341 } else if fi.Mode()&os.ModeDir == 0 { 342 if err := os.RemoveAll(fullForestPath); err != nil { 343 fmt.Fprintf(os.Stderr, "Failed to remove '%s': %s", forestDir, err) 344 os.Exit(1) 345 } 346 createForestDir = true 347 } 348 if createForestDir { 349 if err := os.MkdirAll(fullForestPath, 0777); err != nil { 350 fmt.Fprintf(os.Stderr, "Could not mkdir '%s': %s\n", forestDir, err) 351 os.Exit(1) 352 } 353 context.mkdirCount.Add(1) 354 } 355 356 // Start with a list of items that already exist in the forest, and remove 357 // each element as it is processed in allEntries. Any remaining items in 358 // forestMapForDeletion must be removed. (This handles files which were 359 // removed since the previous forest generation). 360 forestMapForDeletion := readdirToMap(shared.JoinPath(context.topdir, forestDir)) 361 362 for _, f := range allEntries { 363 if f[0] == '.' { 364 continue // Ignore dotfiles 365 } 366 delete(forestMapForDeletion, f) 367 // todo add deletionCount metric 368 369 // The full paths of children in the input trees and in the output tree 370 forestChild := shared.JoinPath(forestDir, f) 371 srcChild := shared.JoinPath(srcDir, f) 372 if f == "BUILD.bazel" && renamingBuildFile { 373 srcChild = shared.JoinPath(srcDir, "BUILD") 374 } 375 buildFilesChild := shared.JoinPath(buildFilesDir, f) 376 377 // Descend in the instruction tree if it exists 378 var instructionsChild *instructionsNode 379 if instructions != nil { 380 instructionsChild = instructions.children[f] 381 } 382 383 srcChildEntry, sExists := srcDirMap[f] 384 buildFilesChildEntry, bExists := buildFilesMap[f] 385 386 if instructionsChild != nil && instructionsChild.excluded { 387 if bExists { 388 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, buildFilesChild)) 389 } 390 continue 391 } 392 393 sDir := sExists && isDir(shared.JoinPath(context.topdir, srcChild), srcChildEntry) 394 bDir := bExists && isDir(shared.JoinPath(context.topdir, buildFilesChild), buildFilesChildEntry) 395 396 if !sExists { 397 if bDir && instructionsChild != nil { 398 // Not in the source tree, but we have to exclude something from under 399 // this subtree, so descend 400 context.wg.Add(1) 401 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild) 402 } else { 403 // Not in the source tree, symlink BUILD file 404 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, buildFilesChild)) 405 } 406 } else if !bExists { 407 if sDir && instructionsChild != nil { 408 // Not in the build file tree, but we have to exclude something from 409 // under this subtree, so descend 410 context.wg.Add(1) 411 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild) 412 } else { 413 // Not in the build file tree, symlink source tree, carry on 414 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, srcChild)) 415 } 416 } else if sDir && bDir { 417 // Both are directories. Descend. 418 context.wg.Add(1) 419 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild) 420 } else if !sDir && !bDir { 421 // Neither is a directory. Merge them. 422 srcBuildFile := shared.JoinPath(context.topdir, srcChild) 423 generatedBuildFile := shared.JoinPath(context.topdir, buildFilesChild) 424 // The Android.bp file that codegen used to produce `buildFilesChild` is 425 // already a dependency, we can ignore `buildFilesChild`. 426 context.depCh <- srcChild 427 if err := mergeBuildFiles(shared.JoinPath(context.topdir, forestChild), srcBuildFile, generatedBuildFile, context.verbose); err != nil { 428 fmt.Fprintf(os.Stderr, "Error merging %s and %s: %s", 429 srcBuildFile, generatedBuildFile, err) 430 os.Exit(1) 431 } 432 } else { 433 // Both exist and one is a file. This is an error. 434 fmt.Fprintf(os.Stderr, 435 "Conflict in workspace symlink tree creation: both '%s' and '%s' exist and exactly one is a directory\n", 436 srcChild, buildFilesChild) 437 os.Exit(1) 438 } 439 } 440 441 // Remove all files in the forest that exist in neither the source 442 // tree nor the build files tree. (This handles files which were removed 443 // since the previous forest generation). 444 for f := range forestMapForDeletion { 445 var instructionsChild *instructionsNode 446 if instructions != nil { 447 instructionsChild = instructions.children[f] 448 } 449 450 if instructionsChild != nil && instructionsChild.excluded { 451 // This directory may be excluded because bazel writes to it under the 452 // forest root. Thus this path is intentionally left alone. 453 continue 454 } 455 forestChild := shared.JoinPath(context.topdir, forestDir, f) 456 if err := os.RemoveAll(forestChild); err != nil { 457 fmt.Fprintf(os.Stderr, "Failed to remove '%s/%s': %s", forestDir, f, err) 458 os.Exit(1) 459 } 460 } 461} 462 463// PlantSymlinkForest Creates a symlink forest by merging the directory tree at "buildFiles" and 464// "srcDir" while excluding paths listed in "exclude". Returns the set of paths 465// under srcDir on which readdir() had to be called to produce the symlink 466// forest. 467func PlantSymlinkForest(verbose bool, topdir string, forest string, buildFiles string, exclude []string) (deps []string, mkdirCount, symlinkCount uint64) { 468 context := &symlinkForestContext{ 469 verbose: verbose, 470 topdir: topdir, 471 depCh: make(chan string), 472 mkdirCount: atomic.Uint64{}, 473 symlinkCount: atomic.Uint64{}, 474 } 475 476 err := maybeCleanSymlinkForest(topdir, forest, verbose) 477 if err != nil { 478 fmt.Fprintln(os.Stderr, err) 479 os.Exit(1) 480 } 481 482 instructions := instructionsFromExcludePathList(exclude) 483 go func() { 484 context.wg.Add(1) 485 plantSymlinkForestRecursive(context, instructions, forest, buildFiles, ".") 486 context.wg.Wait() 487 close(context.depCh) 488 }() 489 490 for dep := range context.depCh { 491 deps = append(deps, dep) 492 } 493 494 err = maybeWriteVersionFile(topdir, forest) 495 if err != nil { 496 fmt.Fprintln(os.Stderr, err) 497 os.Exit(1) 498 } 499 500 return deps, context.mkdirCount.Load(), context.symlinkCount.Load() 501} 502