blob: 4e67c2d96981359217c8fc91aa7925c68183d860 [file] [log] [blame]
Serge Bazanskif369cfa2020-05-22 18:36:42 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package main
18
19import (
20 "fmt"
21)
22
23// The Planner provides the main DSL and high-level control logic for resolving dependencies. It is the main API that
24// fietsje users should consume.
25
26// planner is a builder for a single world of Go package dependencies, and what is then emitted into a Starlark file
27// containing gazelle go_repository rules.
28// The planner's builder system covers three increasingly specific contextx:
29// - planner (this structure, allows for 'collecting' in high-level dependencies. ie. collections)
30// - collection (represents what has been pulled in by a high-level dependency, and allows for 'using' transitive
31// dependencies from a collection)
32// - optionized (represents a collection with extra build flags, eg. disabled proto builds)
33type planner struct {
34 // available is a map of importpaths to dependencies that the planner knows. This is a flat structure that is the
35 // main source of truth of actual dependency data, like a registry of everything that the planner knows about.
36 // The available dependency for a given importpath, as the planner progresses, might change, ie. when there is a
37 // version conflict. As such, code should use importpaths as atoms describing dependencies, instead of holding
38 // dependency pointers.
39 available map[string]*dependency
40 // enabled is a map of dependencies that will be emitted by the planner into the build via Gazelle.
41 enabled map[string]bool
42 // seen is a map of 'dependency' -> 'parent' importpaths, ie. returns what higher-level dependency (ie. one enabled
43 // with .collect()) pulled in a given dependency. This is only used for error messages to help the user find what
44 // a transitive dependency has been pulled in by.
45 seen map[string]string
46
47 shelf *shelf
48}
49
50func (p *planner) collect(importpath, version string, opts ...buildOpt) *collection {
51 return p.collectInternal(importpath, version, false, opts...)
52}
53
54func (p *planner) collectOverride(importpath, version string, opts ...buildOpt) *collection {
55 return p.collectInternal(importpath, version, true, opts...)
56}
57
58// collectInternal pulls in a high-level dependency into the planner and
59// enables it. It also parses all of its transitive // dependencies (not just
60// directly transitive, but recursively transitive) and makes the planner aware
61// of them. It does not enable these transitive dependencies, but returns a
62// collection builder, which can be used to do se by calling .use().
63func (p *planner) collectInternal(importpath, version string, override bool, opts ...buildOpt) *collection {
64 // Ensure overrides are explicit and minimal.
65 by, ok := p.seen[importpath]
66 if ok && !override {
67 panic(fmt.Errorf("%s is being collected, but has already been declared by %s; replace it by a use(%q) call on %s or use collectOverride", importpath, by, importpath, by))
68 }
69 if !ok && override {
70 panic(fmt.Errorf("%s is being collected with override, but has not been seen as a dependency previously - use .collect(%q, %q) instead", importpath, importpath, version))
71 }
72
73 d := &dependency{
74 shelf: p.shelf,
75 importpath: importpath,
76 version: version,
77 }
78 for _, o := range opts {
79 o(d)
80 }
81
82 // automatically enable direct import
83 p.enabled[d.importpath] = true
84 p.available[d.importpath] = d
85
86 td, err := d.getTransitiveDeps()
87 if err != nil {
88 panic(fmt.Errorf("could not get transitive deps for %q: %v", d.importpath, err))
89 }
90 // add transitive deps to 'available' map
91 for k, v := range td {
92 // skip dependencies that have already been enabled, dependencies are 'first enabled version wins'.
93 if _, ok := p.available[k]; ok && p.enabled[k] {
94 continue
95 }
96
97 p.available[k] = v
98
99 // make note of the high-level dependency that pulled in the dependency.
100 p.seen[v.importpath] = d.importpath
101 }
102
103 return &collection{
104 p: p,
105 highlevel: d,
106 transitive: td,
107 }
108}
109
110// collection represents the context of the planner after pulling/collecting in a high-level dependency. In this state,
111// the planner can be used to enable transitive dependencies of the high-level dependency.
112type collection struct {
113 p *planner
114
115 highlevel *dependency
116 transitive map[string]*dependency
117}
118
119// use enables given dependencies defined in the collection by a high-level dependency.
120func (c *collection) use(paths ...string) *collection {
121 return c.with().use(paths...)
122}
123
124// inject adds a dependency to a collection as if requested by the high-level dependency of the collection. This should
125// be used sparingly, for instance when high-level dependencies contain bazel code that uses some external workspaces
126// from Go modules, and those workspaces are not defined in parsed transitive dependency definitions like go.mod/sum.
127func (c *collection) inject(importpath, version string) *collection {
128 d := c.highlevel.child(importpath, version)
129 c.transitive[importpath] = d
130 c.p.available[importpath] = d
131 c.p.enabled[importpath] = true
132
133 return c
134}
135
136// with transforms a collection into an optionized, by setting some build options.
137func (c *collection) with(o ...buildOpt) *optionized {
138 return &optionized{
139 c: c,
140 opts: o,
141 }
142}
143
144// optionized is a collection that has some build options set, that will be applied to all dependencies 'used' in this
145// context
146type optionized struct {
147 c *collection
148 opts []buildOpt
149}
150
151// buildOpt is a build option passed to Gazelle.
152type buildOpt func(d *dependency)
153
154// buildTags sets the given buildTags in affected dependencies.
155func buildTags(tags ...string) buildOpt {
156 return func(d *dependency) {
157 d.buildTags = tags
158 }
159}
160
161// disabledProtoBuild disables protobuf builds in affected dependencies.
162func disabledProtoBuild(d *dependency) {
163 d.disableProtoBuild = true
164}
165
166// patches applies patches in affected dependencies after BUILD file generation.
167func patches(patches ...string) buildOpt {
168 return func(d *dependency) {
169 d.patches = patches
170 }
171}
172
173// use enables given dependencies defined in the collection by a high-level dependency, with any set build options.
174// After returning, the builder degrades to a collection - ie, all build options are reset.
175func (o *optionized) use(paths ...string) *collection {
176 for _, path := range paths {
177 el, ok := o.c.transitive[path]
178 if !ok {
179 msg := fmt.Sprintf("dependency %q not found in %q", path, o.c.highlevel.importpath)
180 if alternative, ok := o.c.p.seen[path]; ok {
181 msg += fmt.Sprintf(" (but found in %q)", alternative)
182 } else {
183 msg += " or any other collected library"
184 }
185 panic(msg)
186 }
187 for _, o := range o.opts {
188 o(el)
189 }
190 o.c.p.enabled[path] = true
191 }
192
193 return o.c
194}