go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipkg/base/generators/import.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 generators
    16  
    17  import (
    18  	"context"
    19  	"crypto"
    20  	"fmt"
    21  	"io/fs"
    22  	"os"
    23  	"os/exec"
    24  	"path"
    25  	"path/filepath"
    26  
    27  	"go.chromium.org/luci/cipkg/core"
    28  )
    29  
    30  type ImportTarget struct {
    31  	Source  string
    32  	Version string
    33  	Mode    fs.FileMode
    34  
    35  	FollowSymlinks bool
    36  
    37  	// Tf true, the import target will be considered different if source path
    38  	// changed. Otherwise only Version will be take into account.
    39  	SourcePathDependent bool
    40  }
    41  
    42  // ImportTargets is used to import file/directory from host environment. The
    43  // builder itself won't detect the change of the imported file/directory. A
    44  // version string should be generated to indicate the change if it matters.
    45  // By default, target will be symlinked. When Mode in target is set to anything
    46  // other than symlink, a hash version will be generated if there is no version
    47  // provided.
    48  type ImportTargets struct {
    49  	Name     string
    50  	Metadata *core.Action_Metadata
    51  	Targets  map[string]ImportTarget
    52  }
    53  
    54  func (i *ImportTargets) Generate(ctx context.Context, plats Platforms) (*core.Action, error) {
    55  	files := make(map[string]*core.ActionFilesCopy_Source)
    56  	for k, v := range i.Targets {
    57  		src := filepath.FromSlash(v.Source)
    58  		dst := filepath.FromSlash(k)
    59  		if !filepath.IsAbs(src) {
    60  			return nil, fmt.Errorf("import target source must be absolute path: %s", src)
    61  		}
    62  
    63  		m := getMode(v)
    64  
    65  		// Always generate a version if target is not a symlink and no version is
    66  		// provided. Otherwise we won't be able to track the change.
    67  		ver := v.Version
    68  		if m.Type() != fs.ModeSymlink && ver == "" {
    69  			h := crypto.SHA256.New()
    70  			switch m.Type() {
    71  			case fs.ModeDir:
    72  				if err := getHashFromFS(os.DirFS(src), h); err != nil {
    73  					return nil, fmt.Errorf("failed to generate hash from src: %s: %w", src, err)
    74  				}
    75  			default: // Regular File
    76  				f, err := os.Open(src)
    77  				if err != nil {
    78  					return nil, fmt.Errorf("failed to open src: %s: %w", src, err)
    79  				}
    80  				defer f.Close()
    81  				if err := getHashFromFile(src, f, h); err != nil {
    82  					return nil, fmt.Errorf("failed to generate hash from src: %s: %w", src, err)
    83  				}
    84  			}
    85  			ver = fmt.Sprintf("%x", h.Sum(nil))
    86  		}
    87  		// Append source path to version so the version in action changed if source
    88  		// path changed.
    89  		if ver != "" && v.SourcePathDependent {
    90  			ver = fmt.Sprintf("%s:%s", ver, v.Source)
    91  		}
    92  
    93  		// By default, create a symlink for the target.
    94  		files[dst] = &core.ActionFilesCopy_Source{
    95  			Content: &core.ActionFilesCopy_Source_Local_{
    96  				Local: &core.ActionFilesCopy_Source_Local{Path: src, Version: ver, FollowSymlinks: v.FollowSymlinks},
    97  			},
    98  			Mode: uint32(m),
    99  		}
   100  	}
   101  
   102  	// If any file is symlink, mark the output as imported to help e.g. docker
   103  	// avoid using its content.
   104  	for _, f := range files {
   105  		if fs.FileMode(f.Mode).Type() == fs.ModeSymlink {
   106  			files[filepath.Join("build-support", "base_import.stamp")] = &core.ActionFilesCopy_Source{
   107  				Content: &core.ActionFilesCopy_Source_Raw{},
   108  				Mode:    0o666,
   109  			}
   110  			break
   111  		}
   112  	}
   113  
   114  	return &core.Action{
   115  		Name:     i.Name,
   116  		Metadata: i.Metadata,
   117  		Spec: &core.Action_Copy{
   118  			Copy: &core.ActionFilesCopy{
   119  				Files: files,
   120  			},
   121  		},
   122  	}, nil
   123  }
   124  
   125  // 1. If any permission bit set, return mode as it is.
   126  // 2. If mode is empty, use ModeSymlink by default.
   127  // 3. Use 0o777 as default permission for directories.
   128  // 4. Use 0o666 as default permission for file.
   129  func getMode(i ImportTarget) fs.FileMode {
   130  	if i.Mode.Perm() != 0 || i.Mode.Type() == fs.ModeSymlink {
   131  		return i.Mode
   132  	}
   133  
   134  	m := i.Mode
   135  	if mt := i.Mode.Type(); mt.IsDir() {
   136  		m |= 0o777
   137  	} else if mt.IsRegular() {
   138  		m |= 0o666
   139  	}
   140  
   141  	return m
   142  }
   143  
   144  var importFromPathMap = make(map[string]struct {
   145  	target ImportTarget
   146  	err    error
   147  })
   148  
   149  // FindBinaryFunc returns a slash separated path for the provided binary name.
   150  type FindBinaryFunc func(bin string) (path string, err error)
   151  
   152  // LookPath looks up file in the PATH and returns a slash separated path if
   153  // the file exists.
   154  func lookPath(file string) (string, error) {
   155  	p, err := exec.LookPath(file)
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	return filepath.ToSlash(p), err
   160  }
   161  
   162  // FromPathBatch is a wrapper for builtins.Import generator. It finds binaries
   163  // using finder func and caches the result based on the name. if finder is nil,
   164  // binaries will be searched from the PATH environment.
   165  func FromPathBatch(name string, finder FindBinaryFunc, bins ...string) (*ImportTargets, error) {
   166  	if finder == nil {
   167  		finder = lookPath
   168  	}
   169  
   170  	i := &ImportTargets{
   171  		Name:    name,
   172  		Targets: make(map[string]ImportTarget),
   173  	}
   174  	for _, bin := range bins {
   175  		ret, ok := importFromPathMap[bin]
   176  		if !ok {
   177  			ret.target, ret.err = func() (ImportTarget, error) {
   178  				path, err := finder(bin)
   179  				if err != nil {
   180  					return ImportTarget{}, fmt.Errorf("failed to find binary: %s: %w", bin, err)
   181  				}
   182  				return ImportTarget{
   183  					Source: path,
   184  					Mode:   fs.ModeSymlink,
   185  				}, nil
   186  			}()
   187  
   188  			importFromPathMap[bin] = ret
   189  		}
   190  
   191  		if ret.err != nil {
   192  			return nil, ret.err
   193  		}
   194  		i.Targets[path.Join("bin", path.Base(ret.target.Source))] = ret.target
   195  	}
   196  	return i, nil
   197  }