blob: ff990a316fc55888787da8e11e1ce4df9f5fc821 [file] [log] [blame] [view]
Fietsje
=======
The little Gazelle that could.
Introduction
------------
Fietsje is a dependency management system for Go dependencies in nxt. It does
not replace either gomods or Gazelle, but instead builds upon both on them in
a way that makes sense for our particular usecase: pulling in a large set of
dependency trees from third\_party projects, and sticking to those as much as
possible.
When run, Fietsje consults rules written themselves in Go (in `deps_.*go`
files), and uses this high-level intent to write a `repositories.bzl` file
that is then consumed by Gazelle. It caches 'locked' versions (ie. Go import
path and version to a particular checksum) in the Shelf, a text proto file
that lives alongside `repositories.bzl`. The Shelf should not be modified
manually.
The effective source of truth used for builds is still the `repositories.bzl`
file in the actual build path. Definitions in Go are in turn the high-level
intent that is used to build `repositories.bzl`.
Running
-------
You should run Fietsje any time you want to update dependencies. The following
should be a no-op if you haven't changed anything in `deps_*.go`:
scripts/bin/bazel run //:fietsje
Otherwise, if any definition in build/fietsje/deps_*.go has been changed,
third_party/go/repositories.bzl will now reflect that.
Fietsje Definition DSL (collect/use/...)
----------------------------------------
Definitions are kept in pure Go source, with a light DSL focused around a
'planner' builder.
The builder allows for two kinds of actions:
- import a high level dependency (eg. Kubernetes, google/tpm) at a particular
version. This is done using the `collect()` call. The dependency will now
be part of the build, but its transitive dependencies will not. A special
flavor of collect() is collectOverride(), that explicitely allows for
overriding a dependency that has already been pulled in by another high
level dependency.
- enable a transitive dependency defined by a high-level definition using the `use()`
call. This can only be done in a `collection` builder context, ie. after a
`collect()`/`collectOverride()`call.
In addition, the builder allows to augment a `collection` context with build flags
(like enabled patches, build tags, etc) that will be applied to the next `.use()`
call only. This is done by calling `.with()`.
In general, `.collect()`/`.collectOverride()` calls should be limited only to
dependencies 'we' (as developers) want. These 'high-level' dependencies are
large projects like Kubernetes, or direct imports from nxt itself. Every
transitive dependency of those should just be enabled by calling `.use()`,
instead of another `.collectOverride()` call that might pin it to a wrong
version.
After updating definitions, run Fietsje as above.
How to: add a new high-level dependency
---------------------------------------
To add a new high-level dependency, first consider making a new `deps_*.go`
file for it. If you're pulling in a separate ecosystem of code (ie. a large
third-party project like kubernetes), it should live in its own file for
clarity. If you're just pulling in a simple dependency (eg. a library low on
transitive dependencies) you can drop it into `main.go`.
The first step is to pick a version of the dependency you want to use. If
possible, pick a tag/release. Otherwise, pick the current master commit hash.
You can find version information by visiting the project's git repository web
viewer, or first cloning the repository locally.
Once you've picked a version, add a line like this:
p.collect("github.com/example/foo", "1.2.3")
If you now re-run Fietsje and rebuild your code, it should be able to link
against the dependency directly. If this works, you're done. If not, you will
start getting errors about the newly included library trying to link against
missing dependencies (ie. external Bazel workspaces). This means you need to
enable these transitive dependencies for the high-level dependency you've just
included.
If your high-level dependency contains a go.mod/go.sum file, you can call
`.use` on the return of the `collect()` call to enable them. Only enable the
ones that are necessary to build your code. In the future, audit flows might be
implemented to find and eradicate unused transitive dependencies, while enabling
ones that are needed - but for now this has to be done manually - usually by a
cycle of:
- try to build your code
- find missing transitive library, enable via .use()
- repeat until code builds
With our previous example, enabling transitive dependencies would look something
like this:
p.collect(
"github.com/example/foo", "1.2.3",
).use(
"github.com/example/libbar",
"github.com/example/libbaz",
"github.com/golang/glog",
)
What this means is that github.com/{example/libbar,example/libbaz,golang/glog}
will now be available to the build at whatever version example/foo defines them
in its go.mod/go.sum.
If your high-level dependency is not go.mod/go.sum compatible, you have
different ways to proceed:
- if the project uses some alternative resolution/vendoring code, write
support for it in transitive.go/`getTransitiveDeps`
- otherwise, if you're not in a rush, try to convince and/or send a PR to
upstream to enable Go module support
- if the dependency has little transitive dependencies, use `.inject()` to
add transitive dependencies manually after your `.collect()` call
- otherwise, extend fietsje to allow for out-of-tree go.mod/go.sum files kept
within nxt, or come up with some other solution.
Your new dependency might conflict with existing dependencies, which usually
manifests in build failures due to incompatible types. If this happens, you
will have to start digging to find a way to bring in compatible versions of
the two dependencies that are interacting with eachother. Do also mention any
such constraints in code comments near your `.collect()` call.
How to: update a high-level dependency
--------------------------------------
If you want to update a .collect()/.collectOverride() call, find out the
version you want to bump to and update it in the call. Re-running fietsje
will automatically update all enable transitive dependencies. Build and test
your code. Again, any possible conflicts will have to be resolved manually.
In the future, an audit flow might be provided for checking what the newest
available version of a high-level dependency is, to allow for easier,
semi-automated version bumps.
Version resolution conflicts
----------------------------
Any time a `.collect()`/`.collectOverride()` call is made, Fietsje will note
what transitive dependencies did the specified high-level dependency request.
Then, subsequent `.use()` calls will enable these dependencies in the build. On
subsequent `.collect()`/`.collectOverride()` calls, any transitive dependency
that already has been pulled in will be ignored, and the existing version will
be kept.
This means that Fietsje does not detect or handle version conflicts at a granular
level comparable to gomod. However, it does perform 'well enough', and in general
the Go ecosystem is stable enough that incompatibilites arise rarely - especially as
everything moves forward to versioned to go modules, which allow for multiple
incompatible versions to coexist as fully separate import paths.
It is as such the programmer's job to understand the relationship between imported
high-level dependencies. In the future, helper heuristics can be included that will
help understand and reason about dependency relationships. For now, Fietsje will just
help a user when they call `.use()` on the wrong dependency, ie. when the requested
transitive dependency has not been pulled in by a given high-level dependency.