go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/vpython/wheels/wheels.go (about) 1 // Copyright 2022 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 wheels includes implementation for installing wheels inside venv from 16 // vpython spec. 17 package wheels 18 19 import ( 20 "context" 21 "encoding/json" 22 "os" 23 "path/filepath" 24 "strings" 25 26 "google.golang.org/protobuf/proto" 27 "google.golang.org/protobuf/types/known/anypb" 28 29 "go.chromium.org/luci/cipd/client/cipd" 30 "go.chromium.org/luci/cipd/client/cipd/ensure" 31 "go.chromium.org/luci/cipd/client/cipd/template" 32 "go.chromium.org/luci/cipkg/base/actions" 33 "go.chromium.org/luci/cipkg/base/generators" 34 "go.chromium.org/luci/cipkg/core" 35 "go.chromium.org/luci/common/errors" 36 "go.chromium.org/luci/common/system/environ" 37 38 "go.chromium.org/luci/vpython/api/vpython" 39 "go.chromium.org/luci/vpython/spec" 40 ) 41 42 type vpythonSpecGenerator struct { 43 spec *vpython.Spec 44 pep425tags generators.Generator 45 } 46 47 func (g *vpythonSpecGenerator) Generate(ctx context.Context, plats generators.Platforms) (*core.Action, error) { 48 p, err := g.pep425tags.Generate(ctx, plats) 49 if err != nil { 50 return nil, err 51 } 52 s, err := anypb.New(g.spec) 53 if err != nil { 54 return nil, err 55 } 56 return &core.Action{ 57 Name: "wheels", 58 Deps: []*core.Action{p}, 59 Spec: &core.Action_Extension{Extension: s}, 60 }, nil 61 } 62 63 func FromSpec(spec *vpython.Spec, pep425tags generators.Generator) generators.Generator { 64 return &vpythonSpecGenerator{spec: spec, pep425tags: pep425tags} 65 } 66 67 func MustSetTransformer(cipdCacheDir string, ap *actions.ActionProcessor) { 68 v := &vpythonSpecTransformer{ 69 cipdCacheDir: cipdCacheDir, 70 } 71 actions.MustSetTransformer[*vpython.Spec](ap, v.Transform) 72 } 73 74 type vpythonSpecTransformer struct { 75 cipdCacheDir string 76 } 77 78 func (v *vpythonSpecTransformer) Transform(spec *vpython.Spec, deps []actions.Package) (*core.Derivation, error) { 79 drv, err := actions.ReexecDerivation(spec, true) 80 if err != nil { 81 return nil, err 82 } 83 env := environ.New(drv.Env) 84 env.Set(cipd.EnvCacheDir, v.cipdCacheDir) 85 for _, d := range deps { 86 drv.FixedOutput += "+" + d.DerivationID 87 env.Set(d.Action.Name, d.Handler.OutputDirectory()) 88 } 89 drv.Env = env.Sorted() 90 return drv, nil 91 } 92 93 func MustSetExecutor(reexec *actions.ReexecRegistry) { 94 actions.MustSetExecutor[*vpython.Spec](reexec, actionVPythonSpecExecutor) 95 } 96 97 func actionVPythonSpecExecutor(ctx context.Context, s *vpython.Spec, out string) error { 98 envs := environ.FromCtx(ctx) 99 100 // Parse tags file 101 var tags []*vpython.PEP425Tag 102 tagsDir := envs.Get("python_pep425tags") 103 raw, err := os.Open(filepath.Join(tagsDir, "pep425tags.json")) 104 if err != nil { 105 return err 106 } 107 defer raw.Close() 108 if err := json.NewDecoder(raw).Decode(&tags); err != nil { 109 return err 110 } 111 112 // Translates vpython spec into a CIPD ensure file. 113 ef, err := ensureFileFromVPythonSpec(s, tags) 114 if err != nil { 115 return err 116 } 117 var efs strings.Builder 118 if err := ef.Serialize(&efs); err != nil { 119 return err 120 } 121 122 // Execute cipd export 123 if err := actions.ActionCIPDExportExecutor(ctx, &core.ActionCIPDExport{ 124 EnsureFile: efs.String(), 125 Env: envs.Sorted(), 126 }, out); err != nil { 127 return err 128 } 129 130 // Generate requirements.txt 131 wheels := filepath.Join(out, "wheels") 132 ws, err := scanDir(wheels) 133 if err != nil { 134 return errors.Annotate(err, "failed to scan wheels").Err() 135 } 136 if err := writeRequirementsFile(filepath.Join(out, "requirements.txt"), ws); err != nil { 137 return errors.Annotate(err, "failed to write requirements.txt").Err() 138 } 139 140 return nil 141 } 142 143 func ensureFileFromVPythonSpec(s *vpython.Spec, tags []*vpython.PEP425Tag) (*ensure.File, error) { 144 s = proto.Clone(s).(*vpython.Spec) 145 146 // Remove unmatched wheels from spec 147 if err := spec.NormalizeSpec(s, tags); err != nil { 148 return nil, err 149 } 150 151 // Get vpython template from tags 152 expander := template.DefaultExpander() 153 if t := pep425TagSelector(tags); t != nil { 154 p := PlatformForPEP425Tag(t) 155 expander = p.Expander() 156 if err := addPEP425CIPDTemplateForTag(expander, t); err != nil { 157 return nil, err 158 } 159 } 160 161 // Construct cipd packages 162 names := make(map[string]struct{}) 163 pslice := make(ensure.PackageSlice, len(s.Wheel)) 164 for i, pkg := range s.Wheel { 165 name, err := expander.Expand(pkg.Name) 166 if err != nil { 167 if errors.Is(err, template.ErrSkipTemplate) { 168 continue 169 } 170 return nil, errors.Annotate(err, "expanding %v", pkg).Err() 171 } 172 if _, ok := names[name]; ok { 173 return nil, errors.Reason("duplicated package: %v", pkg).Err() 174 } 175 names[name] = struct{}{} 176 177 pslice[i] = ensure.PackageDef{ 178 PackageTemplate: name, 179 UnresolvedVersion: pkg.Version, 180 } 181 } 182 183 return &ensure.File{ 184 PackagesBySubdir: map[string]ensure.PackageSlice{"wheels": pslice}, 185 }, nil 186 }