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  }