|
| 1 | +name: Build kernel .deb |
| 2 | + |
| 3 | +# Manual-only and deliberately not on push/pull_request: this packages a real |
| 4 | +# kernel (make bindeb-pkg), which is an order of magnitude heavier than the |
| 5 | +# config-generation matrix in generate-config.yml -- much more disk, much |
| 6 | +# more time -- so it only runs when someone explicitly asks for a specific |
| 7 | +# kernel version, not on every commit. |
| 8 | +on: |
| 9 | + workflow_dispatch: |
| 10 | + inputs: |
| 11 | + kernel_version: |
| 12 | + description: > |
| 13 | + Exact kernel version to build, e.g. 6.19.4 or 7.1.0-rc2. No |
| 14 | + reference config is required for this -- genconfig.sh only needs |
| 15 | + one if you also turn on "validate" below. |
| 16 | + required: true |
| 17 | + type: string |
| 18 | + flavor: |
| 19 | + description: "Which flavor to build the .deb for." |
| 20 | + required: false |
| 21 | + default: generic |
| 22 | + type: choice |
| 23 | + options: |
| 24 | + - generic |
| 25 | + - incus-os |
| 26 | + validate: |
| 27 | + description: > |
| 28 | + Also compare the generated config against misc/<series>/zabbly-config |
| 29 | + for this kernel's series, purely informational (shown in the job |
| 30 | + summary). Off by default: a reference isn't needed to build a .deb, |
| 31 | + only to check fidelity against it, and this lets you package a |
| 32 | + kernel series that has no reference config yet. Turning this on |
| 33 | + fails the run if that series has no reference. |
| 34 | + required: false |
| 35 | + default: false |
| 36 | + type: boolean |
| 37 | + |
| 38 | +jobs: |
| 39 | + build-deb: |
| 40 | + # Each target is a distro whose own toolchain (gcc, pahole, libc) the |
| 41 | + # package has to be built against -- a .deb built with debian-12's gcc |
| 42 | + # isn't guaranteed compatible with debian-13 or ubuntu-24.04, and things |
| 43 | + # this repo's own tooling probes at generation time (CC_VERSION_TEXT, |
| 44 | + # PAHOLE_VERSION) need to reflect whichever toolchain actually builds the |
| 45 | + # kernel, not the runner's own Ubuntu image. So generation AND build both |
| 46 | + # happen inside the matching container, not just the final `make`. |
| 47 | + name: ${{ matrix.target }} (${{ inputs.flavor }} / ${{ inputs.kernel_version }}) |
| 48 | + runs-on: ubuntu-latest |
| 49 | + # Generous but explicit: a full-featured build with modules can run well |
| 50 | + # past an hour on a 4-core runner. Adjust if your flavor is smaller/bigger. |
| 51 | + timeout-minutes: 180 |
| 52 | + |
| 53 | + strategy: |
| 54 | + # One target failing (say, a package name that doesn't exist on |
| 55 | + # debian-13 yet) shouldn't hide the others' results. |
| 56 | + fail-fast: false |
| 57 | + matrix: |
| 58 | + target: |
| 59 | + #- debian-12 |
| 60 | + - debian-13 |
| 61 | + #- ubuntu-24.04 |
| 62 | + include: |
| 63 | + # Fully-qualified references rather than short names ("debian:12") |
| 64 | + # -- rootless podman's unqualified-search-registry behavior is a |
| 65 | + # config setting that varies by host, and being explicit here means |
| 66 | + # this doesn't depend on however the runner image has it set. |
| 67 | + #- target: debian-12 |
| 68 | + # image: docker.io/library/debian:12 |
| 69 | + - target: debian-13 |
| 70 | + image: docker.io/library/debian:13 |
| 71 | + #- target: ubuntu-24.04 |
| 72 | + # image: docker.io/library/ubuntu:24.04 |
| 73 | + |
| 74 | + env: |
| 75 | + FLAVOR: ${{ inputs.flavor }} |
| 76 | + KERNEL_VERSION: ${{ inputs.kernel_version }} |
| 77 | + |
| 78 | + steps: |
| 79 | + - name: Check out |
| 80 | + uses: actions/checkout@v4 |
| 81 | + with: |
| 82 | + submodules: recursive |
| 83 | + |
| 84 | + # Always computed (used in the job summary either way), but only |
| 85 | + # required to resolve to an existing reference when --validate is on. |
| 86 | + - name: Determine kernel series |
| 87 | + run: | |
| 88 | + set -euo pipefail |
| 89 | + series="$(echo "${KERNEL_VERSION%%-*}" | cut -d. -f1,2)" |
| 90 | + echo "series=$series" >> "$GITHUB_ENV" |
| 91 | + echo "Kernel series: $series" |
| 92 | +
|
| 93 | + # Only matters if --validate is going to be passed to genconfig.sh |
| 94 | + # later: fail fast, before spending time on a download, rather than |
| 95 | + # discovering the missing reference after the kernel fetch. When |
| 96 | + # validate is off, generation needs no reference at all, so this step |
| 97 | + # is skipped entirely. |
| 98 | + - name: Check the kernel series has a reference config |
| 99 | + if: inputs.validate |
| 100 | + run: | |
| 101 | + set -euo pipefail |
| 102 | + if [ ! -f "misc/${series}/zabbly-config" ]; then |
| 103 | + echo "error: no misc/${series}/zabbly-config for kernel version '$KERNEL_VERSION' (series '$series')" >&2 |
| 104 | + echo " available series: $(ls misc | tr '\n' ' ')" >&2 |
| 105 | + echo " or turn off 'validate' to build without comparing against a reference" >&2 |
| 106 | + exit 1 |
| 107 | + fi |
| 108 | + echo "Building $FLAVOR for Linux $KERNEL_VERSION (series $series), will validate against misc/${series}/zabbly-config" |
| 109 | +
|
| 110 | + # A kernel .deb build is one of the few things that reliably runs a |
| 111 | + # GitHub-hosted runner out of disk: default free space is ~25-29 GB out |
| 112 | + # of ~84 GB total, most of the rest being preinstalled toolchains |
| 113 | + # (Android SDK, .NET, multiple JDKs, GHC, ...) this job never touches. |
| 114 | + # Clearing them first is the standard workaround -- and matters even |
| 115 | + # more here, since podman's own image/container storage lands on the |
| 116 | + # same root filesystem as everything else. |
| 117 | + - name: Free up disk space |
| 118 | + run: | |
| 119 | + set -x |
| 120 | + df -h / |
| 121 | + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc \ |
| 122 | + /opt/hostedtoolcache/CodeQL /usr/local/.ghcup \ |
| 123 | + /usr/local/share/powershell /usr/local/share/chromium \ |
| 124 | + /usr/share/swift || true |
| 125 | + df -h / |
| 126 | +
|
| 127 | + # Podman ships preinstalled on the GitHub-hosted Ubuntu runner images, |
| 128 | + # so there's nothing to install here -- just pull the target image as |
| 129 | + # its own step for clearer logs/timing, separate from the build itself. |
| 130 | + - name: Pull the ${{ matrix.target }} image |
| 131 | + run: podman pull "${{ matrix.image }}" |
| 132 | + |
| 133 | + # Shared across all three matrix legs (same key, keyed only on kernel |
| 134 | + # version) since the source tarball itself doesn't depend on which |
| 135 | + # distro it's eventually packaged for. If two legs race on a cold |
| 136 | + # cache, the loser just re-downloads -- harmless, not a failure. |
| 137 | + - name: Cache kernel source tree |
| 138 | + id: kernel-cache |
| 139 | + uses: actions/cache@v4 |
| 140 | + with: |
| 141 | + path: ~/kernel/linux-${{ inputs.kernel_version }} |
| 142 | + key: linux-src-${{ inputs.kernel_version }} |
| 143 | + |
| 144 | + - name: Download kernel source tree |
| 145 | + if: steps.kernel-cache.outputs.cache-hit != 'true' |
| 146 | + run: | |
| 147 | + set -euo pipefail |
| 148 | + major="${KERNEL_VERSION%%.*}" |
| 149 | + url="https://cdn.kernel.org/pub/linux/kernel/v${major}.x/linux-${KERNEL_VERSION}.tar.xz" |
| 150 | + mkdir -p ~/kernel |
| 151 | + echo "Fetching $url" |
| 152 | + curl -fSL --retry 3 "$url" | tar -xJ -C ~/kernel |
| 153 | + test -f ~/kernel/linux-"$KERNEL_VERSION"/Makefile |
| 154 | +
|
| 155 | + # Written with container-internal paths, not host ones -- this .env is |
| 156 | + # only ever read from inside the container below. /kernel and /repo are |
| 157 | + # bind mounts set up in the podman run step further down. |
| 158 | + - name: Create .env |
| 159 | + run: | |
| 160 | + set -euo pipefail |
| 161 | + sed -e "s|^KERNEL_TREE_PATH=.*|KERNEL_TREE_PATH=/kernel/linux-${KERNEL_VERSION}|" \ |
| 162 | + -e "s|^KERNEL_TREE_BUILD_PATH=.*|KERNEL_TREE_BUILD_PATH=/kbuild|" \ |
| 163 | + .env.example > .env |
| 164 | + cat .env |
| 165 | +
|
| 166 | + # Everything from config generation through package collection runs as |
| 167 | + # one script inside the container, rather than as separate podman run |
| 168 | + # invocations per step -- state (the generated config, the build |
| 169 | + # directory) only needs to survive within a single container's |
| 170 | + # lifetime, not across steps. Written into the checkout itself so it |
| 171 | + # rides along on the /repo bind mount below without a mount of its own; |
| 172 | + # it's throwaway CI scaffolding, never committed. |
| 173 | + - name: Write the in-container build script |
| 174 | + run: | |
| 175 | + cat > .ci-build-deb.sh << 'SCRIPT' |
| 176 | + #!/bin/bash |
| 177 | + set -euo pipefail |
| 178 | + set -x |
| 179 | +
|
| 180 | + apt-get update |
| 181 | + apt-get install -y --no-install-recommends \ |
| 182 | + build-essential flex bison bc libelf-dev libssl-dev dwarves \ |
| 183 | + python3 fakeroot dpkg-dev rsync ca-certificates debhelper libdw-dev kmod |
| 184 | +
|
| 185 | + ./check_slices.py "$FLAVOR" |
| 186 | +
|
| 187 | + # Always normalized here, unlike the regular config-generation |
| 188 | + # workflow where it's a toggle: a .deb has to come from a config |
| 189 | + # the kernel's own Kconfig actually accepts. --validate is passed |
| 190 | + # only when asked for -- generation itself needs no reference. |
| 191 | + if [ "$VALIDATE" = "true" ]; then |
| 192 | + ./genconfig.sh "$FLAVOR" --normalize --validate |
| 193 | + else |
| 194 | + ./genconfig.sh "$FLAVOR" --normalize --no-validate |
| 195 | + fi |
| 196 | +
|
| 197 | + # genconfig.sh --validate, as its last step, runs the reference |
| 198 | + # config through fix-config.sh too, reusing KERNEL_TREE_BUILD_PATH |
| 199 | + # as scratch space -- so when validate is on, by the time it |
| 200 | + # returns, that directory's .config is the *reference's* normalized |
| 201 | + # config, not ours. Restore ours before building. GENERATED_CONFIG_PATH |
| 202 | + # as read from .env is unsuffixed; genconfig.sh appends "-$FLAVOR" |
| 203 | + # internally for every flavor but generic, and that never gets |
| 204 | + # written back to .env, so mirror the rule here too. |
| 205 | + source .env |
| 206 | + if [ "$FLAVOR" != "generic" ]; then |
| 207 | + GENERATED_CONFIG_PATH="${GENERATED_CONFIG_PATH}-${FLAVOR}" |
| 208 | + fi |
| 209 | + cp "$GENERATED_CONFIG_PATH" "${KERNEL_TREE_BUILD_PATH}/.config" |
| 210 | +
|
| 211 | + # LOCALVERSION disambiguates flavors within one target -- without |
| 212 | + # it, generic and incus-os would produce identically-named .debs |
| 213 | + # for the same kernel version and target, since neither flavor sets |
| 214 | + # its own CONFIG_LOCALVERSION. |
| 215 | + make -C "$KERNEL_TREE_PATH" O="$KERNEL_TREE_BUILD_PATH" \ |
| 216 | + -j"$(nproc)" LOCALVERSION="-${FLAVOR}" bindeb-pkg |
| 217 | +
|
| 218 | + # bindeb-pkg's output location has moved around across kernel |
| 219 | + # versions (srctree/.. vs objtree/..), so search both rather than |
| 220 | + # hardcode one, and copy into /out -- the one directory in this |
| 221 | + # container that's bind-mounted back to the host, so anything not |
| 222 | + # copied there is lost the moment the container is removed. |
| 223 | + mkdir -p /out |
| 224 | + found=0 |
| 225 | + for dir in "$(dirname "$KERNEL_TREE_PATH")" "$(dirname "$KERNEL_TREE_BUILD_PATH")"; do |
| 226 | + while IFS= read -r -d '' f; do |
| 227 | + cp "$f" /out/ |
| 228 | + found=1 |
| 229 | + done < <(find "$dir" -maxdepth 1 -name '*.deb' -print0 2>/dev/null) |
| 230 | + done |
| 231 | + if [ "$found" -eq 0 ]; then |
| 232 | + echo "error: bindeb-pkg produced no .deb files -- the build must have failed silently" >&2 |
| 233 | + exit 1 |
| 234 | + fi |
| 235 | + ls -lh /out |
| 236 | + SCRIPT |
| 237 | + chmod +x .ci-build-deb.sh |
| 238 | +
|
| 239 | + - name: Prepare the output directory |
| 240 | + run: mkdir -p "$RUNNER_TEMP/out-${{ matrix.target }}" |
| 241 | + |
| 242 | + # /repo is the checkout itself (rw) -- .env, the generated config, and |
| 243 | + # output/<flavor>/ all land there and are visible on the host |
| 244 | + # afterwards for free, no extra mount needed. /kernel is the cached |
| 245 | + # source tree, shared across all three targets. /out is where the |
| 246 | + # script above copies finished .debs before the container is removed. |
| 247 | + # KERNEL_TREE_BUILD_PATH (/kbuild) is deliberately NOT mounted: it's |
| 248 | + # pure scratch space, already extracted from before the container exits. |
| 249 | + - name: Build inside ${{ matrix.target }} |
| 250 | + run: | |
| 251 | + podman run --rm \ |
| 252 | + -e FLAVOR \ |
| 253 | + -e VALIDATE="${{ inputs.validate }}" \ |
| 254 | + -v "$PWD":/repo \ |
| 255 | + -v "$HOME/kernel":/kernel \ |
| 256 | + -v "$RUNNER_TEMP/out-${{ matrix.target }}":/out \ |
| 257 | + -w /repo \ |
| 258 | + "${{ matrix.image }}" \ |
| 259 | + bash /repo/.ci-build-deb.sh |
| 260 | +
|
| 261 | + # Rootless podman commonly maps the container's root user to a |
| 262 | + # subordinate UID range on the host, so files the script above wrote |
| 263 | + # into bind mounts as "root" may not be owned by the runner's own user |
| 264 | + # afterwards. Reads have generally been fine in testing (default |
| 265 | + # permissions are world-readable), but reclaiming ownership explicitly |
| 266 | + # is cheap insurance against a confusing permission error on upload. |
| 267 | + - name: Fix up ownership of container-written files |
| 268 | + if: always() |
| 269 | + run: | |
| 270 | + sudo chown -R "$(id -u):$(id -g)" \ |
| 271 | + "$RUNNER_TEMP/out-${{ matrix.target }}" \ |
| 272 | + "$GITHUB_WORKSPACE" || true |
| 273 | +
|
| 274 | + - name: Show diff against the reference config |
| 275 | + if: inputs.validate |
| 276 | + run: | |
| 277 | + echo "## Building $FLAVOR / $KERNEL_VERSION (${{ matrix.target }})" >> "$GITHUB_STEP_SUMMARY" |
| 278 | + echo >> "$GITHUB_STEP_SUMMARY" |
| 279 | + if [ -f "output/${FLAVOR}/diff" ]; then |
| 280 | + lines=$(wc -l < "output/${FLAVOR}/diff") |
| 281 | + echo "Config diff against misc/${series}/zabbly-config: $lines line(s)." >> "$GITHUB_STEP_SUMMARY" |
| 282 | + fi |
| 283 | +
|
| 284 | + - name: Upload .deb packages |
| 285 | + if: always() |
| 286 | + uses: actions/upload-artifact@v4 |
| 287 | + with: |
| 288 | + name: ${{ inputs.flavor }}-${{ inputs.kernel_version }}-${{ matrix.target }}-deb |
| 289 | + path: ${{ runner.temp }}/out-${{ matrix.target }}/*.deb |
| 290 | + if-no-files-found: warn |
0 commit comments