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  }