blob: 21d651668f472540b318e47add2329b9dd96284a [file] [log] [blame]
// Copyright 2023 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package actions
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"google.golang.org/protobuf/proto"
"go.chromium.org/luci/common/system/environ"
"go.chromium.org/luci/cipkg/core"
)
// ActionFilesCopyTransformer is the default transformer for
// core.ActionFilesCopy.
func ActionFilesCopyTransformer(a *core.ActionFilesCopy, deps []Package) (*core.Derivation, error) {
drv, err := ReexecDerivation(a, false)
if err != nil {
return nil, err
}
// Clone the action so we can remove path when regenerating the hash.
// If version is set, we don't need path to determine whether content
// changed. Recalculate the hash based on the assumption.
m := proto.Clone(a).(*core.ActionFilesCopy)
for _, f := range m.Files {
if l := f.GetLocal(); f.GetVersion() != "" && fs.FileMode(f.Mode).Type() != fs.ModeSymlink {
l.Path = ""
}
}
if drv.FixedOutput, err = sha256String(m); err != nil {
return nil, err
}
for _, d := range deps {
name := d.Action.Name
outDir := d.Handler.OutputDirectory()
drv.Env = append(drv.Env, fmt.Sprintf("%s=%s", name, outDir))
}
return drv, nil
}
var defaultFilesCopyExecutor = newFilesCopyExecutor()
// RegisterEmbed regists the embedded fs with ref. It can be retrieved by copy
// actions using embed source. Embedded fs need to be registered in init() for
// re-exec executor.
func RegisterEmbed(ref string, e embed.FS) {
defaultFilesCopyExecutor.StoreEmbed(ref, e)
}
type fileInfo struct {
Mode fs.FileMode
WinAttrs uint32
}
func fileInfoFromSrc(src *core.ActionFilesCopy_Source) fileInfo {
return fileInfo{
Mode: fs.FileMode(src.Mode),
WinAttrs: src.WinAttrs,
}
}
func fileInfoFromFS(fi fs.FileInfo) fileInfo {
return fileInfo{
Mode: fi.Mode(),
}
}
// FilesCopyExecutor is the default executor for core.ActionFilesCopy.
// All embed.FS must be registered in init() so they are available when being
// executed from reexec.
type filesCopyExecutor struct {
sealed bool
embeds map[string]embed.FS
}
func newFilesCopyExecutor() *filesCopyExecutor {
return &filesCopyExecutor{
embeds: make(map[string]embed.FS),
}
}
// StoreEmbed stores an embedded fs for copy executor. This need to be called
// before main function otherwise executor may not be able to load the fs.
// Panic if the same ref is stored more than once.
func (f *filesCopyExecutor) StoreEmbed(ref string, e embed.FS) {
if f.sealed {
panic("all embedded fs must be stored before use")
}
if _, ok := f.embeds[ref]; ok {
panic(fmt.Sprintf("embedded fs with ref: \"%s\" registed twice", ref))
}
f.embeds[ref] = e
}
// LoadEmbed load a stored embedded fs.
func (f *filesCopyExecutor) LoadEmbed(ref string) (embed.FS, bool) {
f.sealed = true
e, ok := f.embeds[ref]
return e, ok
}
// Execute copies the files listed in the core.ActionFilesCopy to the out
// directory provided.
func (f *filesCopyExecutor) Execute(ctx context.Context, a *core.ActionFilesCopy, out string) error {
for dst, srcFile := range a.Files {
dst = filepath.Join(out, dst)
if err := os.MkdirAll(filepath.Dir(dst), fs.ModePerm); err != nil {
return fmt.Errorf("failed to create directory: %s: %w", path.Base(dst), err)
}
fi := fileInfoFromSrc(srcFile)
switch c := srcFile.Content.(type) {
case *core.ActionFilesCopy_Source_Raw:
if err := copyRaw(c.Raw, dst, fi); err != nil {
return err
}
case *core.ActionFilesCopy_Source_Local_:
if err := copyLocal(c.Local, dst, fi); err != nil {
return err
}
case *core.ActionFilesCopy_Source_Embed_:
if err := f.copyEmbed(c.Embed, dst, fi); err != nil {
return err
}
case *core.ActionFilesCopy_Source_Output_:
if err := copyOutput(c.Output, environ.FromCtx(ctx), dst, fi); err != nil {
return err
}
default:
return fmt.Errorf("unknown file type for %s: %s", dst, fi.Mode.Type())
}
}
return nil
}
func copyRaw(raw []byte, dst string, fi fileInfo) error {
switch fi.Mode.Type() {
case fs.ModeSymlink:
return fmt.Errorf("symlink is not supported for the source type")
case fs.ModeDir:
if err := os.MkdirAll(dst, fi.Mode); err != nil {
return fmt.Errorf("failed to create directory: %s: %w", path.Base(dst), err)
}
return nil
case 0: // Regular File
if err := createFile(dst, fi, bytes.NewReader(raw)); err != nil {
return fmt.Errorf("failed to create file: %s: %w", dst, err)
}
return nil
default:
return fmt.Errorf("unknown file type for %s: %s", dst, fi.Mode.Type())
}
}
func copyLocal(s *core.ActionFilesCopy_Source_Local, dst string, fi fileInfo) error {
src := s.Path
switch fi.Mode.Type() {
case fs.ModeSymlink:
if s.FollowSymlinks {
return fmt.Errorf("invalid file spec: followSymlinks can't be used with symlink dst: %s", dst)
}
if err := os.Symlink(src, dst); err != nil {
return fmt.Errorf("failed to create symlink: %s -> %s: %w", dst, src, err)
}
return nil
case fs.ModeDir:
return copyFS(os.DirFS(src), s.FollowSymlinks, dst)
case 0: // Regular File
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file for %s: %s: %w", dst, src, err)
}
defer f.Close()
if err := createFile(dst, fi, f); err != nil {
return fmt.Errorf("failed to create file for %s: %s: %w", dst, src, err)
}
return nil
default:
return fmt.Errorf("unknown file type for %s: %s", dst, fi.Mode.Type())
}
}
func (f *filesCopyExecutor) copyEmbed(s *core.ActionFilesCopy_Source_Embed, dst string, fi fileInfo) error {
e, ok := f.LoadEmbed(s.Ref)
if !ok {
return fmt.Errorf("failed to load embedded fs for %s: %s", dst, s)
}
switch fi.Mode.Type() {
case fs.ModeSymlink:
return fmt.Errorf("symlink not supported for the source type")
case fs.ModeDir:
path := s.Path
if path == "" {
path = "."
}
src, err := fs.Sub(e, path)
if err != nil {
return fmt.Errorf("failed to load subdir fs for %s: %s: %w", dst, s, err)
}
return copyFS(src, true, dst)
case 0: // Regular File
f, err := e.Open(s.Path)
if err != nil {
return fmt.Errorf("failed to open source file for %s: %s: %w", dst, s, err)
}
defer f.Close()
if err := createFile(dst, fi, f); err != nil {
return fmt.Errorf("failed to create file for %s: %s: %w", dst, s, err)
}
return nil
default:
return fmt.Errorf("unknown file type for %s: %s", dst, fi.Mode.Type())
}
}
func copyOutput(s *core.ActionFilesCopy_Source_Output, env environ.Env, dst string, fi fileInfo) error {
out := env.Get(s.Name)
if out == "" {
return fmt.Errorf("output not found: %s: %s", dst, s)
}
return copyLocal(&core.ActionFilesCopy_Source_Local{
Path: filepath.Join(out, s.Path),
FollowSymlinks: false,
}, dst, fi)
}
func copyFS(src fs.FS, followSymlinks bool, dst string) error {
return fs.WalkDir(src, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
log.Printf("copying %s\n", name)
dstName := filepath.Join(dst, filepath.FromSlash(name))
var cerr error
err = func() error {
switch {
case d.Type() == fs.ModeSymlink && !followSymlinks:
src, ok := src.(ReadLinkFS)
if !ok {
return fmt.Errorf("readlink not supported on the source filesystem: %s", name)
}
target, err := src.ReadLink(name)
if err != nil {
return fmt.Errorf("failed to readlink: %s: %w", name, err)
}
if filepath.IsAbs(target) {
return fmt.Errorf("absolute symlink target not supported: %s", name)
}
if err := os.Symlink(target, dstName); err != nil {
return fmt.Errorf("failed to create symlink: %s -> %s: %w", name, target, err)
}
case d.Type() == fs.ModeDir:
if err := os.MkdirAll(dstName, fs.ModePerm); err != nil {
return fmt.Errorf("failed to create dir: %s: %w", name, err)
}
return nil
default: // Regular File or following symlinks
srcFile, err := src.Open(name)
if err != nil {
return fmt.Errorf("failed to open src file: %s: %w", name, err)
}
defer func() { cerr = errors.Join(cerr, srcFile.Close()) }()
info, err := fs.Stat(src, name)
if err != nil {
return fmt.Errorf("failed to get file mode: %s: %w", name, err)
}
if err := createFile(dstName, fileInfoFromFS(info), srcFile); err != nil {
return fmt.Errorf("failed to create file: %s: %w", name, err)
}
}
return nil
}()
return errors.Join(err, cerr)
})
}