github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/client/tree.go (about) 1 package client 2 3 // This module provides functionality for constructing a Merkle tree of uploadable inputs. 4 import ( 5 "context" 6 "fmt" 7 "os" 8 "path/filepath" 9 "regexp" 10 "sort" 11 "strings" 12 13 cpb "github.com/bazelbuild/remote-apis-sdks/go/api/command" 14 "github.com/bazelbuild/remote-apis-sdks/go/pkg/command" 15 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 16 "github.com/bazelbuild/remote-apis-sdks/go/pkg/filemetadata" 17 "github.com/bazelbuild/remote-apis-sdks/go/pkg/uploadinfo" 18 "github.com/pkg/errors" 19 20 repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2" 21 log "github.com/golang/glog" 22 ) 23 24 // treeNode represents a file tree, which is an intermediate representation used to encode a Merkle 25 // tree later. It corresponds roughly to a *repb.Directory, but with pointers, not digests, used to 26 // refer to other nodes. 27 type treeNode struct { 28 leaves map[string]*fileSysNode 29 children map[string]*treeNode 30 } 31 32 type fileNode struct { 33 ue *uploadinfo.Entry 34 isExecutable bool 35 } 36 37 type symlinkNode struct { 38 target string 39 } 40 41 type fileSysNode struct { 42 file *fileNode 43 emptyDirectoryMarker bool 44 symlink *symlinkNode 45 nodeProperties *cpb.NodeProperties 46 } 47 48 // TreeStats contains various stats/metadata of the constructed Merkle tree. 49 // Note that these stats count the overall input tree, even if some parts of it are not unique. 50 // For example, if a file "foo" of 10 bytes occurs 5 times in the tree, it will be counted as 5 51 // InputFiles and 50 TotalInputBytes. 52 type TreeStats struct { 53 // The total number of input files. 54 InputFiles int 55 // The total number of input directories. 56 InputDirectories int 57 // The total number of input symlinks 58 InputSymlinks int 59 // The overall number of bytes from all the inputs. 60 TotalInputBytes int64 61 // TODO(olaola): number of FileMetadata cache hits/misses go here. 62 } 63 64 // TreeSymlinkOpts controls how symlinks are handled when constructing a tree. 65 type TreeSymlinkOpts struct { 66 // By default, a symlink is converted into its targeted file. 67 // If true, preserve the symlink. 68 Preserved bool 69 // If true, the symlink target (if not dangling) is followed. 70 FollowsTarget bool 71 // If true, overrides Preserved=true for symlinks that point outside the 72 // exec root, converting them into their targeted files while preserving 73 // symlinks that point to files within the exec root. Has no effect if 74 // Preserved=false, as all symlinks are materialized. 75 MaterializeOutsideExecRoot bool 76 } 77 78 // DefaultTreeSymlinkOpts returns a default DefaultTreeSymlinkOpts object. 79 func DefaultTreeSymlinkOpts() *TreeSymlinkOpts { 80 return &TreeSymlinkOpts{ 81 FollowsTarget: true, 82 } 83 } 84 85 // treeSymlinkOpts returns a TreeSymlinkOpts object based on the given SymlinkBehaviorType. 86 func treeSymlinkOpts(opts *TreeSymlinkOpts, sb command.SymlinkBehaviorType) *TreeSymlinkOpts { 87 if opts == nil { 88 opts = DefaultTreeSymlinkOpts() 89 } 90 switch sb { 91 case command.ResolveSymlink: 92 opts.Preserved = false 93 case command.PreserveSymlink: 94 opts.Preserved = true 95 } 96 return opts 97 } 98 99 // shouldIgnore returns whether a given input should be excluded based on the given InputExclusions, 100 func shouldIgnore(inp string, t command.InputType, excl []*command.InputExclusion) bool { 101 for _, r := range excl { 102 if r.Type != command.UnspecifiedInputType && r.Type != t { 103 continue 104 } 105 if m, _ := regexp.MatchString(r.Regex, inp); m { 106 return true 107 } 108 } 109 return false 110 } 111 112 // shouldIgnoreErr returns whether a given error should be ignored. 113 func shouldIgnoreErr(err error) bool { 114 // We should skip files without read permissions. If the user doesn't have read permissions, 115 // the file is unlikely to be used in the build in the first place. 116 if e, ok := err.(*filemetadata.FileError); ok { 117 return os.IsPermission(e.Err) 118 } 119 return os.IsPermission(err) 120 } 121 122 func getRelPath(base, path string) (string, error) { 123 rel, err := filepath.Rel(base, path) 124 if err != nil { 125 return "", err 126 } 127 if strings.HasPrefix(rel, "..") { 128 return "", fmt.Errorf("path %v is not under %v", path, base) 129 } 130 return rel, nil 131 } 132 133 // getTargetRelPath returns two versions of targetPath, the first is relative to execRoot 134 // and the second is relative to the directory of symlinkRelPath. 135 // symlinkRelPath must be relative to execRoot. 136 // targetPath must either be absolute or relative to the directory of symlinkRelPath. 137 // If targetPath is not a descendant of execRoot, an error is returned. 138 func getTargetRelPath(execRoot, symlinkRelPath string, targetPath string) (relExecRoot string, relSymlinkDir string, err error) { 139 symlinkAbsDir := filepath.Join(execRoot, filepath.Dir(symlinkRelPath)) 140 if !filepath.IsAbs(targetPath) { 141 targetPath = filepath.Join(symlinkAbsDir, targetPath) 142 } 143 144 relExecRoot, err = getRelPath(execRoot, targetPath) 145 if err != nil { 146 return "", "", err 147 } 148 149 relSymlinkDir, err = filepath.Rel(symlinkAbsDir, targetPath) 150 return relExecRoot, relSymlinkDir, err 151 } 152 153 // getRemotePath generates a remote path for a given local path 154 // by replacing workingDir component with remoteWorkingDir 155 func getRemotePath(path, workingDir, remoteWorkingDir string) (string, error) { 156 workingDirRelPath, err := filepath.Rel(workingDir, path) 157 if err != nil { 158 return "", fmt.Errorf("getRemotePath failed while trying to get working dir relative path of %q, err: %v", path, err) 159 } 160 remotePath := filepath.Join(remoteWorkingDir, workingDirRelPath) 161 return remotePath, nil 162 } 163 164 // getExecRootRelPaths returns local and remote exec-root-relative paths for a given local absolute path 165 // path may be relative or absolute. In both cases it's joined to and relativised to the execRoot. 166 // This has unintuitive implications. For example, execRoot=/root and path=/foo, returns relPath=foo. 167 func getExecRootRelPaths(path, execRoot, workingDir, remoteWorkingDir string) (relPath string, remoteRelPath string, err error) { 168 absPath := filepath.Join(execRoot, path) 169 if relPath, err = getRelPath(execRoot, absPath); err != nil { 170 return "", "", err 171 } 172 if remoteWorkingDir == "" || remoteWorkingDir == workingDir { 173 return relPath, relPath, nil 174 } 175 if remoteRelPath, err = getRemotePath(relPath, workingDir, remoteWorkingDir); err != nil { 176 return relPath, "", err 177 } 178 log.V(3).Infof("getExecRootRelPaths(%q, %q, %q, %q)=(%q, %q)", path, execRoot, workingDir, remoteWorkingDir, relPath, remoteRelPath) 179 return relPath, remoteRelPath, nil 180 } 181 182 // evalParentSymlinks replaces each parent element in relPath with its target if it's a symlink. 183 // 184 // Returns the evaluated path with a list of parent symlinks if any. All are relative to execRoot, but not necessarily descendents of it. 185 // Returned paths may not be filepath.Clean. 186 // The basename of relPath is not resolved. It remains a symlink if it is one. 187 // Any errors would be from accessing files. 188 // Example: execRoot=/a relPath=b/c/d/e.go, b->bb, evaledPath=/a/bb/c/d/e.go, symlinks=[a/b] 189 func evalParentSymlinks(execRoot, relPath string, materializeOutsideExecRoot bool, fmdCache filemetadata.Cache) (string, []string, error) { 190 var symlinks []string 191 evaledPathBuilder := strings.Builder{} 192 // targetPathBuilder captures the absolute path to the evaluated target so far. 193 // It is effectively what relative symlinks are relative to. If materialization 194 // is enabled, the materialized path may represent a different tree which makes it 195 // unusable with relative symlinks. 196 targetPathBuilder := strings.Builder{} 197 targetPathBuilder.WriteString(execRoot) 198 targetPathBuilder.WriteRune(filepath.Separator) 199 200 ps := strings.Split(relPath, string(filepath.Separator)) 201 lastIndex := len(ps) - 1 202 for i, p := range ps { 203 if i != 0 { 204 evaledPathBuilder.WriteRune(filepath.Separator) 205 targetPathBuilder.WriteRune(filepath.Separator) 206 } 207 if i == lastIndex { 208 // Do not resolve basename. 209 evaledPathBuilder.WriteString(p) 210 break 211 } 212 213 relP := evaledPathBuilder.String() + p 214 absP := filepath.Join(execRoot, relP) 215 fmd := fmdCache.Get(absP) 216 if fmd.Symlink == nil { 217 // Not a symlink. 218 evaledPathBuilder.WriteString(p) 219 targetPathBuilder.WriteString(p) 220 continue 221 } 222 223 if filepath.IsAbs(fmd.Symlink.Target) { 224 targetPathBuilder.Reset() 225 } 226 targetPathBuilder.WriteString(fmd.Symlink.Target) 227 228 _, targetRelSymlinkDir, err := getTargetRelPath(execRoot, relP, targetPathBuilder.String()) 229 if err != nil { 230 if materializeOutsideExecRoot { 231 evaledPathBuilder.WriteString(p) 232 continue 233 } 234 return "", nil, err 235 } 236 evaledPathBuilder.WriteString(targetRelSymlinkDir) 237 symlinks = append(symlinks, relP) 238 } 239 return evaledPathBuilder.String(), symlinks, nil 240 } 241 242 // loadIntermediateSymlinks inserts symlink nodes into fs. 243 // If the symlink source path already exists in fs (e.g. a for a-->../a_file), it is not 244 // overwritten, which means the first entry wins. 245 // This helps avoid redundant allocations from shared ancestors. 246 // For example, if fs["a/b/c"] is already associated with a symlink node with target ../c_target, and symlinks has 247 // "a/b/c"-->../cc_target, the result will not change and fs["a/b/c"] will still point to ../c_target. 248 // However, the case should always be that the target is identical. 249 func loadIntermediateSymlinks(symlinks []string, execRoot, workingDir, remoteWorkingDir string, cache filemetadata.Cache, fs map[string]*fileSysNode) error { 250 for _, relPath := range symlinks { 251 relPath, remoteRelPath, err := getExecRootRelPaths(relPath, execRoot, workingDir, remoteWorkingDir) 252 if err != nil { 253 return err 254 } 255 // Only skip if the path is already associated with a symlink node. 256 // This also means that an existing non-symlink node will get overwritten. 257 if n := fs[remoteRelPath]; n != nil && n.symlink != nil { 258 log.V(3).Infof("loadIntermediateSymlinks.Skipped: symlink=%s", relPath) 259 continue 260 } 261 absPath := filepath.Join(execRoot, relPath) 262 meta := cache.Get(absPath) 263 if meta.Symlink == nil { 264 return fmt.Errorf("%q is not a symlink", absPath) 265 } 266 _, targetSymDir, err := getTargetRelPath(execRoot, relPath, meta.Symlink.Target) 267 if err != nil { 268 return err 269 } 270 fs[remoteRelPath] = &fileSysNode{ 271 symlink: &symlinkNode{target: targetSymDir}, 272 } 273 log.V(3).Infof("loadIntermediateSymlinks: symlink=%s", relPath) 274 } 275 return nil 276 } 277 278 // loadFiles reads all files specified by the given InputSpec (descending into subdirectories 279 // recursively), and loads their contents into the provided map. 280 func loadFiles(execRoot, localWorkingDir, remoteWorkingDir string, excl []*command.InputExclusion, filesToProcess []string, fs map[string]*fileSysNode, cache filemetadata.Cache, opts *TreeSymlinkOpts, nodeProperties map[string]*cpb.NodeProperties) error { 281 if opts == nil { 282 opts = DefaultTreeSymlinkOpts() 283 } 284 285 for len(filesToProcess) != 0 { 286 relPath := filesToProcess[0] 287 filesToProcess = filesToProcess[1:] 288 289 if relPath == "" { 290 return errors.New("empty Input, use \".\" for entire exec root") 291 } 292 if opts.Preserved { 293 evaledPath, parentSymlinks, err := evalParentSymlinks(execRoot, relPath, opts.MaterializeOutsideExecRoot, cache) 294 log.V(3).Infof("loadFiles: path=%s, evaled=%s, parentSymlinks=%v, err=%v", relPath, evaledPath, parentSymlinks, err) 295 if err != nil { 296 return err 297 } 298 relPath = evaledPath 299 if err := loadIntermediateSymlinks(parentSymlinks, execRoot, localWorkingDir, remoteWorkingDir, cache, fs); err != nil { 300 return err 301 } 302 } 303 absPath := filepath.Join(execRoot, relPath) 304 normPath, remoteNormPath, err := getExecRootRelPaths(relPath, execRoot, localWorkingDir, remoteWorkingDir) 305 if err != nil { 306 return err 307 } 308 np := nodeProperties[remoteNormPath] 309 meta := cache.Get(absPath) 310 311 // An implication of this is that, if a path is a symlink to a 312 // directory, then the symlink attribute takes precedence. 313 if meta.Symlink != nil && meta.Symlink.IsDangling && !opts.Preserved { 314 // For now, we do not treat a dangling symlink as an error. In the case 315 // where the symlink is not preserved (i.e. needs to be converted to a 316 // file), we simply ignore this path in the finalized tree. 317 continue 318 } else if meta.Symlink != nil && opts.Preserved { 319 if shouldIgnore(absPath, command.SymlinkInputType, excl) { 320 continue 321 } 322 targetExecRoot, targetSymDir, err := getTargetRelPath(execRoot, normPath, meta.Symlink.Target) 323 if err != nil { 324 // The symlink points to a file outside the exec root. This is an 325 // error unless materialization of symlinks pointing outside the 326 // exec root is enabled. 327 if !opts.MaterializeOutsideExecRoot { 328 return errors.Wrapf(err, "failed to determine the target of symlink %q as a child of %q", normPath, execRoot) 329 } 330 if meta.Symlink.IsDangling { 331 return errors.Errorf("failed to materialize dangling symlink %q with target %q", normPath, meta.Symlink.Target) 332 } 333 goto processNonSymlink 334 } 335 336 fs[remoteNormPath] = &fileSysNode{ 337 // We cannot directly use meta.Symlink.Target, because it could be 338 // an absolute path. Since the remote worker will map the exec root 339 // to a different directory, we must strip away the local exec root. 340 // See https://github.com/bazelbuild/remote-apis-sdks/pull/229#discussion_r524830458 341 symlink: &symlinkNode{target: targetSymDir}, 342 nodeProperties: np, 343 } 344 345 if !meta.Symlink.IsDangling && opts.FollowsTarget { 346 // getTargetRelPath validates this target is under execRoot, 347 // and the iteration loop will get the relative path to execRoot, 348 filesToProcess = append(filesToProcess, targetExecRoot) 349 } 350 351 // Done processing this symlink, a subsequent iteration will process 352 // the targeted file if necessary. 353 continue 354 } 355 356 processNonSymlink: 357 log.V(3).Infof("loadFiles.non-sl: path=%s", relPath) 358 if meta.IsDirectory { 359 if shouldIgnore(absPath, command.DirectoryInputType, excl) { 360 continue 361 } else if meta.Err != nil { 362 if shouldIgnoreErr(meta.Err) { 363 continue 364 } 365 return meta.Err 366 } 367 368 f, err := os.Open(absPath) 369 if err != nil { 370 if shouldIgnoreErr(err) { 371 continue 372 } 373 return err 374 } 375 376 files, err := f.Readdirnames(-1) 377 f.Close() 378 if err != nil { 379 return err 380 } 381 382 if len(files) == 0 { 383 if normPath != "." { 384 fs[remoteNormPath] = &fileSysNode{emptyDirectoryMarker: true, nodeProperties: np} 385 } 386 continue 387 } 388 for _, f := range files { 389 filesToProcess = append(filesToProcess, filepath.Join(normPath, f)) 390 } 391 } else { 392 if shouldIgnore(absPath, command.FileInputType, excl) { 393 continue 394 } else if meta.Err != nil { 395 if shouldIgnoreErr(meta.Err) { 396 continue 397 } 398 return meta.Err 399 } 400 401 fs[remoteNormPath] = &fileSysNode{ 402 file: &fileNode{ 403 ue: uploadinfo.EntryFromFile(meta.Digest, absPath), 404 isExecutable: meta.IsExecutable, 405 }, 406 nodeProperties: np, 407 } 408 } 409 } 410 return nil 411 } 412 413 // ComputeMerkleTree packages an InputSpec into uploadable inputs, returned as uploadinfo.Entrys 414 func (c *Client) ComputeMerkleTree(ctx context.Context, execRoot, workingDir, remoteWorkingDir string, is *command.InputSpec, cache filemetadata.Cache) (root digest.Digest, inputs []*uploadinfo.Entry, stats *TreeStats, err error) { 415 stats = &TreeStats{} 416 fs := make(map[string]*fileSysNode) 417 slOpts := treeSymlinkOpts(c.TreeSymlinkOpts, is.SymlinkBehavior) 418 for _, i := range is.VirtualInputs { 419 if i.Path == "" { 420 return digest.Empty, nil, nil, errors.New("empty Path in VirtualInputs") 421 } 422 path := i.Path 423 if slOpts.Preserved { 424 evaledPath, parentSymlinks, err := evalParentSymlinks(execRoot, path, slOpts.MaterializeOutsideExecRoot, cache) 425 log.V(3).Infof("ComputeMerkleTree.VirtualInput: path=%s, evaled=%s, parentSymlinks=%v, err=%v", path, evaledPath, parentSymlinks, err) 426 if err != nil { 427 return digest.Empty, nil, nil, err 428 } 429 path = evaledPath 430 if err := loadIntermediateSymlinks(parentSymlinks, execRoot, workingDir, remoteWorkingDir, cache, fs); err != nil { 431 return digest.Empty, nil, nil, err 432 } 433 } 434 normPath, remoteNormPath, err := getExecRootRelPaths(path, execRoot, workingDir, remoteWorkingDir) 435 if err != nil { 436 return digest.Empty, nil, nil, err 437 } 438 np := is.InputNodeProperties[remoteNormPath] 439 if i.IsEmptyDirectory { 440 if normPath != "." { 441 fs[remoteNormPath] = &fileSysNode{emptyDirectoryMarker: true, nodeProperties: np} 442 } 443 continue 444 } 445 if i.Digest != "" && len(i.Contents) > 0 { 446 return digest.Empty, nil, nil, errors.New("digest and file content cannot be provided for the same virtual input") 447 } 448 var entry *uploadinfo.Entry 449 if i.Digest != "" { 450 dg, err := digest.NewFromString(i.Digest) 451 if err != nil { 452 return digest.Empty, nil, nil, err 453 } 454 absPath := filepath.Join(execRoot, normPath) 455 entry = uploadinfo.EntryFromVirtualFile(dg, absPath) 456 } else { 457 entry = uploadinfo.EntryFromBlob(i.Contents) 458 } 459 fs[remoteNormPath] = &fileSysNode{ 460 file: &fileNode{ 461 ue: entry, 462 isExecutable: i.IsExecutable, 463 }, 464 nodeProperties: np, 465 } 466 } 467 if err := loadFiles(execRoot, workingDir, remoteWorkingDir, is.InputExclusions, is.Inputs, fs, cache, slOpts, is.InputNodeProperties); err != nil { 468 return digest.Empty, nil, nil, err 469 } 470 ft, err := buildTree(fs) 471 if err != nil { 472 return digest.Empty, nil, nil, err 473 } 474 var blobs map[digest.Digest]*uploadinfo.Entry 475 root, blobs, err = packageTree(ft, stats) 476 if err != nil { 477 return digest.Empty, nil, nil, err 478 } 479 for _, ue := range blobs { 480 inputs = append(inputs, ue) 481 } 482 return root, inputs, stats, nil 483 } 484 485 func buildTree(files map[string]*fileSysNode) (*treeNode, error) { 486 root := &treeNode{} 487 for name, fn := range files { 488 segs := strings.Split(name, string(filepath.Separator)) 489 // The last segment is the filename, so split it off. 490 segs, base := segs[0:len(segs)-1], segs[len(segs)-1] 491 492 node := root 493 for _, s := range segs { 494 if node.children == nil { 495 node.children = make(map[string]*treeNode) 496 } 497 child := node.children[s] 498 if child == nil { 499 child = &treeNode{} 500 node.children[s] = child 501 } 502 node = child 503 } 504 505 if fn.emptyDirectoryMarker { 506 if node.children == nil { 507 node.children = make(map[string]*treeNode) 508 } 509 if node.children[base] == nil { 510 node.children[base] = &treeNode{} 511 } 512 continue 513 } 514 if node.leaves == nil { 515 node.leaves = make(map[string]*fileSysNode) 516 } 517 node.leaves[base] = fn 518 } 519 return root, nil 520 } 521 522 // If tree is not nil, it will be populated with a flattened tree of path->digest. 523 // prefix should always be provided as an empty string which will be used to accumolate path prefixes during recursion. 524 func packageTree(t *treeNode, stats *TreeStats) (root digest.Digest, blobs map[digest.Digest]*uploadinfo.Entry, err error) { 525 dir := &repb.Directory{} 526 blobs = make(map[digest.Digest]*uploadinfo.Entry) 527 528 for name, child := range t.children { 529 dg, childBlobs, err := packageTree(child, stats) 530 if err != nil { 531 return digest.Empty, nil, err 532 } 533 534 dir.Directories = append(dir.Directories, &repb.DirectoryNode{Name: name, Digest: dg.ToProto()}) 535 for d, b := range childBlobs { 536 blobs[d] = b 537 } 538 } 539 sort.Slice(dir.Directories, func(i, j int) bool { return dir.Directories[i].Name < dir.Directories[j].Name }) 540 541 for name, n := range t.leaves { 542 // A node can have exactly one of file/symlink/emptyDirectoryMarker. 543 if n.file != nil { 544 dg := n.file.ue.Digest 545 dir.Files = append(dir.Files, &repb.FileNode{Name: name, Digest: dg.ToProto(), IsExecutable: n.file.isExecutable, NodeProperties: command.NodePropertiesToAPI(n.nodeProperties)}) 546 blobs[dg] = n.file.ue 547 stats.InputFiles++ 548 stats.TotalInputBytes += dg.Size 549 continue 550 } 551 if n.symlink != nil { 552 dir.Symlinks = append(dir.Symlinks, &repb.SymlinkNode{Name: name, Target: n.symlink.target, NodeProperties: command.NodePropertiesToAPI(n.nodeProperties)}) 553 stats.InputSymlinks++ 554 } 555 } 556 557 sort.Slice(dir.Files, func(i, j int) bool { return dir.Files[i].Name < dir.Files[j].Name }) 558 sort.Slice(dir.Symlinks, func(i, j int) bool { return dir.Symlinks[i].Name < dir.Symlinks[j].Name }) 559 560 ue, err := uploadinfo.EntryFromProto(dir) 561 if err != nil { 562 return digest.Empty, nil, err 563 } 564 dg := ue.Digest 565 blobs[dg] = ue 566 stats.TotalInputBytes += dg.Size 567 stats.InputDirectories++ 568 return dg, blobs, nil 569 } 570 571 // TreeOutput represents a leaf output node in a nested directory structure (a file, a symlink, or an empty directory). 572 type TreeOutput struct { 573 Digest digest.Digest 574 Path string 575 IsExecutable bool 576 IsEmptyDirectory bool 577 SymlinkTarget string 578 NodeProperties *repb.NodeProperties 579 } 580 581 // FlattenTree takes a Tree message and calculates the relative paths of all the files to 582 // the tree root. Note that only files/symlinks/empty directories are included in the returned slice, 583 // not the intermediate directories. Directories containing only other directories will be omitted. 584 func (c *Client) FlattenTree(tree *repb.Tree, rootPath string) (map[string]*TreeOutput, error) { 585 root, err := digest.NewFromMessage(tree.Root) 586 if err != nil { 587 return nil, err 588 } 589 dirs := make(map[digest.Digest]*repb.Directory) 590 dirs[root] = tree.Root 591 for _, ue := range tree.Children { 592 dg, e := digest.NewFromMessage(ue) 593 if e != nil { 594 return nil, e 595 } 596 dirs[dg] = ue 597 } 598 return flattenTree(root, rootPath, dirs) 599 } 600 601 func flattenTree(root digest.Digest, rootPath string, dirs map[digest.Digest]*repb.Directory) (map[string]*TreeOutput, error) { 602 // Create a queue of unprocessed directories, along with their flattened 603 // path names. 604 type queueElem struct { 605 d digest.Digest 606 p string 607 } 608 queue := []*queueElem{} 609 queue = append(queue, &queueElem{d: root, p: rootPath}) 610 611 // Process the queue, recording all flattened TreeOutputs as we go. 612 flatFiles := make(map[string]*TreeOutput) 613 for len(queue) > 0 { 614 flatDir := queue[0] 615 queue = queue[1:] 616 617 dir, ok := dirs[flatDir.d] 618 if !ok { 619 return nil, fmt.Errorf("couldn't find directory %s with digest %s", flatDir.p, flatDir.d) 620 } 621 622 // Check whether this is an empty directory. 623 if len(dir.Files)+len(dir.Directories)+len(dir.Symlinks) == 0 { 624 flatFiles[flatDir.p] = &TreeOutput{ 625 Path: flatDir.p, 626 Digest: digest.Empty, 627 IsEmptyDirectory: true, 628 NodeProperties: dir.NodeProperties, 629 } 630 continue 631 } 632 // Add files to the set to return 633 for _, file := range dir.Files { 634 out := &TreeOutput{ 635 Path: filepath.Join(flatDir.p, file.Name), 636 Digest: digest.NewFromProtoUnvalidated(file.Digest), 637 IsExecutable: file.IsExecutable, 638 NodeProperties: file.NodeProperties, 639 } 640 flatFiles[out.Path] = out 641 } 642 643 // Add symlinks to the set to return 644 for _, sm := range dir.Symlinks { 645 out := &TreeOutput{ 646 Path: filepath.Join(flatDir.p, sm.Name), 647 SymlinkTarget: sm.Target, 648 NodeProperties: sm.NodeProperties, 649 } 650 flatFiles[out.Path] = out 651 } 652 653 // Add subdirectories to the queue 654 for _, subdir := range dir.Directories { 655 digest := digest.NewFromProtoUnvalidated(subdir.Digest) 656 name := filepath.Join(flatDir.p, subdir.Name) 657 queue = append(queue, &queueElem{d: digest, p: name}) 658 } 659 } 660 return flatFiles, nil 661 } 662 663 func packageDirectories(t *treeNode) (root *repb.Directory, files map[digest.Digest]*uploadinfo.Entry, treePb *repb.Tree, err error) { 664 root = &repb.Directory{} 665 files = make(map[digest.Digest]*uploadinfo.Entry) 666 childDirs := make([]string, 0, len(t.children)) 667 treePb = &repb.Tree{} 668 669 for name := range t.children { 670 childDirs = append(childDirs, name) 671 } 672 sort.Strings(childDirs) 673 674 for _, name := range childDirs { 675 child := t.children[name] 676 chRoot, childFiles, chTree, err := packageDirectories(child) 677 if err != nil { 678 return nil, nil, nil, err 679 } 680 ue, err := uploadinfo.EntryFromProto(chRoot) 681 if err != nil { 682 return nil, nil, nil, err 683 } 684 dg := ue.Digest 685 root.Directories = append(root.Directories, &repb.DirectoryNode{Name: name, Digest: dg.ToProto()}) 686 for d, b := range childFiles { 687 files[d] = b 688 } 689 treePb.Children = append(treePb.Children, chRoot) 690 treePb.Children = append(treePb.Children, chTree.Children...) 691 } 692 sort.Slice(root.Directories, func(i, j int) bool { return root.Directories[i].Name < root.Directories[j].Name }) 693 694 for name, n := range t.leaves { 695 // A node can have exactly one of file/symlink/emptyDirectoryMarker. 696 if n.file != nil { 697 dg := n.file.ue.Digest 698 root.Files = append(root.Files, &repb.FileNode{Name: name, Digest: dg.ToProto(), IsExecutable: n.file.isExecutable, NodeProperties: command.NodePropertiesToAPI(n.nodeProperties)}) 699 files[dg] = n.file.ue 700 continue 701 } 702 if n.symlink != nil { 703 root.Symlinks = append(root.Symlinks, &repb.SymlinkNode{Name: name, Target: n.symlink.target, NodeProperties: command.NodePropertiesToAPI(n.nodeProperties)}) 704 } 705 } 706 sort.Slice(root.Files, func(i, j int) bool { return root.Files[i].Name < root.Files[j].Name }) 707 sort.Slice(root.Symlinks, func(i, j int) bool { return root.Symlinks[i].Name < root.Symlinks[j].Name }) 708 709 return root, files, treePb, nil 710 } 711 712 // ComputeOutputsToUpload transforms the provided local output paths into uploadable Chunkers. 713 // The paths have to be relative to execRoot. 714 // It also populates the remote ActionResult, packaging output directories as trees where required. 715 func (c *Client) ComputeOutputsToUpload(execRoot, workingDir string, paths []string, cache filemetadata.Cache, sb command.SymlinkBehaviorType, nodeProperties map[string]*cpb.NodeProperties) (map[digest.Digest]*uploadinfo.Entry, *repb.ActionResult, error) { 716 outs := make(map[digest.Digest]*uploadinfo.Entry) 717 resPb := &repb.ActionResult{} 718 for _, path := range paths { 719 absPath := filepath.Join(execRoot, workingDir, path) 720 if _, err := getRelPath(execRoot, absPath); err != nil { 721 return nil, nil, err 722 } 723 meta := cache.Get(absPath) 724 if meta.Err != nil { 725 if e, ok := meta.Err.(*filemetadata.FileError); ok && e.IsNotFound { 726 continue // Ignore missing outputs. 727 } 728 if shouldIgnoreErr(meta.Err) { 729 continue 730 } 731 return nil, nil, meta.Err 732 } 733 normPath, err := filepath.Rel(filepath.Join(execRoot, workingDir), absPath) 734 if err != nil { 735 return nil, nil, err 736 } 737 if !meta.IsDirectory { 738 // A regular file. 739 ue := uploadinfo.EntryFromFile(meta.Digest, absPath) 740 outs[meta.Digest] = ue 741 resPb.OutputFiles = append(resPb.OutputFiles, &repb.OutputFile{Path: normPath, Digest: meta.Digest.ToProto(), IsExecutable: meta.IsExecutable, NodeProperties: command.NodePropertiesToAPI(nodeProperties[normPath])}) 742 continue 743 } 744 // A directory. 745 fs := make(map[string]*fileSysNode) 746 if e := loadFiles(absPath, "", "", nil, []string{"."}, fs, cache, treeSymlinkOpts(c.TreeSymlinkOpts, sb), nodeProperties); e != nil { 747 return nil, nil, e 748 } 749 ft, err := buildTree(fs) 750 if err != nil { 751 return nil, nil, err 752 } 753 754 rootDir, files, treePb, err := packageDirectories(ft) 755 if err != nil { 756 return nil, nil, err 757 } 758 ue, err := uploadinfo.EntryFromProto(rootDir) 759 if err != nil { 760 return nil, nil, err 761 } 762 outs[ue.Digest] = ue 763 treePb.Root = rootDir 764 ue, err = uploadinfo.EntryFromProto(treePb) 765 if err != nil { 766 return nil, nil, err 767 } 768 outs[ue.Digest] = ue 769 for _, ue := range files { 770 outs[ue.Digest] = ue 771 } 772 resPb.OutputDirectories = append(resPb.OutputDirectories, &repb.OutputDirectory{Path: normPath, TreeDigest: ue.Digest.ToProto()}) 773 // Upload the child directories individually as well 774 ueRoot, _ := uploadinfo.EntryFromProto(treePb.Root) 775 outs[ueRoot.Digest] = ueRoot 776 for _, child := range treePb.Children { 777 ueChild, _ := uploadinfo.EntryFromProto(child) 778 outs[ueChild.Digest] = ueChild 779 } 780 } 781 return outs, resPb, nil 782 }