github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/build/path_mapping.go (about)

     1  package build
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/tilt-dev/tilt/internal/ospath"
    15  	"github.com/tilt-dev/tilt/pkg/model"
    16  )
    17  
    18  // PathMapping represents a mapping from the local path to the tarball path
    19  //
    20  // To send a local file into a container, we copy it into a tarball, send the
    21  // tarball to docker, and then run a sequence of steps to unpack the tarball in
    22  // the container file system.
    23  //
    24  // That means every file has 3 paths:
    25  // 1) LocalPath
    26  // 2) TarballPath
    27  // 3) ContainerPath
    28  //
    29  // In incremental builds, TarballPath and ContainerPath are always the
    30  // same, so it was correct to use TarballPath and ContainerPath interchangeably.
    31  //
    32  // In DockerBuilds, this is no longer the case.
    33  //
    34  // TODO(nick): Do a pass on renaming all the path types
    35  type PathMapping struct {
    36  	LocalPath     string
    37  	ContainerPath string
    38  }
    39  
    40  func (m PathMapping) PrettyStr() string {
    41  	return fmt.Sprintf("'%s' --> '%s'", m.LocalPath, m.ContainerPath)
    42  }
    43  
    44  func (m PathMapping) Filter(matcher model.PathMatcher) ([]PathMapping, error) {
    45  	result := make([]PathMapping, 0)
    46  	err := filepath.WalkDir(m.LocalPath, func(currentLocal string, _ fs.DirEntry, err error) error {
    47  		if err != nil {
    48  			return err
    49  		}
    50  
    51  		match, err := matcher.Matches(currentLocal)
    52  		if err != nil {
    53  			return err
    54  		}
    55  
    56  		if !match {
    57  			return nil
    58  		}
    59  
    60  		rpLocal, err := filepath.Rel(m.LocalPath, currentLocal)
    61  		if err != nil {
    62  			return err
    63  		}
    64  
    65  		result = append(result, PathMapping{
    66  			LocalPath:     currentLocal,
    67  			ContainerPath: path.Join(m.ContainerPath, filepath.ToSlash(rpLocal)),
    68  		})
    69  		return nil
    70  	})
    71  
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	return result, nil
    76  }
    77  
    78  func FilterMappings(mappings []PathMapping, matcher model.PathMatcher) ([]PathMapping, error) {
    79  	result := make([]PathMapping, 0)
    80  	for _, mapping := range mappings {
    81  		filtered, err := mapping.Filter(matcher)
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  
    86  		result = append(result, filtered...)
    87  	}
    88  	return result, nil
    89  }
    90  
    91  // FilesToPathMappings converts a list of absolute local filepaths into pathMappings (i.e.
    92  // associates local filepaths with their syncs and destination paths), returning those
    93  // that it cannot associate with a sync.
    94  func FilesToPathMappings(files []string, syncs []model.Sync) ([]PathMapping, []string, error) {
    95  	pms := make([]PathMapping, 0, len(files))
    96  	pathsMatchingNoSync := []string{}
    97  	for _, f := range files {
    98  		pm, couldMap, err := fileToPathMapping(f, syncs)
    99  		if err != nil {
   100  			return nil, nil, err
   101  		}
   102  
   103  		if couldMap {
   104  			pms = append(pms, pm)
   105  		} else {
   106  			pathsMatchingNoSync = append(pathsMatchingNoSync, f)
   107  		}
   108  	}
   109  
   110  	return pms, pathsMatchingNoSync, nil
   111  }
   112  
   113  func fileToPathMapping(file string, sync []model.Sync) (pm PathMapping, couldMap bool, err error) {
   114  	for _, s := range sync {
   115  		// Open Q: can you sync files inside of syncs?! o_0
   116  		// TODO(maia): are symlinks etc. gonna kick our asses here? If so, will
   117  		// need ospath.RealChild -- but then can't deal with deleted local files.
   118  		relPath, isChild := ospath.Child(s.LocalPath, file)
   119  		if isChild {
   120  			localPathIsFile, err := isFile(s.LocalPath)
   121  			if err != nil {
   122  				return PathMapping{}, false, fmt.Errorf("error stat'ing: %v", err)
   123  			}
   124  			var containerPath string
   125  			if endsWithUnixSeparator(s.ContainerPath) && localPathIsFile {
   126  				fileName := filepath.Base(s.LocalPath)
   127  				containerPath = path.Join(s.ContainerPath, fileName)
   128  			} else {
   129  				containerPath = path.Join(s.ContainerPath, filepath.ToSlash(relPath))
   130  			}
   131  			return PathMapping{
   132  				LocalPath:     file,
   133  				ContainerPath: containerPath,
   134  			}, true, nil
   135  		}
   136  	}
   137  	// The file doesn't match any sync src's.
   138  	return PathMapping{}, false, nil
   139  }
   140  
   141  func endsWithUnixSeparator(path string) bool {
   142  	return strings.HasSuffix(path, "/")
   143  }
   144  
   145  func isFile(path string) (bool, error) {
   146  	fi, err := os.Stat(path)
   147  	if err != nil {
   148  		return false, err
   149  	}
   150  	mode := fi.Mode()
   151  	return !mode.IsDir(), nil
   152  }
   153  
   154  func SyncsToPathMappings(syncs []model.Sync) []PathMapping {
   155  	pms := make([]PathMapping, len(syncs))
   156  	for i, s := range syncs {
   157  		pms[i] = PathMapping{
   158  			LocalPath:     s.LocalPath,
   159  			ContainerPath: s.ContainerPath,
   160  		}
   161  	}
   162  	return pms
   163  }
   164  
   165  // Return all the path mappings for local paths that do not exist.
   166  func MissingLocalPaths(ctx context.Context, mappings []PathMapping) (missing, rest []PathMapping, err error) {
   167  	for _, mapping := range mappings {
   168  		_, err := os.Stat(mapping.LocalPath)
   169  		if err == nil {
   170  			rest = append(rest, mapping)
   171  			continue
   172  		}
   173  
   174  		if os.IsNotExist(err) {
   175  			missing = append(missing, mapping)
   176  		} else {
   177  			return nil, nil, errors.Wrap(err, "MissingLocalPaths")
   178  		}
   179  	}
   180  	return missing, rest, nil
   181  }
   182  
   183  func PathMappingsToContainerPaths(mappings []PathMapping) []string {
   184  	res := make([]string, len(mappings))
   185  	for i, m := range mappings {
   186  		res[i] = m.ContainerPath
   187  	}
   188  	return res
   189  }
   190  
   191  func PathMappingsToLocalPaths(mappings []PathMapping) []string {
   192  	res := make([]string, len(mappings))
   193  	for i, m := range mappings {
   194  		res[i] = m.LocalPath
   195  	}
   196  	return res
   197  }