go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipkg/base/actions/copy.go (about) 1 // Copyright 2023 The LUCI Authors. 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 15 package actions 16 17 import ( 18 "bytes" 19 "context" 20 "embed" 21 "errors" 22 "fmt" 23 "io" 24 "io/fs" 25 "log" 26 "os" 27 "path" 28 "path/filepath" 29 30 "go.chromium.org/luci/cipkg/core" 31 "go.chromium.org/luci/common/system/environ" 32 33 "google.golang.org/protobuf/proto" 34 ) 35 36 // ActionFilesCopyTransformer is the default transformer for 37 // core.ActionFilesCopy. 38 func ActionFilesCopyTransformer(a *core.ActionFilesCopy, deps []Package) (*core.Derivation, error) { 39 drv, err := ReexecDerivation(a, false) 40 if err != nil { 41 return nil, err 42 } 43 44 // Clone the action so we can remove path when regenerating the hash. 45 // If version is set, we don't need path to determine whether content 46 // changed. Recalculate the hash based on the assumption. 47 m := proto.Clone(a).(*core.ActionFilesCopy) 48 for _, f := range m.Files { 49 if l := f.GetLocal(); l.GetVersion() != "" && fs.FileMode(f.Mode).Type() != fs.ModeSymlink { 50 l.Path = "" 51 } 52 } 53 if drv.FixedOutput, err = sha256String(m); err != nil { 54 return nil, err 55 } 56 57 for _, d := range deps { 58 name := d.Action.Name 59 outDir := d.Handler.OutputDirectory() 60 drv.Env = append(drv.Env, fmt.Sprintf("%s=%s", name, outDir)) 61 } 62 return drv, nil 63 } 64 65 var defaultFilesCopyExecutor = newFilesCopyExecutor() 66 67 // RegisterEmbed regists the embedded fs with ref. It can be retrieved by copy 68 // actions using embed source. Embedded fs need to be registered in init() for 69 // re-exec executor. 70 func RegisterEmbed(ref string, e embed.FS) { 71 defaultFilesCopyExecutor.StoreEmbed(ref, e) 72 } 73 74 // FilesCopyExecutor is the default executor for core.ActionFilesCopy. 75 // All embed.FS must be registered in init() so they are available when being 76 // executed from reexec. 77 type filesCopyExecutor struct { 78 sealed bool 79 embeds map[string]embed.FS 80 } 81 82 func newFilesCopyExecutor() *filesCopyExecutor { 83 return &filesCopyExecutor{ 84 embeds: make(map[string]embed.FS), 85 } 86 } 87 88 // StoreEmbed stores an embedded fs for copy executor. This need to be called 89 // before main function otherwise executor may not be able to load the fs. 90 // Panic if the same ref is stored more than once. 91 func (f *filesCopyExecutor) StoreEmbed(ref string, e embed.FS) { 92 if f.sealed { 93 panic("all embedded fs must be stored before use") 94 } 95 if _, ok := f.embeds[ref]; ok { 96 panic(fmt.Sprintf("embedded fs with ref: \"%s\" registed twice", ref)) 97 } 98 f.embeds[ref] = e 99 } 100 101 // LoadEmbed load a stored embedded fs. 102 func (f *filesCopyExecutor) LoadEmbed(ref string) (embed.FS, bool) { 103 f.sealed = true 104 e, ok := f.embeds[ref] 105 return e, ok 106 } 107 108 // Execute copies the files listed in the core.ActionFilesCopy to the out 109 // directory provided. 110 func (f *filesCopyExecutor) Execute(ctx context.Context, a *core.ActionFilesCopy, out string) error { 111 for dst, srcFile := range a.Files { 112 dst = filepath.Join(out, dst) 113 if err := os.MkdirAll(filepath.Dir(dst), fs.ModePerm); err != nil { 114 return fmt.Errorf("failed to create directory: %s: %w", path.Base(dst), err) 115 } 116 m := fs.FileMode(srcFile.Mode) 117 118 switch c := srcFile.Content.(type) { 119 case *core.ActionFilesCopy_Source_Raw: 120 if err := copyRaw(c.Raw, dst, m); err != nil { 121 return err 122 } 123 case *core.ActionFilesCopy_Source_Local_: 124 if err := copyLocal(c.Local, dst, m); err != nil { 125 return err 126 } 127 case *core.ActionFilesCopy_Source_Embed_: 128 if err := f.copyEmbed(c.Embed, dst, m); err != nil { 129 return err 130 } 131 case *core.ActionFilesCopy_Source_Output_: 132 if err := copyOutput(c.Output, environ.FromCtx(ctx), dst, m); err != nil { 133 return err 134 } 135 default: 136 return fmt.Errorf("unknown file type for %s: %s", dst, m.Type()) 137 } 138 } 139 return nil 140 } 141 142 func copyRaw(raw []byte, dst string, m fs.FileMode) error { 143 switch m.Type() { 144 case fs.ModeSymlink: 145 return fmt.Errorf("symlink is not supported for the source type") 146 case fs.ModeDir: 147 if err := os.MkdirAll(dst, m); err != nil { 148 return fmt.Errorf("failed to create directory: %s: %w", path.Base(dst), err) 149 } 150 return nil 151 case 0: // Regular File 152 if err := createFile(dst, m, bytes.NewReader(raw)); err != nil { 153 return fmt.Errorf("failed to create file: %s: %w", dst, err) 154 } 155 return nil 156 default: 157 return fmt.Errorf("unknown file type for %s: %s", dst, m.Type()) 158 } 159 } 160 161 func copyLocal(s *core.ActionFilesCopy_Source_Local, dst string, m fs.FileMode) error { 162 src := s.Path 163 switch m.Type() { 164 case fs.ModeSymlink: 165 if s.FollowSymlinks { 166 return fmt.Errorf("invalid file spec: followSymlinks can't be used with symlink dst: %s", dst) 167 } 168 if err := os.Symlink(src, dst); err != nil { 169 return fmt.Errorf("failed to create symlink: %s -> %s: %w", dst, src, err) 170 } 171 return nil 172 case fs.ModeDir: 173 return copyFS(os.DirFS(src), s.FollowSymlinks, dst) 174 case 0: // Regular File 175 f, err := os.Open(src) 176 if err != nil { 177 return fmt.Errorf("failed to open source file for %s: %s: %w", dst, src, err) 178 } 179 defer f.Close() 180 if err := createFile(dst, m, f); err != nil { 181 return fmt.Errorf("failed to create file for %s: %s: %w", dst, src, err) 182 } 183 return nil 184 default: 185 return fmt.Errorf("unknown file type for %s: %s", dst, m.Type()) 186 } 187 } 188 189 func (f *filesCopyExecutor) copyEmbed(s *core.ActionFilesCopy_Source_Embed, dst string, m fs.FileMode) error { 190 e, ok := f.LoadEmbed(s.Ref) 191 if !ok { 192 return fmt.Errorf("failed to load embedded fs for %s: %s", dst, s) 193 } 194 195 switch m.Type() { 196 case fs.ModeSymlink: 197 return fmt.Errorf("symlink not supported for the source type") 198 case fs.ModeDir: 199 path := s.Path 200 if path == "" { 201 path = "." 202 } 203 src, err := fs.Sub(e, path) 204 if err != nil { 205 return fmt.Errorf("failed to load subdir fs for %s: %s: %w", dst, s, err) 206 } 207 return copyFS(src, true, dst) 208 case 0: // Regular File 209 f, err := e.Open(s.Path) 210 if err != nil { 211 return fmt.Errorf("failed to open source file for %s: %s: %w", dst, s, err) 212 } 213 defer f.Close() 214 if err := createFile(dst, m, f); err != nil { 215 return fmt.Errorf("failed to create file for %s: %s: %w", dst, s, err) 216 } 217 return nil 218 default: 219 return fmt.Errorf("unknown file type for %s: %s", dst, m.Type()) 220 } 221 } 222 223 func copyOutput(s *core.ActionFilesCopy_Source_Output, env environ.Env, dst string, m fs.FileMode) error { 224 out := env.Get(s.Name) 225 if out == "" { 226 return fmt.Errorf("output not found: %s: %s", dst, s) 227 } 228 return copyLocal(&core.ActionFilesCopy_Source_Local{ 229 Path: filepath.Join(out, s.Path), 230 FollowSymlinks: false, 231 }, dst, m) 232 } 233 234 func copyFS(src fs.FS, followSymlinks bool, dst string) error { 235 return fs.WalkDir(src, ".", func(name string, d fs.DirEntry, err error) error { 236 if err != nil { 237 return err 238 } 239 240 log.Printf("copying %s\n", name) 241 242 dstName := filepath.Join(dst, filepath.FromSlash(name)) 243 244 var cerr error 245 err = func() error { 246 switch { 247 case d.Type() == fs.ModeSymlink && !followSymlinks: 248 src, ok := src.(ReadLinkFS) 249 if !ok { 250 return fmt.Errorf("readlink not supported on the source filesystem: %s", name) 251 } 252 target, err := src.ReadLink(name) 253 if err != nil { 254 return fmt.Errorf("failed to readlink: %s: %w", name, err) 255 } 256 if filepath.IsAbs(target) { 257 return fmt.Errorf("absolute symlink target not supported: %s", name) 258 } 259 260 if err := os.Symlink(target, dstName); err != nil { 261 return fmt.Errorf("failed to create symlink: %s -> %s: %w", name, target, err) 262 } 263 case d.Type() == fs.ModeDir: 264 if err := os.MkdirAll(dstName, fs.ModePerm); err != nil { 265 return fmt.Errorf("failed to create dir: %s: %w", name, err) 266 } 267 return nil 268 default: // Regular File or following symlinks 269 srcFile, err := src.Open(name) 270 if err != nil { 271 return fmt.Errorf("failed to open src file: %s: %w", name, err) 272 } 273 defer func() { cerr = errors.Join(cerr, srcFile.Close()) }() 274 275 info, err := fs.Stat(src, name) 276 if err != nil { 277 return fmt.Errorf("failed to get file mode: %s: %w", name, err) 278 } 279 280 if err := createFile(dstName, info.Mode(), srcFile); err != nil { 281 return fmt.Errorf("failed to create file: %s: %w", name, err) 282 } 283 } 284 return nil 285 }() 286 287 return errors.Join(err, cerr) 288 }) 289 } 290 291 func createFile(dst string, m fs.FileMode, r io.Reader) error { 292 f, err := os.Create(dst) 293 if err != nil { 294 return err 295 } 296 defer f.Close() 297 if _, err := io.Copy(f, r); err != nil { 298 return err 299 } 300 if err := os.Chmod(dst, m); err != nil { 301 return err 302 } 303 return nil 304 }