• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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