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 | |
| 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) |
| 33 | type 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 | |
| 50 | func (p *planner) collect(importpath, version string, opts ...buildOpt) *collection { |
| 51 | return p.collectInternal(importpath, version, false, opts...) |
| 52 | } |
| 53 | |
| 54 | func (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(). |
| 63 | func (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. |
| 112 | type 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. |
| 120 | func (c *collection) use(paths ...string) *collection { |
| 121 | return c.with().use(paths...) |
| 122 | } |
| 123 | |
Serge Bazanski | 14cf750 | 2020-05-28 14:29:56 +0200 | [diff] [blame^] | 124 | // replace injects a new dependency with a replacement importpath. This is used to reflect 'replace' stanzas in go.mod |
| 125 | // files of third-party dependencies. This is not done automatically by Fietsje, as a replacement is global to the |
| 126 | // entire build tree, and should be done knowingly and explicitly by configuration. The 'oldpath' importpath will be |
| 127 | // visible to the build system, but will be backed at 'newpath' locked at 'version'. |
| 128 | func (c *collection) replace(oldpath, newpath, version string) *collection { |
| 129 | // Ensure oldpath is in use. We want as little replacements as possible, and if it's not being used by anything, |
| 130 | // it means that we likely don't need it. |
| 131 | c.use(oldpath) |
| 132 | |
| 133 | d := c.highlevel.child(oldpath, version) |
| 134 | d.replace = newpath |
| 135 | c.transitive[oldpath] = d |
| 136 | c.p.available[oldpath] = d |
| 137 | c.p.enabled[oldpath] = true |
| 138 | |
| 139 | return c |
| 140 | } |
| 141 | |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 142 | // inject adds a dependency to a collection as if requested by the high-level dependency of the collection. This should |
| 143 | // be used sparingly, for instance when high-level dependencies contain bazel code that uses some external workspaces |
| 144 | // from Go modules, and those workspaces are not defined in parsed transitive dependency definitions like go.mod/sum. |
| 145 | func (c *collection) inject(importpath, version string) *collection { |
| 146 | d := c.highlevel.child(importpath, version) |
| 147 | c.transitive[importpath] = d |
| 148 | c.p.available[importpath] = d |
| 149 | c.p.enabled[importpath] = true |
| 150 | |
| 151 | return c |
| 152 | } |
| 153 | |
| 154 | // with transforms a collection into an optionized, by setting some build options. |
| 155 | func (c *collection) with(o ...buildOpt) *optionized { |
| 156 | return &optionized{ |
| 157 | c: c, |
| 158 | opts: o, |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | // optionized is a collection that has some build options set, that will be applied to all dependencies 'used' in this |
| 163 | // context |
| 164 | type optionized struct { |
| 165 | c *collection |
| 166 | opts []buildOpt |
| 167 | } |
| 168 | |
| 169 | // buildOpt is a build option passed to Gazelle. |
| 170 | type buildOpt func(d *dependency) |
| 171 | |
| 172 | // buildTags sets the given buildTags in affected dependencies. |
| 173 | func buildTags(tags ...string) buildOpt { |
| 174 | return func(d *dependency) { |
| 175 | d.buildTags = tags |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | // disabledProtoBuild disables protobuf builds in affected dependencies. |
| 180 | func disabledProtoBuild(d *dependency) { |
| 181 | d.disableProtoBuild = true |
| 182 | } |
| 183 | |
| 184 | // patches applies patches in affected dependencies after BUILD file generation. |
| 185 | func patches(patches ...string) buildOpt { |
| 186 | return func(d *dependency) { |
| 187 | d.patches = patches |
| 188 | } |
| 189 | } |
| 190 | |
Serge Bazanski | 14cf750 | 2020-05-28 14:29:56 +0200 | [diff] [blame^] | 191 | func forceBazelGeneration(d *dependency) { |
| 192 | d.forceBazelGeneration = true |
| 193 | } |
| 194 | |
| 195 | func buildExtraArgs(args ...string) buildOpt { |
| 196 | return func(d *dependency) { |
| 197 | d.buildExtraArgs = args |
| 198 | } |
| 199 | } |
| 200 | |
Serge Bazanski | f369cfa | 2020-05-22 18:36:42 +0200 | [diff] [blame] | 201 | // use enables given dependencies defined in the collection by a high-level dependency, with any set build options. |
| 202 | // After returning, the builder degrades to a collection - ie, all build options are reset. |
| 203 | func (o *optionized) use(paths ...string) *collection { |
| 204 | for _, path := range paths { |
| 205 | el, ok := o.c.transitive[path] |
| 206 | if !ok { |
| 207 | msg := fmt.Sprintf("dependency %q not found in %q", path, o.c.highlevel.importpath) |
| 208 | if alternative, ok := o.c.p.seen[path]; ok { |
| 209 | msg += fmt.Sprintf(" (but found in %q)", alternative) |
| 210 | } else { |
| 211 | msg += " or any other collected library" |
| 212 | } |
| 213 | panic(msg) |
| 214 | } |
| 215 | for _, o := range o.opts { |
| 216 | o(el) |
| 217 | } |
| 218 | o.c.p.enabled[path] = true |
| 219 | } |
| 220 | |
| 221 | return o.c |
| 222 | } |