blob: 723813f0eb165725edeb57a9d6de5694ec6d8fe1 [file] [log] [blame] [view]
Serge Bazanskif369cfa2020-05-22 18:36:42 +02001Fietsje
2=======
3
4The little Gazelle that could.
5
6Introduction
7------------
8
Serge Bazanskiacae1ef2021-05-19 11:31:40 +02009Fietsje is a dependency management system for Go dependencies in monogon. It
10does not replace either gomods or Gazelle, but instead builds upon both on them
11in a way that makes sense for our particular usecase: pulling in a large set of
Serge Bazanskif369cfa2020-05-22 18:36:42 +020012dependency trees from third\_party projects, and sticking to those as much as
13possible.
14
15When run, Fietsje consults rules written themselves in Go (in `deps_.*go`
16files), and uses this high-level intent to write a `repositories.bzl` file
17that is then consumed by Gazelle. It caches 'locked' versions (ie. Go import
18path and version to a particular checksum) in the Shelf, a text proto file
19that lives alongside `repositories.bzl`. The Shelf should not be modified
20manually.
21
22The effective source of truth used for builds is still the `repositories.bzl`
23file in the actual build path. Definitions in Go are in turn the high-level
24intent that is used to build `repositories.bzl`.
25
26Running
27-------
28
29You should run Fietsje any time you want to update dependencies. The following
30should be a no-op if you haven't changed anything in `deps_*.go`:
31
32 scripts/bin/bazel run //:fietsje
33
34Otherwise, if any definition in build/fietsje/deps_*.go has been changed,
35third_party/go/repositories.bzl will now reflect that.
36
37Fietsje Definition DSL (collect/use/...)
38----------------------------------------
39
40Definitions are kept in pure Go source, with a light DSL focused around a
41'planner' builder.
42
43The builder allows for two kinds of actions:
44 - import a high level dependency (eg. Kubernetes, google/tpm) at a particular
45 version. This is done using the `collect()` call. The dependency will now
46 be part of the build, but its transitive dependencies will not. A special
47 flavor of collect() is collectOverride(), that explicitely allows for
48 overriding a dependency that has already been pulled in by another high
49 level dependency.
50 - enable a transitive dependency defined by a high-level definition using the `use()`
51 call. This can only be done in a `collection` builder context, ie. after a
52 `collect()`/`collectOverride()`call.
53
54In addition, the builder allows to augment a `collection` context with build flags
55(like enabled patches, build tags, etc) that will be applied to the next `.use()`
56call only. This is done by calling `.with()`.
57
58In general, `.collect()`/`.collectOverride()` calls should be limited only to
59dependencies 'we' (as developers) want. These 'high-level' dependencies are
Serge Bazanskiacae1ef2021-05-19 11:31:40 +020060large projects like Kubernetes, or direct imports from monogon itself. Every
Serge Bazanskif369cfa2020-05-22 18:36:42 +020061transitive dependency of those should just be enabled by calling `.use()`,
62instead of another `.collectOverride()` call that might pin it to a wrong
63version.
64
65After updating definitions, run Fietsje as above.
66
67How to: add a new high-level dependency
68---------------------------------------
69
70To add a new high-level dependency, first consider making a new `deps_*.go`
71file for it. If you're pulling in a separate ecosystem of code (ie. a large
72third-party project like kubernetes), it should live in its own file for
73clarity. If you're just pulling in a simple dependency (eg. a library low on
74transitive dependencies) you can drop it into `main.go`.
75
76The first step is to pick a version of the dependency you want to use. If
77possible, pick a tag/release. Otherwise, pick the current master commit hash.
78You can find version information by visiting the project's git repository web
79viewer, or first cloning the repository locally.
80
81Once you've picked a version, add a line like this:
82
83 p.collect("github.com/example/foo", "1.2.3")
84
85If you now re-run Fietsje and rebuild your code, it should be able to link
86against the dependency directly. If this works, you're done. If not, you will
87start getting errors about the newly included library trying to link against
88missing dependencies (ie. external Bazel workspaces). This means you need to
89enable these transitive dependencies for the high-level dependency you've just
90included.
91
92If your high-level dependency contains a go.mod/go.sum file, you can call
93`.use` on the return of the `collect()` call to enable them. Only enable the
94ones that are necessary to build your code. In the future, audit flows might be
95implemented to find and eradicate unused transitive dependencies, while enabling
96ones that are needed - but for now this has to be done manually - usually by a
97cycle of:
98
99 - try to build your code
100 - find missing transitive library, enable via .use()
101 - repeat until code builds
102
103With our previous example, enabling transitive dependencies would look something
104like this:
105
106 p.collect(
107 "github.com/example/foo", "1.2.3",
108 ).use(
109 "github.com/example/libbar",
110 "github.com/example/libbaz",
111 "github.com/golang/glog",
112 )
113
114What this means is that github.com/{example/libbar,example/libbaz,golang/glog}
115will now be available to the build at whatever version example/foo defines them
116in its go.mod/go.sum.
117
118If your high-level dependency is not go.mod/go.sum compatible, you have
119different ways to proceed:
120
121 - if the project uses some alternative resolution/vendoring code, write
122 support for it in transitive.go/`getTransitiveDeps`
123 - otherwise, if you're not in a rush, try to convince and/or send a PR to
124 upstream to enable Go module support
125 - if the dependency has little transitive dependencies, use `.inject()` to
126 add transitive dependencies manually after your `.collect()` call
127 - otherwise, extend fietsje to allow for out-of-tree go.mod/go.sum files kept
Serge Bazanskiacae1ef2021-05-19 11:31:40 +0200128 within monogon, or come up with some other solution.
Serge Bazanskif369cfa2020-05-22 18:36:42 +0200129
130Your new dependency might conflict with existing dependencies, which usually
131manifests in build failures due to incompatible types. If this happens, you
132will have to start digging to find a way to bring in compatible versions of
133the two dependencies that are interacting with eachother. Do also mention any
134such constraints in code comments near your `.collect()` call.
135
136How to: update a high-level dependency
137--------------------------------------
138
139If you want to update a .collect()/.collectOverride() call, find out the
140version you want to bump to and update it in the call. Re-running fietsje
141will automatically update all enable transitive dependencies. Build and test
142your code. Again, any possible conflicts will have to be resolved manually.
143
144In the future, an audit flow might be provided for checking what the newest
145available version of a high-level dependency is, to allow for easier,
146semi-automated version bumps.
147
148Version resolution conflicts
149----------------------------
150
151Any time a `.collect()`/`.collectOverride()` call is made, Fietsje will note
152what transitive dependencies did the specified high-level dependency request.
153Then, subsequent `.use()` calls will enable these dependencies in the build. On
154subsequent `.collect()`/`.collectOverride()` calls, any transitive dependency
155that already has been pulled in will be ignored, and the existing version will
156be kept.
157
158This means that Fietsje does not detect or handle version conflicts at a granular
159level comparable to gomod. However, it does perform 'well enough', and in general
160the Go ecosystem is stable enough that incompatibilites arise rarely - especially as
161everything moves forward to versioned to go modules, which allow for multiple
162incompatible versions to coexist as fully separate import paths.
163
164It is as such the programmer's job to understand the relationship between imported
165high-level dependencies. In the future, helper heuristics can be included that will
166help understand and reason about dependency relationships. For now, Fietsje will just
167help a user when they call `.use()` on the wrong dependency, ie. when the requested
168transitive dependency has not been pulled in by a given high-level dependency.
169