fietsje: implement
This introduces Fietsje, a little Go dependency manager.
For more information, see third_party/go/fietsje/README.md.
We also bump some dependencies while we're at it, notably, sqliboiler
now uses Go modules. If we weren't to do that, we'd have to add more
heuristics to Fietsje to handle the old version correctly.
Test Plan: fietsje is untested - I'll add some tests to it. Everything else is just regenerating basically the same repositories.bzl file, but with some bumped dependencies.
X-Origin-Diff: phab/D535
GitOrigin-RevId: 4fc919e1bd386bc3f3c1c53e672b1e3b9da17dfc
diff --git a/build/fietsje/planner.go b/build/fietsje/planner.go
new file mode 100644
index 0000000..4e67c2d
--- /dev/null
+++ b/build/fietsje/planner.go
@@ -0,0 +1,194 @@
+// Copyright 2020 The Monogon Project Authors.
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+ "fmt"
+)
+
+// The Planner provides the main DSL and high-level control logic for resolving dependencies. It is the main API that
+// fietsje users should consume.
+
+// planner is a builder for a single world of Go package dependencies, and what is then emitted into a Starlark file
+// containing gazelle go_repository rules.
+// The planner's builder system covers three increasingly specific contextx:
+// - planner (this structure, allows for 'collecting' in high-level dependencies. ie. collections)
+// - collection (represents what has been pulled in by a high-level dependency, and allows for 'using' transitive
+// dependencies from a collection)
+// - optionized (represents a collection with extra build flags, eg. disabled proto builds)
+type planner struct {
+ // available is a map of importpaths to dependencies that the planner knows. This is a flat structure that is the
+ // main source of truth of actual dependency data, like a registry of everything that the planner knows about.
+ // The available dependency for a given importpath, as the planner progresses, might change, ie. when there is a
+ // version conflict. As such, code should use importpaths as atoms describing dependencies, instead of holding
+ // dependency pointers.
+ available map[string]*dependency
+ // enabled is a map of dependencies that will be emitted by the planner into the build via Gazelle.
+ enabled map[string]bool
+ // seen is a map of 'dependency' -> 'parent' importpaths, ie. returns what higher-level dependency (ie. one enabled
+ // with .collect()) pulled in a given dependency. This is only used for error messages to help the user find what
+ // a transitive dependency has been pulled in by.
+ seen map[string]string
+
+ shelf *shelf
+}
+
+func (p *planner) collect(importpath, version string, opts ...buildOpt) *collection {
+ return p.collectInternal(importpath, version, false, opts...)
+}
+
+func (p *planner) collectOverride(importpath, version string, opts ...buildOpt) *collection {
+ return p.collectInternal(importpath, version, true, opts...)
+}
+
+// collectInternal pulls in a high-level dependency into the planner and
+// enables it. It also parses all of its transitive // dependencies (not just
+// directly transitive, but recursively transitive) and makes the planner aware
+// of them. It does not enable these transitive dependencies, but returns a
+// collection builder, which can be used to do se by calling .use().
+func (p *planner) collectInternal(importpath, version string, override bool, opts ...buildOpt) *collection {
+ // Ensure overrides are explicit and minimal.
+ by, ok := p.seen[importpath]
+ if ok && !override {
+ 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))
+ }
+ if !ok && override {
+ 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))
+ }
+
+ d := &dependency{
+ shelf: p.shelf,
+ importpath: importpath,
+ version: version,
+ }
+ for _, o := range opts {
+ o(d)
+ }
+
+ // automatically enable direct import
+ p.enabled[d.importpath] = true
+ p.available[d.importpath] = d
+
+ td, err := d.getTransitiveDeps()
+ if err != nil {
+ panic(fmt.Errorf("could not get transitive deps for %q: %v", d.importpath, err))
+ }
+ // add transitive deps to 'available' map
+ for k, v := range td {
+ // skip dependencies that have already been enabled, dependencies are 'first enabled version wins'.
+ if _, ok := p.available[k]; ok && p.enabled[k] {
+ continue
+ }
+
+ p.available[k] = v
+
+ // make note of the high-level dependency that pulled in the dependency.
+ p.seen[v.importpath] = d.importpath
+ }
+
+ return &collection{
+ p: p,
+ highlevel: d,
+ transitive: td,
+ }
+}
+
+// collection represents the context of the planner after pulling/collecting in a high-level dependency. In this state,
+// the planner can be used to enable transitive dependencies of the high-level dependency.
+type collection struct {
+ p *planner
+
+ highlevel *dependency
+ transitive map[string]*dependency
+}
+
+// use enables given dependencies defined in the collection by a high-level dependency.
+func (c *collection) use(paths ...string) *collection {
+ return c.with().use(paths...)
+}
+
+// inject adds a dependency to a collection as if requested by the high-level dependency of the collection. This should
+// be used sparingly, for instance when high-level dependencies contain bazel code that uses some external workspaces
+// from Go modules, and those workspaces are not defined in parsed transitive dependency definitions like go.mod/sum.
+func (c *collection) inject(importpath, version string) *collection {
+ d := c.highlevel.child(importpath, version)
+ c.transitive[importpath] = d
+ c.p.available[importpath] = d
+ c.p.enabled[importpath] = true
+
+ return c
+}
+
+// with transforms a collection into an optionized, by setting some build options.
+func (c *collection) with(o ...buildOpt) *optionized {
+ return &optionized{
+ c: c,
+ opts: o,
+ }
+}
+
+// optionized is a collection that has some build options set, that will be applied to all dependencies 'used' in this
+// context
+type optionized struct {
+ c *collection
+ opts []buildOpt
+}
+
+// buildOpt is a build option passed to Gazelle.
+type buildOpt func(d *dependency)
+
+// buildTags sets the given buildTags in affected dependencies.
+func buildTags(tags ...string) buildOpt {
+ return func(d *dependency) {
+ d.buildTags = tags
+ }
+}
+
+// disabledProtoBuild disables protobuf builds in affected dependencies.
+func disabledProtoBuild(d *dependency) {
+ d.disableProtoBuild = true
+}
+
+// patches applies patches in affected dependencies after BUILD file generation.
+func patches(patches ...string) buildOpt {
+ return func(d *dependency) {
+ d.patches = patches
+ }
+}
+
+// use enables given dependencies defined in the collection by a high-level dependency, with any set build options.
+// After returning, the builder degrades to a collection - ie, all build options are reset.
+func (o *optionized) use(paths ...string) *collection {
+ for _, path := range paths {
+ el, ok := o.c.transitive[path]
+ if !ok {
+ msg := fmt.Sprintf("dependency %q not found in %q", path, o.c.highlevel.importpath)
+ if alternative, ok := o.c.p.seen[path]; ok {
+ msg += fmt.Sprintf(" (but found in %q)", alternative)
+ } else {
+ msg += " or any other collected library"
+ }
+ panic(msg)
+ }
+ for _, o := range o.opts {
+ o(el)
+ }
+ o.c.p.enabled[path] = true
+ }
+
+ return o.c
+}