• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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