Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 1 | // 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 | |
| 17 | package main |
| 18 | |
| 19 | import ( |
| 20 | "fmt" |
| 21 | ) |
| 22 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 23 | // 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 Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 25 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 26 | // 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 |
| 28 | // planner's builder system covers three increasingly specific contextx: |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 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) |
| 33 | type planner struct { |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 34 | // 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 Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 40 | available map[string]*dependency |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 41 | // enabled is a map of dependencies that will be emitted by the planner into the |
| 42 | // build via Gazelle. |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 43 | enabled map[string]bool |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 44 | // 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 Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 48 | seen map[string]string |
| 49 | |
| 50 | shelf *shelf |
| 51 | } |
| 52 | |
| 53 | func (p *planner) collect(importpath, version string, opts ...buildOpt) *collection { |
| 54 | return p.collectInternal(importpath, version, false, opts...) |
| 55 | } |
| 56 | |
| 57 | func (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(). |
| 66 | func (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 Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 95 | // skip dependencies that have already been enabled, dependencies are 'first |
| 96 | // enabled version wins'. |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 97 | 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 Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 114 | // 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 Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 117 | type collection struct { |
| 118 | p *planner |
| 119 | |
| 120 | highlevel *dependency |
| 121 | transitive map[string]*dependency |
| 122 | } |
| 123 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 124 | // use enables given dependencies defined in the collection by a high-level |
| 125 | // dependency. |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 126 | func (c *collection) use(paths ...string) *collection { |
| 127 | return c.with().use(paths...) |
| 128 | } |
| 129 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 130 | // 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 Bazanski | 14cf750 | 2020-05-28 14:29:56 +0200 | [diff] [blame] | 136 | func (c *collection) replace(oldpath, newpath, version string) *collection { |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 137 | // 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 Bazanski | 14cf750 | 2020-05-28 14:29:56 +0200 | [diff] [blame] | 139 | 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 Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 150 | // 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 Brun | 2073ce3 | 2021-02-03 18:52:59 +0100 | [diff] [blame] | 155 | func (c *collection) inject(importpath, version string, opts ...buildOpt) *collection { |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 156 | d := c.highlevel.child(importpath, version) |
| 157 | c.transitive[importpath] = d |
| 158 | c.p.available[importpath] = d |
| 159 | c.p.enabled[importpath] = true |
Lorenz Brun | 2073ce3 | 2021-02-03 18:52:59 +0100 | [diff] [blame] | 160 | for _, o := range opts { |
| 161 | o(c.transitive[importpath]) |
| 162 | } |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 163 | |
| 164 | return c |
| 165 | } |
| 166 | |
| 167 | // with transforms a collection into an optionized, by setting some build options. |
| 168 | func (c *collection) with(o ...buildOpt) *optionized { |
| 169 | return &optionized{ |
| 170 | c: c, |
| 171 | opts: o, |
| 172 | } |
| 173 | } |
| 174 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 175 | // optionized is a collection that has some build options set, that will be applied |
| 176 | // to all dependencies 'used' in this context |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 177 | type optionized struct { |
| 178 | c *collection |
| 179 | opts []buildOpt |
| 180 | } |
| 181 | |
| 182 | // buildOpt is a build option passed to Gazelle. |
| 183 | type buildOpt func(d *dependency) |
| 184 | |
| 185 | // buildTags sets the given buildTags in affected dependencies. |
| 186 | func buildTags(tags ...string) buildOpt { |
| 187 | return func(d *dependency) { |
| 188 | d.buildTags = tags |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | // disabledProtoBuild disables protobuf builds in affected dependencies. |
| 193 | func disabledProtoBuild(d *dependency) { |
| 194 | d.disableProtoBuild = true |
| 195 | } |
| 196 | |
| 197 | // patches applies patches in affected dependencies after BUILD file generation. |
| 198 | func patches(patches ...string) buildOpt { |
| 199 | return func(d *dependency) { |
| 200 | d.patches = patches |
| 201 | } |
| 202 | } |
| 203 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 204 | // prePatches applies patches in affected dependencies before BUILD file |
| 205 | // generation. |
Lorenz Brun | efb028f | 2020-07-28 17:04:49 +0200 | [diff] [blame] | 206 | func prePatches(patches ...string) buildOpt { |
| 207 | return func(d *dependency) { |
| 208 | d.prePatches = patches |
| 209 | } |
| 210 | } |
| 211 | |
Serge Bazanski | 14cf750 | 2020-05-28 14:29:56 +0200 | [diff] [blame] | 212 | func forceBazelGeneration(d *dependency) { |
| 213 | d.forceBazelGeneration = true |
| 214 | } |
| 215 | |
| 216 | func buildExtraArgs(args ...string) buildOpt { |
| 217 | return func(d *dependency) { |
| 218 | d.buildExtraArgs = args |
| 219 | } |
| 220 | } |
| 221 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 222 | // use enables given dependencies defined in the collection by a high-level |
| 223 | // dependency, with any set build options. After returning, the builder degrades to |
| 224 | // a collection - ie, all build options are reset. |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 225 | func (o *optionized) use(paths ...string) *collection { |
| 226 | for _, path := range paths { |
| 227 | el, ok := o.c.transitive[path] |
| 228 | if !ok { |
| 229 | msg := fmt.Sprintf("dependency %q not found in %q", path, o.c.highlevel.importpath) |
| 230 | if alternative, ok := o.c.p.seen[path]; ok { |
| 231 | msg += fmt.Sprintf(" (but found in %q)", alternative) |
| 232 | } else { |
| 233 | msg += " or any other collected library" |
| 234 | } |
| 235 | panic(msg) |
| 236 | } |
| 237 | for _, o := range o.opts { |
| 238 | o(el) |
| 239 | } |
| 240 | o.c.p.enabled[path] = true |
| 241 | } |
| 242 | |
| 243 | return o.c |
| 244 | } |