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 }