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