|
1 | 1 | # bldr |
2 | 2 |
|
| 3 | +`bldr` is a tool to build and package software distributions. Build process |
| 4 | +runs in [buildkit](https://github.com/moby/buildkit) |
| 5 | +(or [docker buildx](https://github.com/docker/buildx)), build result |
| 6 | +can be exported as container image. |
| 7 | + |
3 | 8 | ## Roadmap |
4 | 9 |
|
5 | 10 | - Tests |
6 | | -- Allow global variables |
7 | 11 | - Link using rpath or static binaries |
8 | 12 | - Dependency resolution |
9 | 13 | - Leverage labels for things like: |
10 | 14 | - Switching the base dir install command (we default to alpine apk install for now). |
11 | 15 | We could add a label on the bldr container `bldr.io.base.distro=[alpine,ubuntu,centos,etc.]` |
12 | 16 | - Automatically detecting `to` in a dependency. |
13 | 17 | We can label the container on a build with what the `finalize.to` was set to, and then automatically `COPY` from that location. |
14 | | -- LLB implementation |
15 | 18 | - Subpackages |
16 | 19 | - Allow for packaging `include` and `lib` into dedicated packages |
| 20 | + |
| 21 | +## Usage |
| 22 | + |
| 23 | +Given directory structure with `Pkgfile` and `pkg.yaml` |
| 24 | +(see [tools repository](https://github.com/talos-systems/tools) as an example),build can be triggered using following commands: |
| 25 | + |
| 26 | +- via `docker buildx`: |
| 27 | + |
| 28 | + ```sh |
| 29 | + docker buildx build -f ./Pkgfile --target tools . |
| 30 | + ``` |
| 31 | + |
| 32 | +- via `buildkit`: |
| 33 | + |
| 34 | + ```sh |
| 35 | + buildctl --frontend=dockerfile.v0 --local context=. --local dockerfile=. --opt filename=Pkgfile --opt target=tools |
| 36 | + ``` |
| 37 | + |
| 38 | +### Target |
| 39 | + |
| 40 | +Each `bldr` invocation specifies a target package to build, it is set |
| 41 | +as `--target` flag for `docker buildx` and `--opt target=` option for |
| 42 | +`buildctl`. `bldr` frontend is launched, it loads `Pkgfile` and scans |
| 43 | +subdirectories for `pkg.yaml` files, resolves dependencies and produces |
| 44 | +[LLB](https://github.com/moby/buildkit#exploring-llb) input |
| 45 | +which is executed in `buildkit` backend. Result of execution |
| 46 | +is the target argument of the invocation. |
| 47 | + |
| 48 | +### Saving output |
| 49 | + |
| 50 | +Build output can be exported from buildkit using any of the methods |
| 51 | +supported by [buildkit](https://github.com/moby/buildkit#output) or |
| 52 | +[docker buildx](https://github.com/docker/buildx#-o---outputpath-typetypekeyvalue). By default, if output is |
| 53 | +not specified output is saved in buildkit cache only. This is still |
| 54 | +useful to test the build before pushing. |
| 55 | + |
| 56 | +Most common output type is pushing to the registry: |
| 57 | + |
| 58 | +- via `docker buildx`: |
| 59 | + |
| 60 | + ```sh |
| 61 | + docker buildx build -f ./Pkgfile --target tools --tag org/repo:version --push . |
| 62 | + ``` |
| 63 | + |
| 64 | +- via `buildctl`: |
| 65 | + |
| 66 | + ```sh |
| 67 | + buildctl --frontend=dockerfile.v0 --local context=. --local dockerfile=. --opt filename=Pkgfile --opt target=tools --output type=image,name=docker.io/org/repo:version,push=true |
| 68 | + ``` |
| 69 | + |
| 70 | +### Graphing packages |
| 71 | + |
| 72 | +Graph of dependencies could be generated via `bldr` CLI: |
| 73 | + |
| 74 | + bldr graph |
| 75 | + |
| 76 | +This command also accepts `--target` flag to graph only part of the tree |
| 77 | +leading to the target: |
| 78 | + |
| 79 | + bldr graph --target tools |
| 80 | + |
| 81 | +`bldr` outputs graph in [graphviz](http://www.graphviz.org/) format which can be rendered to any image format via `dot`: |
| 82 | + |
| 83 | + bldr graph | dot -Tpng > graph.png |
| 84 | + |
| 85 | +This renders graph like: |
| 86 | + |
| 87 | + |
| 88 | + |
| 89 | +Boxes with yellow background are external images as dependencies, white |
| 90 | +nodes are internal stages. Arrows present dependencies: regular arrows |
| 91 | +for build dependencies and green bold arrows for runtime dependencies. |
| 92 | + |
| 93 | +## Format |
| 94 | + |
| 95 | +`bldr` expect following directory structure: |
| 96 | + |
| 97 | +```text |
| 98 | +├── Pkgfile |
| 99 | +├── protobuf |
| 100 | +│ ├── patches |
| 101 | +│ │ └── musl-fix.patch |
| 102 | +│ └── pkg.yaml |
| 103 | +├── protoc-gen-go |
| 104 | +│ └── pkg.yaml |
| 105 | +├── python2 |
| 106 | +│ └── pkg.yaml |
| 107 | +``` |
| 108 | + |
| 109 | +At the directory root there should be `Pkgfile` which triggers dockerfile |
| 110 | +frontend build and contains global options. Each package resides in |
| 111 | +subdirectory with `pkg.yaml` file and any additional files used for |
| 112 | +the build processes (patches, additional files, sources, etc.) |
| 113 | + |
| 114 | +Subdirectory structure could is flexible, `bldr` just looks for |
| 115 | +any subdirectory which has `pkg.yaml` file in it. Subdirectory |
| 116 | +names are ignored. |
| 117 | + |
| 118 | +### `Pkgfile` |
| 119 | + |
| 120 | +```yaml |
| 121 | +# syntax = docker.io/autonomy/bldr:1289eba-frontend |
| 122 | + |
| 123 | +format: v1alpha2 |
| 124 | + |
| 125 | +vars: |
| 126 | + TOOLCHAIN_IMAGE: docker.io/autonomy/toolchain:0714f82 |
| 127 | +``` |
| 128 | +
|
| 129 | +First line of the file should always be shebang which is picked up by |
| 130 | +dockerfile frontend of buildkit and redirects build to the `bldr` frontend. |
| 131 | +Version tag should match version of the `bldr` you want to use. |
| 132 | + |
| 133 | +Rest of the `Pkgfile` is regular YAML file with the following fields: |
| 134 | + |
| 135 | +- `format` (*string*, *required*): format of the `pkg.yaml` files, the only allowed value today is `v1alpha2`. |
| 136 | +- `vars` (*map[str]str*, *optional*): set of variables which are used to process `pkg.yaml` as a template. |
| 137 | + |
| 138 | +`bldr` parses `Pkgfile` as the first thing during the build, it should always |
| 139 | +reside at the root of the build tree. |
| 140 | + |
| 141 | +### Package |
| 142 | + |
| 143 | +Package is a subdirectory with `pkg.yaml` file in it. |
| 144 | + |
| 145 | +```text |
| 146 | +├── protobuf |
| 147 | +│ ├── patches |
| 148 | +│ │ └── musl-fix.patch |
| 149 | +│ └── pkg.yaml |
| 150 | +``` |
| 151 | + |
| 152 | +Any additional files in the directory are copied into the build and |
| 153 | +are available under `/pkg` subdirectory. For example, during the build the patch file above will be copied as `/pkg/patches/musl-fix.patch`. |
| 154 | + |
| 155 | +### `pkg.yaml` |
| 156 | + |
| 157 | +`pkg.yaml` describes build for a single package: |
| 158 | + |
| 159 | +```yaml |
| 160 | +name: bison |
| 161 | +variant: alpine |
| 162 | +install: |
| 163 | + - m4 |
| 164 | +shell: /bin/sh |
| 165 | +dependencies: |
| 166 | + - images: "{{ .TOOLCHAIN_IMAGE }}" |
| 167 | + - stage: perl |
| 168 | +steps: |
| 169 | + - sources: |
| 170 | + - url: https://ftp.gnu.org/gnu/bison/bison-3.0.5.tar.xz |
| 171 | + destination: bison.tar.xz |
| 172 | + sha256: 075cef2e814642e30e10e8155e93022e4a91ca38a65aa1d5467d4e969f97f338 |
| 173 | + sha512: 00b448db8abe91b07e32ff5273c6617bc1350d806f92073a9472f4c2f0de5d22c152795674171b74f2eb9eff8d36f8173b82dacb215601bb071ae39404d4a8a2 |
| 174 | + prepare: |
| 175 | + - tar -xJf bison.tar.xz --strip-components=1 |
| 176 | + - mkdir build |
| 177 | + - cd build |
| 178 | +
|
| 179 | + - | |
| 180 | + ../configure \ |
| 181 | + --prefix=${TOOLCHAIN} \ |
| 182 | + FORCE_UNSAFE_CONFIGURE=1 |
| 183 | + build: |
| 184 | + - cd build |
| 185 | + - make -j $(nproc) |
| 186 | + install: |
| 187 | + - cd build |
| 188 | + - make DESTDIR=/rootfs install |
| 189 | +finalize: |
| 190 | + - from: /rootfs |
| 191 | + to: / |
| 192 | +``` |
| 193 | + |
| 194 | +Before loading `pkg.yaml`, `bldr` runs file contents through [Go template engine](https://godoc.org/text/template) providing merged list of built-in variables (see below) and variables provided in `Pkgfile`. Most common syntax is to render variable value with `{{ .<variable_name> }}`. Due to the YAML syntax limitations, such constructs should be quoted if they start YAML value: `"{{ .VARIABLE }}"`. |
| 195 | + |
| 196 | +On the root level, following properties are available: |
| 197 | + |
| 198 | +- `name` (*str*, *required*): name of the package, also used to reference this package from other packages as dependency. |
| 199 | + |
| 200 | +- `variant` (*str*, *optional*): variant of the base image of the build. Two variants are available: |
| 201 | + - `alpine`: Alpine Linux 3.10 image with `bash` package pre-installed |
| 202 | + - `scratch`: scratch (empty) image |
| 203 | + Default variant is `alpine`. |
| 204 | +- `install`: (*list*, *optional*): list of Alpine packages to be installed as part of the build. These packages are usually build dependencies. |
| 205 | +- `shell`: (*str*, *optional*): path to the shell to execute build step instructions, defaults to `/bin/sh`. |
| 206 | + |
| 207 | +### `dependencies` |
| 208 | + |
| 209 | +Section `dependencies` lists build artifacts this package depends on. |
| 210 | + |
| 211 | +There are two kinds of dependencies: *external* and *internal*. External |
| 212 | +dependencies are container images which are copied into the build. Internal dependencies are references to other packages (by their `name:`) of the same build tree. Internal dependencies are resolved by `bldr` and `buildkit` and cached if there're no changes. Internal dependencies might be intermediate (never exported from the build) or they might be self-contained and exported from the build. |
| 213 | + |
| 214 | +Internal dependency: |
| 215 | + |
| 216 | +```yaml |
| 217 | +- stage: gcc |
| 218 | + runtime: false |
| 219 | + to: / |
| 220 | +``` |
| 221 | + |
| 222 | +External dependency: |
| 223 | + |
| 224 | +```yaml |
| 225 | +- image: docker.io/autonomy/toolchain:0714f82 |
| 226 | + runtime: false |
| 227 | + to: / |
| 228 | +``` |
| 229 | + |
| 230 | +Properties: |
| 231 | + |
| 232 | +- `stage` (*str*, *internal dependency*): name of other package this package depends on. Circular dependencies are not allowed. Contents of the stage are poured into the build at the location specified with `to:` parameter. |
| 233 | +- `image` (*str*, *external dependency*): reference to the registry container image this package depends on. Contents of the image are poured into the build at the location specified with `to:` parameter. |
| 234 | +- `runtime` (*bool*, *optional*): if set, marks dependency as runtime. This means that when this package is pulled in into the build, all the runtime dependencies are pulled in automatically as well. This also applies to transitive runtime dependencies. |
| 235 | +- `to` (*str*, *optional*, default `/`): location to copy dependency contents to. |
| 236 | + |
| 237 | +### `steps` |
| 238 | + |
| 239 | +Build process consists of the sequence of steps. Each step is composed out of phases: download sources, set environment variables, prepare, build, install and test. Each step runs in its own temporary directory. This temporary directory is set as working directory for the duration of the step. |
| 240 | + |
| 241 | +```yaml |
| 242 | +- sources: |
| 243 | + - url: https://dl.google.com/go/go1.13.1.src.tar.gz |
| 244 | + destination: go.src.tar.gz |
| 245 | + sha256: 81f154e69544b9fa92b1475ff5f11e64270260d46e7e36c34aafc8bc96209358 |
| 246 | + sha512: 696fc735271bd76ae59c5015c8efa52121243257f4ffcc1460fd79cf9a5e167db0b30d04137ec71a8789742673c2288bd62d55b546c2d2b2a05e8b3669af8616 |
| 247 | +
|
| 248 | + env: |
| 249 | + GOROOT_BOOTSTRAP: '{{ .TOOLCHAIN }}/go_bootstrap' |
| 250 | + GOROOT_FINAL: '{{ .TOOLCHAIN }}/go' |
| 251 | + CGO_ENABLED: '0' |
| 252 | +
|
| 253 | + prepare: |
| 254 | + - tar -xzf go.src.tar.gz --strip-components=1 |
| 255 | +
|
| 256 | + build: |
| 257 | + - cd src && sh make.bash |
| 258 | + install: |
| 259 | + - rm -rf pkg/obj |
| 260 | + - rm -rf pkg/bootstrap |
| 261 | + - rm -f pkg/tool/*/api |
| 262 | + - | |
| 263 | + find src \( -type f -a -name "*_test.go" \) \ |
| 264 | + -exec rm -rf \{\} \+ |
| 265 | + - | |
| 266 | + find src \( -type d -a -name "testdata" \) \ |
| 267 | + -exec rm -rf \{\} \+ |
| 268 | + - | |
| 269 | + find src -type f -a \( -name "*.bash" -o -name "*.rc" -o -name "*.bat" \) \ |
| 270 | + -exec rm -rf \{\} \+ |
| 271 | +
|
| 272 | + - mkdir -p "/rootfs${GOROOT_FINAL}" |
| 273 | + - mv * "/rootfs${GOROOT_FINAL}" |
| 274 | +``` |
| 275 | + |
| 276 | +Top-level keys describing phases are (all phases are optional): |
| 277 | + |
| 278 | +- `sources` (download) |
| 279 | +- `env` (environment variables) |
| 280 | +- `prepare` (shell script) |
| 281 | +- `build` (shell script) |
| 282 | +- `install` (shell script) |
| 283 | +- `test` (shell script) |
| 284 | + |
| 285 | +Download phase is described in `sources` section: |
| 286 | + |
| 287 | +- `url` (*str*, *required*): HTTP(S) URL of the object to download. |
| 288 | +- `destination` (*str*, *required*): destination file name under the build step temporary directory. |
| 289 | +- `sha256`, `sha512` (*str*, *required*): checksums for the downloaded object. |
| 290 | + |
| 291 | +Section `env` adds additional environment variables to the build. These environment variables persist to the steps following this one. |
| 292 | + |
| 293 | +Sections `prepare`, `build`, `install` and `test` list set of shell instructions to perform the build. They consist of a list of shell instruction. Each instruction is executed as LLB stage, so in terms of caching it's better to split into multiple instructions, but instructions don't share shell state (so `cd` in one instruction won't affect another). |
| 294 | + |
| 295 | +Each instruction is executed as a shell script, so any complex shell constructs can be used. Scripts are executed with options `set -eou pipefail`. |
| 296 | + |
| 297 | +### `finalize` |
| 298 | + |
| 299 | +Step `finalize` performs final copying of the build artifacts into scratch image which will be output of the build. There might be multiple `finalize` instructions in the package, they are executed sequentially. |
| 300 | + |
| 301 | +```yaml |
| 302 | +- from: /rootfs |
| 303 | + to: / |
| 304 | +``` |
| 305 | + |
| 306 | +- `from` (*str*, *optional*): copy source, defaults to `/` |
| 307 | +- `to` (*str*, *optional*): copy destination, defaults to `/` |
| 308 | + |
| 309 | +Finalize instruction `{"from": "/", "to": "/"}` copies full build contents as output image, but usually it doesn't make sense to include build temporary files and build dependencies into the package output. Usual trick to install build result under designated initially empty prefix (e.g. `/rootfs`) and set only contents of that prefix as build output. |
| 310 | + |
| 311 | +### Built-in variables |
| 312 | + |
| 313 | +Variables are made available to the templating engine when processing `pkg.yaml` contents and also pushed into the build as environment variables. |
| 314 | + |
| 315 | +Default variables: |
| 316 | + |
| 317 | +```bash |
| 318 | +CFLAGS="-g0 -Os" |
| 319 | +CXXFLAGS="-g0 -Os" |
| 320 | +LDFLAGS="-s" |
| 321 | +VENDOR="talos" |
| 322 | +SYSROOT="/talos" |
| 323 | +TOOLCHAIN="/toolchain" |
| 324 | +PATH="/toolchain/bin:/bin:/usr/bin:/sbin:/usr/sbin" |
| 325 | +``` |
| 326 | + |
| 327 | +Platform variables depend on build/host platform, for `linux/amd64` they will be: |
| 328 | + |
| 329 | +```bash |
| 330 | +BUILD=x86_64-linux-musl |
| 331 | +HOST=x86_64-linux-musl |
| 332 | +ARCH=x86_64 |
| 333 | +TARGET=x86_64-talos-linux-musl |
| 334 | +``` |
| 335 | + |
| 336 | +### Build flow |
| 337 | + |
| 338 | +When translated to LLB, build flow is the following: |
| 339 | + |
| 340 | +1. Base image (depends on `variant:`): either scratch image or Apline Linux with `bash` pre-installed (`/bin/sh` is a symlink to `/bin/bash`). |
| 341 | +2. Default environment variables are set. |
| 342 | +3. Alpine packages are installed (`install:` section), this makes sense only for `variant: alpine`. |
| 343 | +4. Local context (contents of package subdirectory except for `pkg.yaml`) are copied into `/pkg` directory in the build. |
| 344 | +5. Dependencies are copied into the build, including transitive runtime dependencies (if any). |
| 345 | +6. For each step: |
| 346 | + 1. Temporary directory is created (as working directory). |
| 347 | + 2. All the `sources:` are downloaded, checksums are verified. |
| 348 | + 3. Step-specific environment is set (leaks to the following steps). |
| 349 | + 4. Step instructions are executed for each phase: `prepare`, `build`, `install`, `test`. |
| 350 | +7. Finalize steps are performed. |
| 351 | + |
| 352 | +When internal stage as referenced as dependency, LLB for that step is also emitted and linked into the flow. |
| 353 | + |
| 354 | +Due to the way LLB is executed, some steps might be executed out of order if they don't have all the dependent steps already completed. For example, downloads happen first concurrently. Dependencies of a stage might be also executed concurrently. |
| 355 | + |
| 356 | +## Development |
| 357 | + |
| 358 | +When developing `bldr`, going via `dockerfile` frontend mode is not always the best way as it requires pushing frontend image each time any change is done. To help with development flow, `bldr` CLI supports `llb` command which emits LLB directly which can be piped into `buildctl`: |
| 359 | + |
| 360 | +```shell |
| 361 | +bldr llb --root . --target tools | buildctl ... |
| 362 | +``` |
| 363 | + |
| 364 | +LLB generated in this mode is equivalent to the LLB generated via dockerfile frontend. |
0 commit comments