blob: be955cf0b95912d74fad77927b4928e0dc0248d0 [file] [log] [blame]
// 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...)
}
// replace injects a new dependency with a replacement importpath. This is used to
// reflect 'replace' stanzas in go.mod files of third-party dependencies. This is
// not done automatically by Fietsje, as a replacement is global to the entire
// build tree, and should be done knowingly and explicitly by configuration. The
// 'oldpath' importpath will be visible to the build system, but will be backed at
// 'newpath' locked at 'version'.
func (c *collection) replace(oldpath, newpath, version string) *collection {
// Ensure oldpath is in use. We want as little replacements as possible, and if
// it's not being used by anything, it means that we likely don't need it.
c.use(oldpath)
d := c.highlevel.child(oldpath, version)
d.replace = newpath
c.transitive[oldpath] = d
c.p.available[oldpath] = d
c.p.enabled[oldpath] = true
return c
}
// 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, opts ...buildOpt) *collection {
d := c.highlevel.child(importpath, version)
c.transitive[importpath] = d
c.p.available[importpath] = d
c.p.enabled[importpath] = true
for _, o := range opts {
o(c.transitive[importpath])
}
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
}
}
// prePatches applies patches in affected dependencies before BUILD file
// generation.
func prePatches(patches ...string) buildOpt {
return func(d *dependency) {
d.prePatches = patches
}
}
func forceBazelGeneration(d *dependency) {
d.forceBazelGeneration = true
}
func buildExtraArgs(args ...string) buildOpt {
return func(d *dependency) {
d.buildExtraArgs = args
}
}
// 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
}