1// Copyright 2023 The Bazel Authors. All rights reserved. 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 15package manifest 16 17import ( 18 "crypto/sha256" 19 "fmt" 20 "io" 21 "os" 22 23 "github.com/emirpasic/gods/sets/treeset" 24 25 yaml "gopkg.in/yaml.v2" 26) 27 28// File represents the gazelle_python.yaml file. 29type File struct { 30 Manifest *Manifest `yaml:"manifest,omitempty"` 31 // Integrity is the hash of the requirements.txt file and the Manifest for 32 // ensuring the integrity of the entire gazelle_python.yaml file. This 33 // controls the testing to keep the gazelle_python.yaml file up-to-date. 34 Integrity string `yaml:"integrity"` 35} 36 37// NewFile creates a new File with a given Manifest. 38func NewFile(manifest *Manifest) *File { 39 return &File{Manifest: manifest} 40} 41 42// Encode encodes the manifest file to the given writer. 43func (f *File) Encode(w io.Writer, manifestGeneratorHashFile, requirements io.Reader) error { 44 integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) 45 if err != nil { 46 return fmt.Errorf("failed to encode manifest file: %w", err) 47 } 48 f.Integrity = fmt.Sprintf("%x", integrityBytes) 49 encoder := yaml.NewEncoder(w) 50 defer encoder.Close() 51 if err := encoder.Encode(f); err != nil { 52 return fmt.Errorf("failed to encode manifest file: %w", err) 53 } 54 return nil 55} 56 57// VerifyIntegrity verifies if the integrity set in the File is valid. 58func (f *File) VerifyIntegrity(manifestGeneratorHashFile, requirements io.Reader) (bool, error) { 59 integrityBytes, err := f.calculateIntegrity(manifestGeneratorHashFile, requirements) 60 if err != nil { 61 return false, fmt.Errorf("failed to verify integrity: %w", err) 62 } 63 valid := (f.Integrity == fmt.Sprintf("%x", integrityBytes)) 64 return valid, nil 65} 66 67// calculateIntegrity calculates the integrity of the manifest file based on the 68// provided checksum for the requirements.txt file used as input to the modules 69// mapping, plus the manifest structure in the manifest file. This integrity 70// calculation ensures the manifest files are kept up-to-date. 71func (f *File) calculateIntegrity( 72 manifestGeneratorHash, requirements io.Reader, 73) ([]byte, error) { 74 hash := sha256.New() 75 76 // Sum the manifest part of the file. 77 encoder := yaml.NewEncoder(hash) 78 defer encoder.Close() 79 if err := encoder.Encode(f.Manifest); err != nil { 80 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 81 } 82 83 // Sum the manifest generator checksum bytes. 84 if _, err := io.Copy(hash, manifestGeneratorHash); err != nil { 85 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 86 } 87 88 // Sum the requirements.txt checksum bytes. 89 if _, err := io.Copy(hash, requirements); err != nil { 90 return nil, fmt.Errorf("failed to calculate integrity: %w", err) 91 } 92 93 return hash.Sum(nil), nil 94} 95 96// Decode decodes the manifest file from the given path. 97func (f *File) Decode(manifestPath string) error { 98 file, err := os.Open(manifestPath) 99 if err != nil { 100 return fmt.Errorf("failed to decode manifest file: %w", err) 101 } 102 defer file.Close() 103 104 decoder := yaml.NewDecoder(file) 105 if err := decoder.Decode(f); err != nil { 106 return fmt.Errorf("failed to decode manifest file: %w", err) 107 } 108 109 return nil 110} 111 112// ModulesMapping is the type used to map from importable Python modules to 113// the wheel names that provide these modules. 114type ModulesMapping map[string]string 115 116// MarshalYAML makes sure that we sort the module names before marshaling 117// the contents of `ModulesMapping` to a YAML file. This ensures that the 118// file is deterministically generated from the map. 119func (m ModulesMapping) MarshalYAML() (interface{}, error) { 120 var mapslice yaml.MapSlice 121 keySet := treeset.NewWithStringComparator() 122 for key := range m { 123 keySet.Add(key) 124 } 125 for _, key := range keySet.Values() { 126 mapslice = append(mapslice, yaml.MapItem{Key: key, Value: m[key.(string)]}) 127 } 128 return mapslice, nil 129} 130 131// Manifest represents the structure of the Gazelle manifest file. 132type Manifest struct { 133 // ModulesMapping is the mapping from importable modules to which Python 134 // wheel name provides these modules. 135 ModulesMapping ModulesMapping `yaml:"modules_mapping"` 136 // PipDepsRepositoryName is the name of the pip_install repository target. 137 // DEPRECATED 138 PipDepsRepositoryName string `yaml:"pip_deps_repository_name,omitempty"` 139 // PipRepository contains the information for pip_install or pip_repository 140 // target. 141 PipRepository *PipRepository `yaml:"pip_repository,omitempty"` 142} 143 144type PipRepository struct { 145 // The name of the pip_install or pip_repository target. 146 Name string 147 // UsePipRepositoryAliases allows to use aliases generated pip_repository 148 // when passing incompatible_generate_aliases = True. 149 UsePipRepositoryAliases bool `yaml:"use_pip_repository_aliases,omitempty"` 150} 151