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  }