blob: 183eb12059e81852168efedaab76264f480ecd9f [file] [log] [blame]
Cole Faust324a92e2022-08-23 15:29:05 -07001// 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
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020015package bp2build
16
17import (
18 "fmt"
19 "io/ioutil"
20 "os"
21 "path/filepath"
Cole Faust324a92e2022-08-23 15:29:05 -070022 "regexp"
Cole Faust358ba4f2023-01-18 15:00:42 -080023 "sort"
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +000024 "sync"
25 "sync/atomic"
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020026
27 "android/soong/shared"
28)
29
30// A tree structure that describes what to do at each directory in the created
31// symlink tree. Currently it is used to enumerate which files/directories
32// should be excluded from symlinking. Each instance of "node" represents a file
33// or a directory. If excluded is true, then that file/directory should be
34// excluded from symlinking. Otherwise, the node is not excluded, but one of its
35// descendants is (otherwise the node in question would not exist)
Lukacs T. Berkic541cd22022-10-26 07:26:50 +000036
37type instructionsNode struct {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020038 name string
39 excluded bool // If false, this is just an intermediate node
Lukacs T. Berkic541cd22022-10-26 07:26:50 +000040 children map[string]*instructionsNode
41}
42
43type symlinkForestContext struct {
44 verbose bool
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +000045 topdir string // $TOPDIR
46
47 // State
Usta Shresthada15c612022-11-08 14:12:36 -050048 wg sync.WaitGroup
49 depCh chan string
50 mkdirCount atomic.Uint64
51 symlinkCount atomic.Uint64
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020052}
53
Usta Shresthadb46a9b2022-07-11 11:29:56 -040054// Ensures that the node for the given path exists in the tree and returns it.
Lukacs T. Berkic541cd22022-10-26 07:26:50 +000055func ensureNodeExists(root *instructionsNode, path string) *instructionsNode {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020056 if path == "" {
57 return root
58 }
59
60 if path[len(path)-1] == '/' {
61 path = path[:len(path)-1] // filepath.Split() leaves a trailing slash
62 }
63
64 dir, base := filepath.Split(path)
65
66 // First compute the parent node...
67 dn := ensureNodeExists(root, dir)
68
69 // then create the requested node as its direct child, if needed.
70 if child, ok := dn.children[base]; ok {
71 return child
72 } else {
Lukacs T. Berkic541cd22022-10-26 07:26:50 +000073 dn.children[base] = &instructionsNode{base, false, make(map[string]*instructionsNode)}
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020074 return dn.children[base]
75 }
76}
77
Lukacs T. Berkic541cd22022-10-26 07:26:50 +000078// Turns a list of paths to be excluded into a tree
79func instructionsFromExcludePathList(paths []string) *instructionsNode {
80 result := &instructionsNode{"", false, make(map[string]*instructionsNode)}
Lukacs T. Berkib353cca2021-04-16 13:47:36 +020081
82 for _, p := range paths {
83 ensureNodeExists(result, p).excluded = true
84 }
85
86 return result
87}
88
Cole Faust324a92e2022-08-23 15:29:05 -070089func mergeBuildFiles(output string, srcBuildFile string, generatedBuildFile string, verbose bool) error {
90
91 srcBuildFileContent, err := os.ReadFile(srcBuildFile)
92 if err != nil {
93 return err
94 }
95
96 generatedBuildFileContent, err := os.ReadFile(generatedBuildFile)
97 if err != nil {
98 return err
99 }
100
101 // There can't be a package() call in both the source and generated BUILD files.
102 // bp2build will generate a package() call for licensing information, but if
103 // there's no licensing information, it will still generate a package() call
104 // that just sets default_visibility=public. If the handcrafted build file
105 // also has a package() call, we'll allow it to override the bp2build
106 // generated one if it doesn't have any licensing information. If the bp2build
107 // one has licensing information and the handcrafted one exists, we'll leave
108 // them both in for bazel to throw an error.
109 packageRegex := regexp.MustCompile(`(?m)^package\s*\(`)
110 packageDefaultVisibilityRegex := regexp.MustCompile(`(?m)^package\s*\(\s*default_visibility\s*=\s*\[\s*"//visibility:public",?\s*]\s*\)`)
111 if packageRegex.Find(srcBuildFileContent) != nil {
112 if verbose && packageDefaultVisibilityRegex.Find(generatedBuildFileContent) != nil {
113 fmt.Fprintf(os.Stderr, "Both '%s' and '%s' have a package() target, removing the first one\n",
114 generatedBuildFile, srcBuildFile)
115 }
116 generatedBuildFileContent = packageDefaultVisibilityRegex.ReplaceAll(generatedBuildFileContent, []byte{})
117 }
118
119 outFile, err := os.Create(output)
120 if err != nil {
121 return err
122 }
123
124 _, err = outFile.Write(generatedBuildFileContent)
125 if err != nil {
126 return err
127 }
128
129 if generatedBuildFileContent[len(generatedBuildFileContent)-1] != '\n' {
130 _, err = outFile.WriteString("\n")
131 if err != nil {
132 return err
133 }
134 }
135
136 _, err = outFile.Write(srcBuildFileContent)
137 return err
138}
139
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200140// Calls readdir() and returns it as a map from the basename of the files in dir
141// to os.FileInfo.
142func readdirToMap(dir string) map[string]os.FileInfo {
143 entryList, err := ioutil.ReadDir(dir)
144 result := make(map[string]os.FileInfo)
145
146 if err != nil {
147 if os.IsNotExist(err) {
148 // It's okay if a directory doesn't exist; it just means that one of the
149 // trees to be merged contains parts the other doesn't
150 return result
151 } else {
152 fmt.Fprintf(os.Stderr, "Cannot readdir '%s': %s\n", dir, err)
153 os.Exit(1)
154 }
155 }
156
157 for _, fi := range entryList {
158 result[fi.Name()] = fi
159 }
160
161 return result
162}
163
164// Creates a symbolic link at dst pointing to src
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500165func symlinkIntoForest(topdir, dst, src string) uint64 {
166 srcPath := shared.JoinPath(topdir, src)
167 dstPath := shared.JoinPath(topdir, dst)
168
169 // Check if a symlink already exists.
170 if dstInfo, err := os.Lstat(dstPath); err != nil {
171 if !os.IsNotExist(err) {
172 fmt.Fprintf(os.Stderr, "Failed to lstat '%s': %s", dst, err)
173 os.Exit(1)
174 }
175 } else {
176 if dstInfo.Mode()&os.ModeSymlink != 0 {
177 // Assume that the link's target is correct, i.e. no manual tampering.
178 // E.g. OUT_DIR could have been previously used with a different source tree check-out!
179 return 0
180 } else {
181 if err := os.RemoveAll(dstPath); err != nil {
182 fmt.Fprintf(os.Stderr, "Failed to remove '%s': %s", dst, err)
183 os.Exit(1)
184 }
185 }
186 }
187
188 // Create symlink.
189 if err := os.Symlink(srcPath, dstPath); err != nil {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200190 fmt.Fprintf(os.Stderr, "Cannot create symlink at '%s' pointing to '%s': %s", dst, src, err)
191 os.Exit(1)
192 }
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500193 return 1
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200194}
195
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200196func isDir(path string, fi os.FileInfo) bool {
197 if (fi.Mode() & os.ModeSymlink) != os.ModeSymlink {
198 return fi.IsDir()
199 }
200
Jingwen Chend4b1dc82022-05-12 11:08:03 +0000201 fi2, statErr := os.Stat(path)
202 if statErr == nil {
203 return fi2.IsDir()
204 }
205
206 // Check if this is a dangling symlink. If so, treat it like a file, not a dir.
207 _, lstatErr := os.Lstat(path)
208 if lstatErr != nil {
209 fmt.Fprintf(os.Stderr, "Cannot stat or lstat '%s': %s\n%s\n", path, statErr, lstatErr)
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200210 os.Exit(1)
211 }
212
Jingwen Chend4b1dc82022-05-12 11:08:03 +0000213 return false
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200214}
215
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200216// Recursively plants a symlink forest at forestDir. The symlink tree will
217// contain every file in buildFilesDir and srcDir excluding the files in
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000218// instructions. Collects every directory encountered during the traversal of
219// srcDir .
220func plantSymlinkForestRecursive(context *symlinkForestContext, instructions *instructionsNode, forestDir string, buildFilesDir string, srcDir string) {
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000221 defer context.wg.Done()
222
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000223 if instructions != nil && instructions.excluded {
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500224 // Excluded paths are skipped at the level of the non-excluded parent.
225 fmt.Fprintf(os.Stderr, "may not specify a root-level exclude directory '%s'", srcDir)
226 os.Exit(1)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200227 }
228
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000229 // We don't add buildFilesDir here because the bp2build files marker files is
230 // already a dependency which covers it. If we ever wanted to turn this into
231 // a generic symlink forest creation tool, we'd need to add it, too.
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000232 context.depCh <- srcDir
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000233
234 srcDirMap := readdirToMap(shared.JoinPath(context.topdir, srcDir))
235 buildFilesMap := readdirToMap(shared.JoinPath(context.topdir, buildFilesDir))
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200236
Cole Faust324a92e2022-08-23 15:29:05 -0700237 renamingBuildFile := false
238 if _, ok := srcDirMap["BUILD"]; ok {
239 if _, ok := srcDirMap["BUILD.bazel"]; !ok {
240 if _, ok := buildFilesMap["BUILD.bazel"]; ok {
241 renamingBuildFile = true
242 srcDirMap["BUILD.bazel"] = srcDirMap["BUILD"]
243 delete(srcDirMap, "BUILD")
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500244 if instructions != nil {
245 if _, ok := instructions.children["BUILD"]; ok {
246 instructions.children["BUILD.bazel"] = instructions.children["BUILD"]
247 delete(instructions.children, "BUILD")
248 }
249 }
Cole Faust324a92e2022-08-23 15:29:05 -0700250 }
251 }
252 }
253
Cole Faust358ba4f2023-01-18 15:00:42 -0800254 allEntries := make([]string, 0, len(srcDirMap)+len(buildFilesMap))
Usta Shresthadb46a9b2022-07-11 11:29:56 -0400255 for n := range srcDirMap {
Cole Faust358ba4f2023-01-18 15:00:42 -0800256 allEntries = append(allEntries, n)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200257 }
Usta Shresthadb46a9b2022-07-11 11:29:56 -0400258 for n := range buildFilesMap {
Cole Faust358ba4f2023-01-18 15:00:42 -0800259 if _, ok := srcDirMap[n]; !ok {
260 allEntries = append(allEntries, n)
261 }
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200262 }
Cole Faust358ba4f2023-01-18 15:00:42 -0800263 // Tests read the error messages generated, so ensure their order is deterministic
264 sort.Strings(allEntries)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200265
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500266 fullForestPath := shared.JoinPath(context.topdir, forestDir)
267 createForestDir := false
268 if fi, err := os.Lstat(fullForestPath); err != nil {
269 if os.IsNotExist(err) {
270 createForestDir = true
271 } else {
272 fmt.Fprintf(os.Stderr, "Could not read info for '%s': %s\n", forestDir, err)
273 }
274 } else if fi.Mode()&os.ModeDir == 0 {
275 if err := os.RemoveAll(fullForestPath); err != nil {
276 fmt.Fprintf(os.Stderr, "Failed to remove '%s': %s", forestDir, err)
277 os.Exit(1)
278 }
279 createForestDir = true
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200280 }
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500281 if createForestDir {
282 if err := os.MkdirAll(fullForestPath, 0777); err != nil {
283 fmt.Fprintf(os.Stderr, "Could not mkdir '%s': %s\n", forestDir, err)
284 os.Exit(1)
285 }
286 context.mkdirCount.Add(1)
287 }
288
289 // Start with a list of items that already exist in the forest, and remove
290 // each element as it is processed in allEntries. Any remaining items in
291 // forestMapForDeletion must be removed. (This handles files which were
292 // removed since the previous forest generation).
293 forestMapForDeletion := readdirToMap(shared.JoinPath(context.topdir, forestDir))
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200294
Cole Faust358ba4f2023-01-18 15:00:42 -0800295 for _, f := range allEntries {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200296 if f[0] == '.' {
297 continue // Ignore dotfiles
298 }
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500299 delete(forestMapForDeletion, f)
300 // todo add deletionCount metric
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200301
302 // The full paths of children in the input trees and in the output tree
Lukacs T. Berki3f9416e2021-04-20 13:08:11 +0200303 forestChild := shared.JoinPath(forestDir, f)
304 srcChild := shared.JoinPath(srcDir, f)
Cole Faust324a92e2022-08-23 15:29:05 -0700305 if f == "BUILD.bazel" && renamingBuildFile {
306 srcChild = shared.JoinPath(srcDir, "BUILD")
307 }
Lukacs T. Berki3f9416e2021-04-20 13:08:11 +0200308 buildFilesChild := shared.JoinPath(buildFilesDir, f)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200309
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000310 // Descend in the instruction tree if it exists
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500311 var instructionsChild *instructionsNode
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000312 if instructions != nil {
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500313 instructionsChild = instructions.children[f]
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200314 }
315
Lukacs T. Berki3f9416e2021-04-20 13:08:11 +0200316 srcChildEntry, sExists := srcDirMap[f]
317 buildFilesChildEntry, bExists := buildFilesMap[f]
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200318
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000319 if instructionsChild != nil && instructionsChild.excluded {
Cole Faust324a92e2022-08-23 15:29:05 -0700320 if bExists {
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500321 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, buildFilesChild))
Cole Faust324a92e2022-08-23 15:29:05 -0700322 }
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200323 continue
324 }
325
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000326 sDir := sExists && isDir(shared.JoinPath(context.topdir, srcChild), srcChildEntry)
327 bDir := bExists && isDir(shared.JoinPath(context.topdir, buildFilesChild), buildFilesChildEntry)
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200328
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200329 if !sExists {
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000330 if bDir && instructionsChild != nil {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200331 // Not in the source tree, but we have to exclude something from under
332 // this subtree, so descend
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000333 context.wg.Add(1)
334 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200335 } else {
336 // Not in the source tree, symlink BUILD file
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500337 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, buildFilesChild))
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200338 }
339 } else if !bExists {
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000340 if sDir && instructionsChild != nil {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200341 // Not in the build file tree, but we have to exclude something from
342 // under this subtree, so descend
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000343 context.wg.Add(1)
344 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200345 } else {
346 // Not in the build file tree, symlink source tree, carry on
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500347 context.symlinkCount.Add(symlinkIntoForest(context.topdir, forestChild, srcChild))
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200348 }
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200349 } else if sDir && bDir {
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200350 // Both are directories. Descend.
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000351 context.wg.Add(1)
352 go plantSymlinkForestRecursive(context, instructionsChild, forestChild, buildFilesChild, srcChild)
Lukacs T. Berkie3487c82022-05-02 10:13:19 +0200353 } else if !sDir && !bDir {
Cole Faust324a92e2022-08-23 15:29:05 -0700354 // Neither is a directory. Merge them.
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000355 srcBuildFile := shared.JoinPath(context.topdir, srcChild)
356 generatedBuildFile := shared.JoinPath(context.topdir, buildFilesChild)
Usta Shrestha783ec5c2022-10-25 01:22:36 -0400357 // The Android.bp file that codegen used to produce `buildFilesChild` is
358 // already a dependency, we can ignore `buildFilesChild`.
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000359 context.depCh <- srcChild
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500360 if err := mergeBuildFiles(shared.JoinPath(context.topdir, forestChild), srcBuildFile, generatedBuildFile, context.verbose); err != nil {
Cole Faust324a92e2022-08-23 15:29:05 -0700361 fmt.Fprintf(os.Stderr, "Error merging %s and %s: %s",
362 srcBuildFile, generatedBuildFile, err)
Usta Shrestha071f6c22022-10-25 12:34:06 -0400363 os.Exit(1)
Sasha Smundak0fd93e02022-05-19 19:34:31 -0700364 }
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200365 } else {
366 // Both exist and one is a file. This is an error.
367 fmt.Fprintf(os.Stderr,
Lukacs T. Berkib21166e2021-04-21 11:58:36 +0200368 "Conflict in workspace symlink tree creation: both '%s' and '%s' exist and exactly one is a directory\n",
Lukacs T. Berki3f9416e2021-04-20 13:08:11 +0200369 srcChild, buildFilesChild)
Usta Shrestha071f6c22022-10-25 12:34:06 -0400370 os.Exit(1)
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200371 }
372 }
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200373
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500374 // Remove all files in the forest that exist in neither the source
375 // tree nor the build files tree. (This handles files which were removed
376 // since the previous forest generation).
377 for f := range forestMapForDeletion {
378 var instructionsChild *instructionsNode
379 if instructions != nil {
380 instructionsChild = instructions.children[f]
Lukacs T. Berkibc5f7312022-10-31 09:01:34 +0000381 }
382
Usta (Tsering) Shresthac4c07b12022-11-08 18:31:14 -0500383 if instructionsChild != nil && instructionsChild.excluded {
384 // This directory may be excluded because bazel writes to it under the
385 // forest root. Thus this path is intentionally left alone.
386 continue
387 }
388 forestChild := shared.JoinPath(context.topdir, forestDir, f)
389 if err := os.RemoveAll(forestChild); err != nil {
390 fmt.Fprintf(os.Stderr, "Failed to remove '%s/%s': %s", forestDir, f, err)
Lukacs T. Berkibc5f7312022-10-31 09:01:34 +0000391 os.Exit(1)
392 }
Lukacs T. Berkibc5f7312022-10-31 09:01:34 +0000393 }
Lukacs T. Berkibc5f7312022-10-31 09:01:34 +0000394}
395
Usta Shresthada15c612022-11-08 14:12:36 -0500396// PlantSymlinkForest Creates a symlink forest by merging the directory tree at "buildFiles" and
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200397// "srcDir" while excluding paths listed in "exclude". Returns the set of paths
398// under srcDir on which readdir() had to be called to produce the symlink
399// forest.
Usta Shresthada15c612022-11-08 14:12:36 -0500400func PlantSymlinkForest(verbose bool, topdir string, forest string, buildFiles string, exclude []string) (deps []string, mkdirCount, symlinkCount uint64) {
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000401 context := &symlinkForestContext{
Usta Shresthada15c612022-11-08 14:12:36 -0500402 verbose: verbose,
403 topdir: topdir,
404 depCh: make(chan string),
405 mkdirCount: atomic.Uint64{},
406 symlinkCount: atomic.Uint64{},
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000407 }
408
Lukacs T. Berkic541cd22022-10-26 07:26:50 +0000409 instructions := instructionsFromExcludePathList(exclude)
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000410 go func() {
411 context.wg.Add(1)
412 plantSymlinkForestRecursive(context, instructions, forest, buildFiles, ".")
413 context.wg.Wait()
414 close(context.depCh)
415 }()
416
Lukacs T. Berki647e7ab2022-10-27 09:55:38 +0000417 for dep := range context.depCh {
418 deps = append(deps, dep)
419 }
420
Usta Shresthada15c612022-11-08 14:12:36 -0500421 return deps, context.mkdirCount.Load(), context.symlinkCount.Load()
Lukacs T. Berkib353cca2021-04-16 13:47:36 +0200422}