blob: 9e4889c9dc9f1d53b16d6dae3122040de60eb6ec [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
Serge Bazanski4b1e37c2021-09-28 12:49:15 +020017package fietsje
Serge Bazanskif369cfa2020-05-22 18:36:42 +020018
19import (
20 "fmt"
21)
22
Serge Bazanski216fe7b2021-05-21 18:36:16 +020023// The Planner provides the main DSL and high-level control logic for resolving
24// dependencies. It is the main API that fietsje users should consume.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020025
Serge Bazanski216fe7b2021-05-21 18:36:16 +020026// planner is a builder for a single world of Go package dependencies, and what is
27// then emitted into a Starlark file containing gazelle go_repository rules. The
Serge Bazanski4b1e37c2021-09-28 12:49:15 +020028// planner's builder system covers three increasingly specific contexts:
Serge Bazanskif369cfa2020-05-22 18:36:42 +020029// - 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 {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020034 // available is a map of importpaths to dependencies that the planner knows. This
35 // is a flat structure that is the main source of truth of actual dependency data,
36 // like a registry of everything that the planner knows about. The available
37 // dependency for a given importpath, as the planner progresses, might change, ie.
38 // when there is a version conflict. As such, code should use importpaths as atoms
39 // describing dependencies, instead of holding dependency pointers.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020040 available map[string]*dependency
Serge Bazanski216fe7b2021-05-21 18:36:16 +020041 // enabled is a map of dependencies that will be emitted by the planner into the
42 // build via Gazelle.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020043 enabled map[string]bool
Serge Bazanski216fe7b2021-05-21 18:36:16 +020044 // seen is a map of 'dependency' -> 'parent' importpaths, ie. returns what higher-
45 // level dependency (ie. one enabled with .collect()) pulled in a given dependency.
46 // This is only used for error messages to help the user find what a transitive
47 // dependency has been pulled in by.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020048 seen map[string]string
49
50 shelf *shelf
51}
52
53func (p *planner) collect(importpath, version string, opts ...buildOpt) *collection {
54 return p.collectInternal(importpath, version, false, opts...)
55}
56
57func (p *planner) collectOverride(importpath, version string, opts ...buildOpt) *collection {
58 return p.collectInternal(importpath, version, true, opts...)
59}
60
61// collectInternal pulls in a high-level dependency into the planner and
62// enables it. It also parses all of its transitive // dependencies (not just
63// directly transitive, but recursively transitive) and makes the planner aware
64// of them. It does not enable these transitive dependencies, but returns a
65// collection builder, which can be used to do se by calling .use().
66func (p *planner) collectInternal(importpath, version string, override bool, opts ...buildOpt) *collection {
67 // Ensure overrides are explicit and minimal.
68 by, ok := p.seen[importpath]
69 if ok && !override {
70 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))
71 }
72 if !ok && override {
73 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))
74 }
75
76 d := &dependency{
77 shelf: p.shelf,
78 importpath: importpath,
79 version: version,
80 }
81 for _, o := range opts {
82 o(d)
83 }
84
85 // automatically enable direct import
86 p.enabled[d.importpath] = true
87 p.available[d.importpath] = d
88
89 td, err := d.getTransitiveDeps()
90 if err != nil {
91 panic(fmt.Errorf("could not get transitive deps for %q: %v", d.importpath, err))
92 }
93 // add transitive deps to 'available' map
94 for k, v := range td {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020095 // skip dependencies that have already been enabled, dependencies are 'first
96 // enabled version wins'.
Serge Bazanskif369cfa2020-05-22 18:36:42 +020097 if _, ok := p.available[k]; ok && p.enabled[k] {
98 continue
99 }
100
101 p.available[k] = v
102
103 // make note of the high-level dependency that pulled in the dependency.
104 p.seen[v.importpath] = d.importpath
105 }
106
107 return &collection{
108 p: p,
109 highlevel: d,
110 transitive: td,
111 }
112}
113
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200114// collection represents the context of the planner after pulling/collecting in a
115// high-level dependency. In this state, the planner can be used to enable
116// transitive dependencies of the high-level dependency.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200117type collection struct {
118 p *planner
119
120 highlevel *dependency
121 transitive map[string]*dependency
122}
123
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200124// use enables given dependencies defined in the collection by a high-level
125// dependency.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200126func (c *collection) use(paths ...string) *collection {
127 return c.with().use(paths...)
128}
129
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200130// replace injects a new dependency with a replacement importpath. This is used to
131// reflect 'replace' stanzas in go.mod files of third-party dependencies. This is
132// not done automatically by Fietsje, as a replacement is global to the entire
133// build tree, and should be done knowingly and explicitly by configuration. The
134// 'oldpath' importpath will be visible to the build system, but will be backed at
135// 'newpath' locked at 'version'.
Serge Bazanski14cf7502020-05-28 14:29:56 +0200136func (c *collection) replace(oldpath, newpath, version string) *collection {
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200137 // Ensure oldpath is in use. We want as little replacements as possible, and if
138 // it's not being used by anything, it means that we likely don't need it.
Serge Bazanski14cf7502020-05-28 14:29:56 +0200139 c.use(oldpath)
140
141 d := c.highlevel.child(oldpath, version)
142 d.replace = newpath
143 c.transitive[oldpath] = d
144 c.p.available[oldpath] = d
145 c.p.enabled[oldpath] = true
146
147 return c
148}
149
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200150// inject adds a dependency to a collection as if requested by the high-level
151// dependency of the collection. This should be used sparingly, for instance when
152// high-level dependencies contain bazel code that uses some external workspaces
153// from Go modules, and those workspaces are not defined in parsed transitive
154// dependency definitions like go.mod/sum.
Lorenz Brun2073ce32021-02-03 18:52:59 +0100155func (c *collection) inject(importpath, version string, opts ...buildOpt) *collection {
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200156 d := c.highlevel.child(importpath, version)
157 c.transitive[importpath] = d
158 c.p.available[importpath] = d
159 c.p.enabled[importpath] = true
Lorenz Brun2073ce32021-02-03 18:52:59 +0100160 for _, o := range opts {
161 o(c.transitive[importpath])
162 }
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200163
164 return c
165}
166
167// with transforms a collection into an optionized, by setting some build options.
168func (c *collection) with(o ...buildOpt) *optionized {
169 return &optionized{
170 c: c,
171 opts: o,
172 }
173}
174
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200175// optionized is a collection that has some build options set, that will be applied
176// to all dependencies 'used' in this context
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200177type optionized struct {
178 c *collection
179 opts []buildOpt
180}
181
182// buildOpt is a build option passed to Gazelle.
183type buildOpt func(d *dependency)
184
185// buildTags sets the given buildTags in affected dependencies.
186func buildTags(tags ...string) buildOpt {
187 return func(d *dependency) {
188 d.buildTags = tags
189 }
190}
191
192// disabledProtoBuild disables protobuf builds in affected dependencies.
193func disabledProtoBuild(d *dependency) {
194 d.disableProtoBuild = true
195}
196
197// patches applies patches in affected dependencies after BUILD file generation.
198func patches(patches ...string) buildOpt {
199 return func(d *dependency) {
200 d.patches = patches
201 }
202}
203
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200204// prePatches applies patches in affected dependencies before BUILD file
205// generation.
Lorenz Brunefb028f2020-07-28 17:04:49 +0200206func prePatches(patches ...string) buildOpt {
207 return func(d *dependency) {
208 d.prePatches = patches
209 }
210}
211
Lorenz Brunc2e3b1b2021-11-11 11:06:41 +0100212// useImportAliasNaming instructs Gazelle to name the generated targets in a way
213// which is both compatible with the old go_default_library convention as well
214// as the import convention. See `go_naming_convention=import_alias` in Gazelle.
215func useImportAliasNaming(d *dependency) {
216 d.useImportAliasNaming = true
217}
218
Serge Bazanski14cf7502020-05-28 14:29:56 +0200219func forceBazelGeneration(d *dependency) {
220 d.forceBazelGeneration = true
221}
222
223func buildExtraArgs(args ...string) buildOpt {
224 return func(d *dependency) {
225 d.buildExtraArgs = args
226 }
227}
228
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200229// use enables given dependencies defined in the collection by a high-level
230// dependency, with any set build options. After returning, the builder degrades to
231// a collection - ie, all build options are reset.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200232func (o *optionized) use(paths ...string) *collection {
233 for _, path := range paths {
234 el, ok := o.c.transitive[path]
235 if !ok {
236 msg := fmt.Sprintf("dependency %q not found in %q", path, o.c.highlevel.importpath)
237 if alternative, ok := o.c.p.seen[path]; ok {
238 msg += fmt.Sprintf(" (but found in %q)", alternative)
239 } else {
240 msg += " or any other collected library"
241 }
242 panic(msg)
243 }
244 for _, o := range o.opts {
245 o(el)
246 }
247 o.c.p.enabled[path] = true
248 }
249
250 return o.c
251}