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 }