diff --git a/.dockerignore b/.dockerignore index b46e3e81..cf9fbee7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ # Exclude build artifacts from Docker build context mkosi.output/ +mkosi.input/ mkosi.cache/ mkosi.tools/ mkosi.tools.manifest diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d6a2b591 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# Match Ruff style in .editorconfig format +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 100 + +[{*.sh,*.sh.j2}] +indent_style = space +indent_size = 4 +tab_width = 4 +max_line_length = 100 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 522dfab0..c61e3903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,6 @@ name: For each commit and PR on: + workflow_dispatch: push: branches: - "main" @@ -10,20 +11,27 @@ permissions: packages: write env: - KERNEL_VERSION: 6.18.16 + FORCE_COLOR: "1" + CAPTAIN_VERBOSE: "1" jobs: # ------------------------------------------------------------------- - # Lint — ruff (lint + format) and pyright (type checking) + # Lint — ruff (lint + format) and pyright (type checking). + # Doubles as the "prepare" job: resolves dynamic version tokens once + # (latest kernel.org release of the branch; armbian-next Release ETag) + # and exposes them as outputs consumed by the build jobs. # ------------------------------------------------------------------- - lint: + prepare: runs-on: ubuntu-latest + outputs: + kernel_version: ${{ steps.versions.outputs.kernel_version }} + armbian_version: ${{ steps.versions.outputs.armbian_version }} steps: - name: Checkout code uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Install lint tools run: make lint-install @@ -31,69 +39,80 @@ jobs: - name: Lint run: make lint + - name: Resolve dynamic version tokens + id: versions + run: uv run captain prepare-versions >> "$GITHUB_OUTPUT" + # ------------------------------------------------------------------- - # Fast cache lookup – decides whether the OCI runner is needed - # There are consistent issues with OCI runners not getting scheduled. - # This is the workaround. + # Download tools (containerd, runc, nerdctl, CNI plugins) # ------------------------------------------------------------------- - check-kernel-cache: - runs-on: ubuntu-latest - outputs: - amd64-cache-hit: ${{ steps.amd64-cache.outputs.cache-hit }} - arm64-cache-hit: ${{ steps.arm64-cache.outputs.cache-hit }} + download-tools: + needs: [ prepare ] + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + strategy: + fail-fast: false + matrix: + arch: [ amd64, arm64 ] + env: + ARCH: ${{ matrix.arch }} + MKOSI_MODE: skip + TOOLS_MODE: native steps: - name: Checkout code uses: actions/checkout@v6 - - name: Check amd64 kernel cache - id: amd64-cache + - name: Restore tools cache + id: tools-cache uses: actions/cache/restore@v5 with: path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/amd64 - key: kernel-amd64-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }} - lookup-only: true + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni + key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} - - name: Check arm64 kernel cache - id: arm64-cache - uses: actions/cache/restore@v5 + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Download tools + env: + TOOLS_MODE: native + run: uv run captain tools + + - name: Save tools cache + if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni + key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} + + - name: Upload tools artifacts + uses: actions/upload-artifact@v6 with: + name: tools-${{ matrix.arch }} path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/arm64 - key: kernel-arm64-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }} - lookup-only: true + mkosi.input/tools/${{ matrix.arch }}/usr/local/bin + mkosi.input/tools/${{ matrix.arch }}/opt/cni + retention-days: 1 # ------------------------------------------------------------------- - # Build kernel (vmlinuz + modules) inside Docker + # Build Docker builder image, per-arch; pushes to ghcr.io. # ------------------------------------------------------------------- - build-kernel: - needs: [lint, check-kernel-cache] - # Forks / cache-hit → cheap default runner; mainline cache-miss → fast oracle runner - runs-on: > - ${{ github.repository != 'tinkerbell/captain' && matrix.fork_runner - || needs.check-kernel-cache.outputs[format('{0}-cache-hit', matrix.arch)] == 'true' && matrix.fork_runner - || fromJSON(matrix.mainline_runner) }} + build-dockerfile: + needs: [ prepare ] + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false - matrix: - arch: [amd64, arm64] - include: - - arch: amd64 - fork_runner: ubuntu-latest - mainline_runner: '{"group":"Default","labels":["oracle-vm-16cpu-64gb-x86-64"]}' - - arch: arm64 - fork_runner: ubuntu-24.04-arm - mainline_runner: '{"group":"Default","labels":["oracle-vm-16cpu-64gb-arm64"]}' + matrix: { arch: [ amd64, arm64 ] } env: ARCH: ${{ matrix.arch }} - KERNEL_MODE: docker - MKOSI_MODE: skip steps: - name: Checkout code uses: actions/checkout@v6 - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" + - name: Install uv + uses: astral-sh/setup-uv@v7 - name: Log in to GHCR uses: docker/login-action@v4 @@ -102,321 +121,203 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Compute Dockerfile hash - id: dockerfile-hash - run: echo "hash=$(sha256sum Dockerfile | awk '{print $1}')" >> "$GITHUB_OUTPUT" - - - name: Pull or build builder image - id: builder - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - if docker pull "${REMOTE}:${HASH}-${{ matrix.arch }}"; then - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}:${HASH}" - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}" - echo "built=false" >> "$GITHUB_OUTPUT" - else - docker buildx build --progress=plain -t "${{ env.BUILDER_IMAGE }}:${HASH}" -t "${{ env.BUILDER_IMAGE }}" . - echo "built=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push builder image to GHCR - if: github.ref == 'refs/heads/main' && steps.builder.outputs.built == 'true' - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - docker tag "${{ env.BUILDER_IMAGE }}:${HASH}" "${REMOTE}:${HASH}-${{ matrix.arch }}" - docker push "${REMOTE}:${HASH}-${{ matrix.arch }}" - - - name: Compute kernel cache key - id: kernel-cache-key - run: echo "key=kernel-${{ matrix.arch }}-${{ env.KERNEL_VERSION }}-${{ hashFiles('kernel.configs/*', 'Dockerfile') }}" >> "$GITHUB_OUTPUT" - - - name: Restore kernel cache - id: kernel-cache - uses: actions/cache/restore@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - key: ${{ steps.kernel-cache-key.outputs.key }} - - - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 - - - name: Build kernel - run: uv run ./build.py kernel - - - name: Fix output file ownership - run: sudo chown -R "$(id -u):$(id -g)" mkosi.output/ - - - name: Save kernel cache - if: github.ref == 'refs/heads/main' && steps.kernel-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@v5 - with: - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - key: ${{ steps.kernel-cache-key.outputs.key }} + - name: Build Dockerfile + run: uv run captain builder + # only if a PR, since we don't want to push images for PRs from forks + if: github.event_name == 'pull_request' - - name: Upload kernel artifacts - uses: actions/upload-artifact@v7 - with: - name: kernel-${{ matrix.arch }} - path: | - mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} - retention-days: 1 + - name: Build Dockerfile and push + run: uv run captain builder --push + # only if not a PR, since we don't want to push images for PRs from forks (no perms to push) + if: github.event_name != 'pull_request' # ------------------------------------------------------------------- # Download tools (containerd, runc, nerdctl, CNI plugins) # ------------------------------------------------------------------- - download-tools: - needs: [lint] + build-captainos-kernel: + needs: [ prepare, build-dockerfile ] runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} strategy: fail-fast: false matrix: - arch: [amd64, arm64] + arch: [ amd64, arm64 ] env: ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip - MKOSI_MODE: skip - TOOLS_MODE: native + KERNEL_VERSION: ${{ needs.prepare.outputs.kernel_version }} steps: - name: Checkout code uses: actions/checkout@v6 - - name: Restore tools cache - id: tools-cache + - name: Restore kernel cache + id: kernel-cache uses: actions/cache/restore@v5 with: path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni - key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} + mkosi.input/kernel/${{ matrix.arch }} + key: kernel-${{ matrix.arch }}-${{ needs.prepare.outputs.kernel_version }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: | + kernel-${{ matrix.arch }} - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - - name: Download tools - run: uv run ./build.py tools + - name: Build kernel + run: uv run captain --verbose kernel - - name: Save tools cache - if: github.ref == 'refs/heads/main' && steps.tools-cache.outputs.cache-hit != 'true' + - name: Save kernel cache + if: github.ref == 'refs/heads/main' && steps.kernel-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v5 with: path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni - key: tools-${{ matrix.arch }}-${{ hashFiles('captain/tools.py') }} + mkosi.input/kernel/${{ matrix.arch }} + key: kernel-${{ matrix.arch }}-${{ needs.prepare.outputs.kernel_version }}-${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - - name: Upload tools artifacts - uses: actions/upload-artifact@v7 + - name: Upload kernel artifacts + uses: actions/upload-artifact@v6 with: - name: tools-${{ matrix.arch }} + name: kernel-${{ matrix.arch }} path: | - mkosi.output/tools/${{ matrix.arch }}/usr/local/bin - mkosi.output/tools/${{ matrix.arch }}/opt/cni + mkosi.input/kernel/${{ matrix.arch }} retention-days: 1 - # ------------------------------------------------------------------- - # Build initramfs via mkosi (depends on kernel + tools) - # ------------------------------------------------------------------- - build-initramfs: + + build-captainos-with-kernel: # only for flavors that require the custom CaptainOS kernel; otherwise, just build-captainos + name: "build-captainos (with kernel) ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-kernel, download-tools] + needs: [ prepare, download-tools, build-dockerfile, build-captainos-kernel ] strategy: fail-fast: false matrix: - arch: [amd64, arm64] - env: + include: + - { arch: amd64, output_arch: x86_64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-slim" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: true, iso: true, FLAVOR_ID: "trixie-slim" } + env: &build_env ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip - MKOSI_MODE: native - steps: + MKOSI_MODE: docker + ISO_MODE: docker + FLAVOR_ID: ${{ matrix.FLAVOR_ID }} + # Keep the initramfs/iso caches to only the freshly-built flavor hash; a + # restore-key (prefix) cache hit otherwise leaves the previous hash behind. + CAPTAIN_DROP_OLD_CACHES: "1" + # Latest-of-branch kernel (trixie-slim) and armbian-next repo token + # (armbian flavors), resolved once by the prepare job. + KERNEL_VERSION: ${{ needs.prepare.outputs.kernel_version }} + ARMBIAN_VERSION: ${{ needs.prepare.outputs.armbian_version }} + steps: &build_steps - name: Checkout code uses: actions/checkout@v6 - - name: Download kernel artifacts + - name: Download tools artifacts uses: actions/download-artifact@v8 with: - name: kernel-${{ matrix.arch }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} + name: tools-${{ matrix.arch }} + path: mkosi.input/tools/${{ matrix.arch }} - - name: Download tools artifacts + - name: Download kernel artifacts == ${{ matrix.captainos_kernel }} uses: actions/download-artifact@v8 + if: ${{ matrix.captainos_kernel }} # only if matrix entry had captainos_kernel: true with: - name: tools-${{ matrix.arch }} - path: mkosi.output/tools/${{ matrix.arch }} + name: kernel-${{ matrix.arch }} + path: mkosi.input/kernel/${{ matrix.arch }} - name: Restore tool binary permissions run: | # GitHub Actions artifact upload/download strips execute permissions. # Restore +x on all tool binaries so they work inside the initramfs. - chmod +x mkosi.output/tools/${{ matrix.arch }}/usr/local/bin/* - chmod +x mkosi.output/tools/${{ matrix.arch }}/opt/cni/bin/* + chmod +x mkosi.input/tools/${{ matrix.arch }}/usr/local/bin/* + chmod +x mkosi.input/tools/${{ matrix.arch }}/opt/cni/bin/* + + - name: Restore initramfs cache + id: initramfs-cache + uses: actions/cache/restore@v5 + with: + path: mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- - - name: Refresh apt cache - run: sudo apt-get -o "Dpkg::Use-Pty=0" update + - name: Show initramfs cache restore SHA256's + run: | + echo "initramfs cache hit: ${{ steps.initramfs-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/initramfs -type f || true) || true - - name: setup-mkosi - # Pinned post-v26 to pick up systemd/mkosi@1f811f05 ("tools: move grub-pc-bin - # to arch-specific drop-in"), which fixes arm64 builds failing on the - # default tools-tree pulling in grub-pc-bin (BIOS GRUB, x86-only). - uses: systemd/mkosi@1f811f0524be3096872e79161c8e6ab3e7c2bb1f + - name: Restore iso cache + id: iso-cache + if: ${{ matrix.iso }} # only if matrix entry had iso: true + uses: actions/cache/restore@v5 + with: + path: mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} + # any "previous" cache can also be useful; we'll just churn cache misses more often, but that's better than a complete cache miss and rebuild every time + restore-keys: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}-- - - name: Install bubblewrap + - name: Show iso cache restore SHA256's + if: ${{ matrix.iso }} # only if matrix entry had iso: true run: | - sudo apt-get -o "Dpkg::Use-Pty=0" update - sudo apt-get -o "Dpkg::Use-Pty=0" install -y bubblewrap + echo "iso cache hit: ${{ steps.iso-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/iso -type f || true) || true - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Build initramfs - run: uv run ./build.py initramfs - - - name: Upload initramfs artifacts - uses: actions/upload-artifact@v7 - with: - name: initramfs-${{ matrix.arch }} - path: out/ - retention-days: 1 + run: uv run captain build # full build, incl initramfs and iso when appropriate - # ------------------------------------------------------------------- - # Build UEFI-bootable ISO (depends on initramfs) - # ------------------------------------------------------------------- - build-iso: - runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-initramfs] - strategy: - fail-fast: false - matrix: - arch: [amd64, arm64] - include: - - arch: amd64 - output_arch: x86_64 - - arch: arm64 - output_arch: aarch64 - env: - ARCH: ${{ matrix.arch }} - KERNEL_MODE: skip - MKOSI_MODE: skip - ISO_MODE: docker - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" + - name: Show initramfs cache save SHA256's + run: | + echo "initramfs cache hit: ${{ steps.initramfs-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/initramfs -type f || true) || true - - name: Log in to GHCR - uses: docker/login-action@v4 + - name: Save initramfs cache + if: github.ref == 'refs/heads/main' && steps.initramfs-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Compute Dockerfile hash - id: dockerfile-hash - run: echo "hash=$(sha256sum Dockerfile | awk '{print $1}')" >> "$GITHUB_OUTPUT" + path: mkosi.output/initramfs + key: initramfs-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - - name: Pull or build builder image - id: builder - run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - if docker pull "${REMOTE}:${HASH}-${{ matrix.arch }}"; then - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}:${HASH}" - docker tag "${REMOTE}:${HASH}-${{ matrix.arch }}" "${{ env.BUILDER_IMAGE }}" - echo "built=false" >> "$GITHUB_OUTPUT" - else - docker build -t "${{ env.BUILDER_IMAGE }}:${HASH}" -t "${{ env.BUILDER_IMAGE }}" . - echo "built=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push builder image to GHCR - if: github.ref == 'refs/heads/main' && steps.builder.outputs.built == 'true' + - name: Show iso cache save SHA256's + if: ${{ matrix.iso }} # only if matrix entry had iso: true run: | - HASH="${{ steps.dockerfile-hash.outputs.hash }}" - REMOTE="ghcr.io/${{ github.repository }}/${{ env.BUILDER_IMAGE }}" - docker tag "${{ env.BUILDER_IMAGE }}:${HASH}" "${REMOTE}:${HASH}-${{ matrix.arch }}" - docker push "${REMOTE}:${HASH}-${{ matrix.arch }}" + echo "iso cache hit: ${{ steps.iso-cache.outputs.cache-hit }}" + sha256sum $(find mkosi.output/iso -type f || true) || true - - name: Download kernel artifacts - uses: actions/download-artifact@v8 + - name: Save iso cache + if: matrix.iso && github.ref == 'refs/heads/main' && steps.iso-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 with: - name: kernel-${{ matrix.arch }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.arch }} + path: mkosi.output/iso + key: iso-${{ matrix.arch }}-${{matrix.FLAVOR_ID}}--k${{ needs.prepare.outputs.kernel_version }}-a${{ needs.prepare.outputs.armbian_version }}--${{ hashFiles('**/*.py', 'kernel.configs/*', 'Dockerfile', 'pyproject.toml') }} - - name: Download initramfs artifacts - uses: actions/download-artifact@v8 + - name: Upload initramfs artifacts + uses: actions/upload-artifact@v6 with: - name: initramfs-${{ matrix.arch }} - path: out - - - name: Stage initramfs for ISO build - run: | - mkdir -p "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}" - cp "out/initramfs-${KERNEL_VERSION}-${{ matrix.output_arch }}" \ - "mkosi.output/initramfs/${KERNEL_VERSION}/${{ matrix.arch }}/image.cpio.zst" - - - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + name: initramfs-${{ matrix.FLAVOR_ID }}-${{ matrix.arch }} + # The full 'out/' directory contents, but not any .iso files (if any) since those are uploaded later + path: | + out/ + !out/**/*.iso + retention-days: 1 + # do not upload any .iso files (exclude) - - name: Build ISO - run: uv run ./build.py iso + # ------------------------------------------------------------------- + # UEFI-bootable ISO - only for certain flavors (eg trixie-full) + # ------------------------------------------------------------------- - name: Upload ISO artifact - uses: actions/upload-artifact@v7 + if: ${{ matrix.iso }} # only if matrix entry had iso: true + uses: actions/upload-artifact@v6 with: - name: iso-${{ matrix.arch }} - path: out/captainos-${{ env.KERNEL_VERSION }}-${{ matrix.output_arch }}.iso + name: iso-${{ matrix.FLAVOR_ID }}-${{ matrix.arch }} + path: out/captainos-${{ matrix.FLAVOR_ID }}-${{ matrix.output_arch }}.iso retention-days: 1 - # ------------------------------------------------------------------- - # Publish per-arch artifacts and compute checksums - # ------------------------------------------------------------------- - publish-per-arch: - if: github.ref == 'refs/heads/main' - runs-on: ${{ matrix.target == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} - needs: [build-iso] - strategy: - fail-fast: false - matrix: - target: [amd64, arm64] - env: - ARCH: ${{ matrix.target }} - TARGET: ${{ matrix.target }} - steps: - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Load shared config - run: cat .github/config.env >> "$GITHUB_ENV" - - - name: Download kernel artifacts - uses: actions/download-artifact@v8 - with: - name: kernel-${{ matrix.target }} - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/${{ matrix.target }} - - - name: Download initramfs artifacts - uses: actions/download-artifact@v8 - with: - name: initramfs-${{ matrix.target }} - path: out - - - name: Download ISO artifact - uses: actions/download-artifact@v8 - with: - name: iso-${{ matrix.target }} - path: out - - - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + # ------------------------------------------------------------------- + # Publish per-arch artifacts and compute checksums + # ------------------------------------------------------------------- - name: Log in to GHCR + if: github.ref == 'refs/heads/main' uses: docker/login-action@v4 with: registry: ghcr.io @@ -424,65 +325,81 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish artifacts to GHCR - run: uv run ./build.py release publish + # only pushes to main, and not PR workflows, should try to push to OCI registry + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + env: + TARGET: ${{ matrix.arch }} + run: uv run captain release publish + + build-captainos: # those use regular Debian apt repos for kernels + name: "build-captainos ${{ matrix.FLAVOR_ID }} (${{ matrix.arch }})" + steps: *build_steps + env: *build_env + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }} + needs: [ prepare, download-tools, build-dockerfile ] + strategy: + fail-fast: false + matrix: + include: + - { arch: amd64, output_arch: x86_64, captainos_kernel: false, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: true, FLAVOR_ID: "trixie-full" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-rockchip64-vendor" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-genio" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-meson64" } + - { arch: arm64, output_arch: aarch64, captainos_kernel: false, iso: false, FLAVOR_ID: "trixie-armbian-rpi" } # ------------------------------------------------------------------- # Publish combined multi-arch image (reuses per-arch registry blobs) # ------------------------------------------------------------------- publish-combined: if: github.ref == 'refs/heads/main' + strategy: + fail-fast: false + matrix: + include: + - { combined_flavor: "trixie-full" } + - { combined_flavor: "trixie-slim" } + name: "publish-combined ${{ matrix.combined_flavor }}" runs-on: ubuntu-latest - needs: [publish-per-arch] + needs: [ build-captainos, build-captainos-with-kernel ] # depend on all flavors being built env: + FLAVOR_ID: ${{ matrix.combined_flavor }} ARCH: amd64 TARGET: combined steps: - name: Checkout code uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Load shared config run: cat .github/config.env >> "$GITHUB_ENV" - - name: Download kernel artifacts (amd64) - uses: actions/download-artifact@v8 - with: - name: kernel-amd64 - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/amd64 - - name: Download initramfs artifacts (amd64) uses: actions/download-artifact@v8 with: - name: initramfs-amd64 + name: initramfs-${{ matrix.combined_flavor }}-amd64 path: out - name: Download ISO artifact (amd64) uses: actions/download-artifact@v8 with: - name: iso-amd64 + name: iso-${{ matrix.combined_flavor }}-amd64 path: out - - name: Download kernel artifacts (arm64) - uses: actions/download-artifact@v8 - with: - name: kernel-arm64 - path: mkosi.output/kernel/${{ env.KERNEL_VERSION }}/arm64 - - name: Download initramfs artifacts (arm64) uses: actions/download-artifact@v8 with: - name: initramfs-arm64 + name: initramfs-${{ matrix.combined_flavor }}-arm64 path: out - name: Download ISO artifact (arm64) uses: actions/download-artifact@v8 with: - name: iso-arm64 + name: iso-${{ matrix.combined_flavor }}-arm64 path: out - name: Install uv - uses: astral-sh/setup-uv@v8.1.0 + uses: astral-sh/setup-uv@v7 - name: Log in to GHCR uses: docker/login-action@v4 @@ -492,4 +409,6 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Publish combined image to GHCR - run: uv run ./build.py release publish + # only pushes to main, and not PR workflows, should try to push to OCI registry + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + run: uv run captain release publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e249c28..6bc2aaa4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,10 @@ permissions: contents: write packages: write +env: + FORCE_COLOR: "1" + CAPTAIN_VERBOSE: "1" + jobs: release: runs-on: ubuntu-latest @@ -30,21 +34,55 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v8.1.0 - - name: Pull release artifacts (combined) - env: - VERSION_EXCLUDE: ${{ github.ref_name }} - run: uv run ./build.py release pull --target combined --pull-output artifacts/combined + - name: Pull release artifacts + run: | + uv run captain --flavor-id trixie-full release --target combined pull --pull-output release-artifacts + uv run captain --flavor-id trixie-slim release --target combined pull --pull-output release-artifacts + uv run captain --flavor-id trixie-armbian-rpi release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-rockchip64 release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-meson64 release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-rockchip64-vendor release --target arm64 pull --pull-output release-artifacts + uv run captain --flavor-id trixie-genio release --target arm64 pull --pull-output release-artifacts + + - name: "directories -> tarball" + working-directory: out/release-artifacts + run: | + for dir in */; do + # if not a directory, skip + [[ ! -d "$dir" || "$dir" == "." || "$dir" == ".." ]] && continue + tarball="${dir%/}.tar.gz" + echo "Creating tarball for '$dir' -> '$tarball'" + tar -czf "${dir%/}.tar.gz" "$dir" + echo "Removing directory '$dir'" + rm -rf "$dir" + done - name: Create GitHub Release env: GH_TOKEN: ${{ github.token }} run: | gh release create "${{ github.ref_name }}" \ - artifacts/combined/* \ + out/release-artifacts/* \ --generate-notes \ --title "${{ github.ref_name }}" - - name: Tag OCI artifacts with version - env: - VERSION_EXCLUDE: ${{ github.ref_name }} - run: uv run ./build.py release tag ${{ github.ref_name }} + - name: Tag (trixie-full) + run: uv run captain --flavor-id trixie-full release tag --new-tag "${{ github.ref_name }}" + + - name: Tag (trixie-slim) + run: uv run captain --flavor-id trixie-slim release tag --new-tag "${{ github.ref_name }}-slim" + + - name: Tag (trixie-armbian-rpi) + run: uv run captain --flavor-id trixie-armbian-rpi --arch arm64 release tag --new-tag "${{ github.ref_name }}-rpi" + + - name: Tag (trixie-rockchip64) + run: uv run captain --flavor-id trixie-rockchip64 --arch arm64 release tag --new-tag "${{ github.ref_name }}-rockchip64" + + - name: Tag (trixie-meson64) + run: uv run captain --flavor-id trixie-meson64 --arch arm64 release tag --new-tag "${{ github.ref_name }}-meson64" + + - name: Tag (trixie-rockchip64-vendor) + run: uv run captain --flavor-id trixie-rockchip64-vendor --arch arm64 release tag --new-tag "${{ github.ref_name }}-rockchip64-vendor" + + - name: Tag (trixie-genio) + run: uv run captain --flavor-id trixie-genio --arch arm64 release tag --new-tag "${{ github.ref_name }}-genio" diff --git a/.gitignore b/.gitignore index 6781065c..8fb78ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,26 @@ # Build outputs mkosi.output/ +mkosi.input/ mkosi.cache/ mkosi.tools/ mkosi.tools.manifest mkosi.builddir/ -kernel.configs/.config.resolved.* out/ +# Generated artifacts +/mkosi.conf +/mkosi.finalize +/mkosi.postinst +/mkosi.extra +/mkosi.sandbox +/mkosi.skeleton + # Editor *.swp *.swo *~ .vscode/ +.idea/ # OS .DS_Store @@ -29,3 +38,6 @@ dist/ .mypy_cache/ .pyright/ .pytest_cache/ + +# Ignore uv.lock for now; we should always use the latest version of packages etc +uv.lock diff --git a/Dockerfile b/Dockerfile index 07061d59..ea532c9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,24 @@ # Builder container for CaptainOS using mkosi # Encapsulates all mkosi dependencies for reproducible builds. -# Usage: docker build -t captainos-builder . && docker run --rm --privileged -v $(pwd):/work captainos-builder build +# Includes skopeo and buildah for OCI image manipulation, and uv for Python tool management. FROM debian:trixie -# Pinned post-v26 to pick up systemd/mkosi@1f811f05 ("tools: move grub-pc-bin -# to arch-specific drop-in"), which fixes arm64 builds failing on the default -# tools-tree pulling in grub-pc-bin (BIOS GRUB, x86-only). Bump to a release -# tag once v27 lands. -ARG MKOSI_VERSION=1f811f0524be3096872e79161c8e6ab3e7c2bb1f - # Avoid interactive prompts -ENV DEBIAN_FRONTEND=noninteractive +ENV DEBIAN_FRONTEND=noninteractive BUILDAH_ISOLATION=chroot FORCE_COLOR=1 -# Install mkosi runtime dependencies and kernel build dependencies in one layer -RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ +# Add foreign architecture for cross-compilation (arm64 on amd64 and vice versa) and apt-update +# Immediately install the cross-arch grub dependencies +RUN <<-FRAGMENT_WITH_VARIABLES +# Determine arch/cross-arch and install grub and other basic packages (around 200mb layer) +NATIVE_ARCH="$(dpkg --print-architecture)" +FOREIGN_ARCH=$([ "$NATIVE_ARCH" = "amd64" ] && echo "arm64" || echo "amd64") +dpkg --add-architecture "$FOREIGN_ARCH" +apt-get -o "Dpkg::Use-Pty=0" update +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + "grub-efi-${NATIVE_ARCH}-bin" \ + "grub-efi-${FOREIGN_ARCH}-bin:${FOREIGN_ARCH}" \ + "libssl-dev:${FOREIGN_ARCH}" \ + grub-common \ apt \ dpkg \ debian-archive-keyring \ @@ -25,71 +30,125 @@ RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install systemd-container \ systemd \ udev \ - bubblewrap \ squashfs-tools \ mtools \ erofs-utils \ dosfstools \ e2fsprogs \ btrfs-progs \ - # Kernel build deps - build-essential \ - gcc \ - gcc-aarch64-linux-gnu \ + tree +FRAGMENT_WITH_VARIABLES + +# Cross-architecture support (arm64 on x86_64 and vice versa) - huge single package +RUN <<-QEMU_USER_FRAGMENT +# Install qemu-user but then delete all un-needed qemu binaries to save space (we only need aarch64 and x86_64) +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends qemu-user +echo 'All qemus: ' +ls -lah /usr/bin/qemu-* +# keep only qemu binary for the arches we're interested in: aarch64 and x86_64 +echo 'To be deleted: ' +find /usr/bin -name 'qemu-*' -not -name 'qemu-aarch64' -not -name 'qemu-x86_64' -not -name 'qemu-arm*' -not -name 'qemu-amd*' -print0 | xargs -0 ls -lah +echo 'Deleting: ' +find /usr/bin -name 'qemu-*' -not -name 'qemu-aarch64' -not -name 'qemu-x86_64' -not -name 'qemu-arm*' -not -name 'qemu-amd*' -print0 | xargs -0 rm -fv +echo 'Remaining: ' +ls -lah /usr/bin/qemu-* + +QEMU_USER_FRAGMENT + +# Extra kernel build tools +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ make \ flex \ bison \ bc \ libelf-dev \ libssl-dev \ + dpkg-dev \ dwarves \ - pahole \ + pahole + +# Those are pulled by build-essential (cross...), but are quite big; pull them ealier to balance layer size +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + binutils-common \ + libasan8 \ + liblsan0 \ + libubsan1 \ + libhwasan0 \ + binutils-x86-64-linux-gnu \ + libasan8-amd64-cross \ + liblsan0-amd64-cross \ + libtsan2-amd64-cross \ + libc6-amd64-cross \ + linux-libc-dev-amd64-cross \ + libc6-dev-amd64-cross + +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ rsync \ coreutils \ - # Cross-architecture support (arm64 on x86_64 and vice versa) - qemu-user-static \ - # Network tools (for fetching kernel source etc.) git \ curl \ ca-certificates \ - # Binary compression + qemu-user-static + +# Then both, of which one will already be fulfilled +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends crossbuild-essential-arm64 +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends crossbuild-essential-amd64 + +# Buildah and Skopeo +# Binary compression +# ISO image creation +# Kernel build deps: build-essential +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ + build-essential \ + containernetworking-plugins \ + bubblewrap \ + skopeo \ upx-ucl \ - # ISO image creation - xorriso \ - grub-common \ - && NATIVE_ARCH="$(dpkg --print-architecture)" \ - && FOREIGN_ARCH=$([ "$NATIVE_ARCH" = "amd64" ] && echo "arm64" || echo "amd64") \ - && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends "grub-efi-${NATIVE_ARCH}-bin" \ - && dpkg --add-architecture "$FOREIGN_ARCH" \ - && apt-get -o "Dpkg::Use-Pty=0" update \ - && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends "grub-efi-${FOREIGN_ARCH}-bin:${FOREIGN_ARCH}" \ - && rm -rf /var/lib/apt/lists/* + xorriso -# Install astral-sh's uv with a script - install to /usr for global access -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh +# Buildah is pretty huge, gets its own layer. +RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends buildah -# Verify uv is functional -RUN uv --version +# FYI: Rust stuff for kernel build is about 1.3gb +# RUN apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends rustc rust-src bindgen rustfmt rust-clippy -# Install mkosi from GitHub (not on PyPI) via uv; symlink to /usr/bin for global access -RUN uv tool install "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" -RUN ln -sf ~/.local/bin/mkosi /usr/bin/mkosi +RUN <<-EXTRA_FRAG +# Extra packages for mkosi and kernel builds -# Verify mkosi is functional -RUN mkosi --version +# This is just to appease mkosi's later stages, as it re-launches itself using the system Python +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends python3 python3-pip python3-pefile -# Install project dependencies into a persistent venv so that -# `uv run` inside the container reuses it instead of recreating one. -COPY pyproject.toml /opt/captain/pyproject.toml -COPY captain /opt/captain/captain -COPY build.py /opt/captain/build.py -RUN uv venv /opt/captain-venv && \ - VIRTUAL_ENV=/opt/captain-venv uv pip install --project /opt/captain /opt/captain +# For kernel's bindeb-pkg and menuconfig +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends debhelper libdw-dev lsb-release libncurses-dev + +# For mkosi using Content.PackageDirectories (creating local repo) +apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends apt-utils +EXTRA_FRAG + +RUN <<-CONFIG_FRAG +## A few small config fragments to make life easier +# git: Ignore owner mismatches in /work, which will be bind-mounted from the host +git config --global --add safe.directory /work \ +# buildah: Configure rootless storage driver and chroot isolation (no user-namespace required — we only assemble scratch images, never RUN anything inside them). +printf '[storage]\ndriver = "vfs"\nrunroot = "/var/tmp/buildah-runroot"\ngraphroot = "/var/tmp/buildah-storage"\n' > /etc/containers/storage.conf +# Buildah 1.39+ on Debian requires netavark but we never need networking +# (all images are FROM scratch with no RUN steps). A no-op stub satisfies +# the startup check. +mkdir -p /usr/libexec/podman +printf '#!/bin/sh\nexit 0\n' > /usr/libexec/podman/netavark +chmod +x /usr/libexec/podman/netavark +CONFIG_FRAG + +# Install astral-sh's uv with a script - install to /usr for global access +RUN echo -n 'System Python: ' && python3 --version && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh && uv --version -# Point uv at the pre-built venv for all future runs. -ENV VIRTUAL_ENV=/opt/captain-venv -ENV UV_PROJECT_ENVIRONMENT=/opt/captain-venv +# Install mkosi from GitHub (not on PyPI) via the system pip3 +ARG MKOSI_VERSION=v26 +RUN pip3 install --break-system-packages --root-user-action=ignore "git+https://github.com/systemd/mkosi.git@${MKOSI_VERSION}" && mkosi --version && command -v mkosi +# Prime uv's cache with our pyproject.toml to speed up runtime +COPY pyproject.toml /work/pyproject.toml +COPY captain /work/captain +COPY build.py /work/build.py WORKDIR /work -ENTRYPOINT ["mkosi"] -CMD ["build"] +RUN uv --verbose run captain --version diff --git a/Dockerfile.release b/Dockerfile.release deleted file mode 100644 index b63204cd..00000000 --- a/Dockerfile.release +++ /dev/null @@ -1,46 +0,0 @@ -# Lightweight container for OCI release operations (publish, index, pull, tag). -# -# Usage: -# docker build -f Dockerfile.release -t captainos-release . -# docker run --rm -v $(pwd):/work captainos-release release publish -FROM debian:trixie - -# Install buildah, skopeo, curl and git -RUN apt-get -o "Dpkg::Use-Pty=0" update && apt-get -o "Dpkg::Use-Pty=0" install -y --no-install-recommends \ - buildah \ - skopeo \ - curl \ - git \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* \ - && git config --global --add safe.directory /work - -# Configure rootless storage driver and chroot isolation (no user-namespace -# required — we only assemble scratch images, never RUN anything inside them). -RUN printf '[storage]\ndriver = "vfs"\nrunroot = "/var/tmp/buildah-runroot"\ngraphroot = "/var/tmp/buildah-storage"\n' \ - > /etc/containers/storage.conf -ENV BUILDAH_ISOLATION=chroot - -# Buildah 1.39+ on Debian requires netavark but we never need networking -# (all images are FROM scratch with no RUN steps). A no-op stub satisfies -# the startup check. -RUN mkdir -p /usr/libexec/podman \ - && printf '#!/bin/sh\nexit 0\n' > /usr/libexec/podman/netavark \ - && chmod +x /usr/libexec/podman/netavark - -# Install astral-sh's uv with a script - install to /usr for global access -RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/usr/bin" sh - -# Verify uv is functional -RUN uv --version - -# Prime uv's cache with our pyproject.toml to speed up runtime -COPY pyproject.toml /tmp/pyproject.toml -COPY captain /tmp/captain -COPY build.py /tmp/build.py -WORKDIR /tmp -RUN uv --verbose run build.py --help - -WORKDIR /work -ENTRYPOINT ["/usr/bin/uv", "run", "/work/build.py"] -CMD ["release", "--help"] diff --git a/README.md b/README.md index 00e2a5b5..baa42165 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,22 @@ A minimal, systemd-based in-memory OS for [Tinkerbell](https://tinkerbell.org) bare-metal provisioning. -CaptainOS boots via PXE/iPXE, runs entirely from RAM as a compressed CPIO initramfs, and provides a container runtime environment for the [tink-agent](https://github.com/tinkerbell/tinkerbell) — the component that drives hardware provisioning workflows. +CaptainOS boots via PXE/iPXE, runs entirely from RAM as a compressed CPIO initramfs, and provides a container runtime +environment for the [tink-agent](https://github.com/tinkerbell/tinkerbell) — the component that drives hardware +provisioning workflows. ## Why does CaptainOS exist? -CaptainOS is the next generation of Tinkerbell's in-memory OS, building on years of experience building, maintaining, and operating [HookOS](https://github.com/tinkerbell/hook) in production. -It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal systemd-based Debian OS that runs entirely from RAM. +CaptainOS is the next generation of Tinkerbell's in-memory OS, building on years of experience building, maintaining, +and operating [HookOS](https://github.com/tinkerbell/hook) in production. +It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal systemd-based Debian OS that runs +entirely from RAM. - **Significantly smaller initramfs** — small enough to boot comfortably on resource-constrained single-board computers. -- **No Docker-in-Docker** — tink-agent runs directly on the host with containerd, giving it native access to the container runtime without any nesting. -- **Familiar operations** — systemd foundation with journalctl, networkd, and standard service management make debugging and troubleshooting straightforward. +- **No Docker-in-Docker** — tink-agent runs directly on the host with containerd, giving it native access to the + container runtime without any nesting. +- **Familiar operations** — systemd foundation with journalctl, networkd, and standard service management make debugging + and troubleshooting straightforward. - **Simpler architecture** — fewer layers between hardware and workload, easier to develop against and extend. ## How it works @@ -19,237 +25,175 @@ It is built with [mkosi](https://github.com/systemd/mkosi), producing a minimal 1. The machine PXE boots the kernel (`vmlinuz`) and initramfs (`initramfs`) or runs the UEFI-bootable ISO image 2. A custom `/init` script transitions the rootfs to tmpfs, then exec's systemd 3. systemd-networkd configures DHCP on all ethernet interfaces -4. containerd starts, then `tink-agent-setup` pulls the tink-agent container image (configured via kernel cmdline), extracts the binary, and runs it as a host process +4. containerd starts, then `tink-agent-setup` pulls the tink-agent container image (configured via kernel cmdline), + extracts the binary, and runs it as a host process 5. tink-agent connects to the Tinkerbell server and executes provisioning workflows -## Architecture +## Build model -The build has four stages: +The current CLI is click-based (`captain`) with flavor-driven image generation. -1. **Kernel compilation** (`./build.py kernel`) — builds a Linux kernel from source using defconfigs from `kernel.configs/` -2. **Tool download** (`./build.py tools`) — fetches pinned binary releases of the container runtime stack -3. **Initramfs build** (`./build.py initramfs`) — assembles a Debian Trixie CPIO initramfs with systemd, injecting the kernel, modules, and tools using `mkosi` -4. **ISO assembly** (`./build.py iso`) — builds a UEFI-bootable ISO with GRUB via `grub-mkrescue` +- `captain build` runs: **tools -> initramfs (mkosi) -> iso (if flavor supports it) -> collect artifacts** +- `captain kernel` is a separate stage used when building kernel artifacts directly (for example `trixie-slim` + workflows) +- Flavor templates and static overlays are rendered into generated `mkosi.*` files +- `mkosi.output/` keeps hashed per-flavor build outputs; `out/` gets stable publishable filenames -## Usage +## Build system Usage -**Prerequisites:** `uv` (Python), Docker. +**Prerequisites:** `uv` (Python), Docker (with buildx/BuildKit). + +- On Debian, the regular Debian-supplied `docker.io` and `docker-cli`/`docker-buildx` packages work fine. You can also + use Docker CE from Docker Inc if you prefer. +- On MacOS with brew, `colima` is a good alternative to Docker Desktop or similar. ```bash # Install Astral's `uv` if you don't have it: https://docs.astral.sh/uv/getting-started/installation/ curl -LsSf https://astral.sh/uv/install.sh | sh # then re-log in -# Build with defaults (amd64, kernel 6.18.16) -uv run ./build.py --help - -usage: build.py [flags] - -Build CaptainOS images. Stages: kernel → tools → initramfs → iso. - -options: - -h, --help show this help message and exit - -build configuration: - --arch {amd64,arm64} target architecture (default: amd64) - --builder-image IMAGE Docker builder image name (default: captainos-builder) - --no-cache rebuild builder image without Docker cache +# top-level help +uv run captain --help +``` -kernel: - --kernel-version VER kernel version to build (default: 6.18.16) - --kernel-src PATH path to local kernel source tree - --kernel-mode {docker,native,skip} kernel stage execution mode (default: docker) - --force-kernel force kernel rebuild even if outputs exist +### Core commands -tools: - --tools-mode {docker,native,skip} tools stage execution mode (default: docker) - --force-tools re-download tools even if outputs exist +**Important**: please pay attention to the level flags are defined. For example, `--flavor-id` is a global option and +needs to be passed after `uv run captain` but before the subcommand (`build`, `kernel`, etc). +We're investigating ways to make this more flexible using Python's Click library, but for now please follow the +documented usage. -initramfs (mkosi): - --mkosi-mode {docker,native,skip} mkosi stage execution mode (default: docker) - --force passed through to mkosi as --force +```bash +uv run captain builder --help +uv run captain build --help +uv run captain kernel --help +uv run captain tools --help +uv run captain iso --help +uv run captain qemu --help +uv run captain release --help +uv run captain shell --help +``` -iso: - --iso-mode {docker,native,skip} iso stage execution mode (default: docker) - --force-iso force ISO rebuild even if outputs exist +## Flavors -commands: - build Run all build stages: kernel → tools → initramfs → iso (default) - kernel Build only the kernel + modules - tools Download tools (containerd, runc, nerdctl, CNI) - initramfs Build only the initramfs via mkosi - iso Build a UEFI-bootable ISO image - checksums Compute SHA-256 checksums for specified files - shell Interactive shell inside the builder container - clean Remove all build artifacts - summary Print mkosi configuration summary - qemu-test Boot the image in QEMU for testing +Available flavors are discovered dynamically from `captain/flavors/`. Here is a current list: -``` +| Flavor | Architectures | ISO | Description | +|----------------------------|------------------|-----|-----------------------------------------------------------------------------------------------------------------------| +| `trixie-full` | `amd64`, `arm64` | yes | Debian Trixie, standard Debian linux-generic kernel, working `apt` package manager | +| `trixie-slim` | `amd64`, `arm64` | yes | Kernel built from source, slimmed down Debian userspace, ACPI/UEFI only | +| `trixie-armbian-rpi` | `arm64` | no | For RaspberryPi's, using Armbian's bcm2711 kernel; produces `rpi-firmware` which is ready-to-netboot on RPi's | +| `trixie-meson64` | `arm64` | no | For Amlogic boards, using Armbian's meson64 kernel | +| `trixie-rockchip64` | `arm64` | no | For Rockchip boards (rk35xx, rk33xx, etc) using Armbian's _mainline_ rockchip64 kernel | +| `trixie-rockchip64-vendor` | `arm64` | no | For Rockchip boards (rk35xx) using the Rockchip vendor kernel from Armbian. Prefer the non-vendor flavor if possible. | +| `trixie-genio` | `arm64` | no | For MediaTek Genioboards, using Armbian's genio kernel | -Output artifacts are placed in `out/`: +Use `--flavor-id` (or environment `FLAVOR_ID`) to select one. -- `out/vmlinuz--` — the kernel -- `out/initramfs--` — the initramfs -- `out/captainos--.iso` — UEFI-bootable ISO image -- `out/sha256sums--.txt` — SHA-256 checksums +## Build modes -Here `` is the kernel version (e.g. `6.18.16`) and `` is the Linux architecture name (`x86_64` or `aarch64`). +Most stages support `docker | native | skip`: -## Release +- `--tools-mode` / `TOOLS_MODE` +- `--mkosi-mode` / `MKOSI_MODE` +- `--iso-mode` / `ISO_MODE` +- `--kernel-mode` / `KERNEL_MODE` +- `captain release --release-mode` / `RELEASE_MODE` -CI publishes build artifacts as OCI images on every push to `main`. Pushing a version tag (`v*`) creates a GitHub Release with downloadable files and tags the OCI images with the release version. +Defaults are command-specific (for example, `captain build` defaults to `--tools-mode native`, while `captain tools` +defaults to `--tools-mode docker`). Use `--help` on each command for current defaults. -### OCI artifact images +## Customization via `custom/` -Three multi-arch OCI indexes are published per build: +The `custom/` directory is mounted automatically in docker-mode runs and applied by Debian-based flavors. -| Image | Tag | Contents | -| --- | --- | --- | -| amd64-only | `vX.Y.Z--amd64` | vmlinuz, initramfs, ISO, checksums (x86_64) | -| arm64-only | `vX.Y.Z--arm64` | vmlinuz, initramfs, ISO, checksums (aarch64) | -| combined | `vX.Y.Z-` | all artifacts from both architectures | +- `custom/packages.txt`: one package per line (comments allowed) +- `custom/files/`: copied into the image (mapped under `mkosi.extra/`) +- `custom/mkosi-postinst/`: appended scripts for post-install phase +- `custom/mkosi-finalize/`: appended scripts for finalize phase -Each artifact file is pushed as its own OCI layer. Deterministic tar creation (zeroed metadata) ensures identical layer digests across per-arch and combined images, so registries deduplicate shared blobs — the combined image adds zero additional storage. +See examples under `custom/` in this repository. -All three images are multi-arch OCI indexes with `linux/amd64` and `linux/arm64` platform entries pointing to the same content, so any platform can pull them. Images are compatible with: +## Output artifacts -- **containerd** — valid `rootfs.diff_ids` in the config; Kubernetes image-volume mounts work -- **skopeo** — extracts individual artifact files for release workflows +Final artifacts are copied to `out/` with flavor-aware names: -### GitHub Release +- `out/vmlinuz--` +- `out/initramfs--` +- `out/captainos--.iso` (only for ISO-capable flavors) +- `out/dtb--/` (directory, arm64 flavors that export DTBs) -When a `v*` tag is pushed, the release workflow: +`` uses Linux output architecture names (`x86_64`, `aarch64`). -1. Pulls the combined OCI image (both architectures) -2. Attaches all artifacts as downloadable files on the GitHub Release page: - - `vmlinuz--x86_64`, `initramfs--x86_64`, `captainos--x86_64.iso`, `sha256sums--x86_64.txt` - - `vmlinuz--aarch64`, `initramfs--aarch64`, `captainos--aarch64.iso`, `sha256sums--aarch64.txt` -3. Tags all three OCI images with the clean release version (`vX.Y.Z`, `vX.Y.Z-amd64`, `vX.Y.Z-arm64`) +## Release (OCI artifacts) -### Release subcommands +Release operations are grouped under `captain release`: ```bash -# Publish artifacts as a multi-arch OCI image -./build.py release publish --target amd64 +# publish +uv run captain release --target amd64 publish -# Pull and extract artifacts -./build.py release pull --target combined --pull-output ./out/release/ +# pull artifacts into out/release/ +uv run captain release --target combined pull --pull-output release -# Tag all artifact images with a release version -./build.py release tag v1.0.0 +# retag existing images +uv run captain release --src-tag v0.0.0-abcdef1-trixie-full tag --new-tag v1.0.0 ``` -Run `./build.py release -h` for full flag reference. +Notes: -## Build modes +- Base source tag defaults to `v0.0.0--`. +- Per-arch publish refs append `-amd64` or `-arm64`; combined uses the bare source tag. +- `publish` computes `sha256sums--.txt` and includes checksums as OCI layers. +- `REGISTRY_INSECURE=1` can be used for insecure registries (propagated to buildah/skopeo wrappers). -Each stage can be executed in one of three modes: +For the rationale behind the Buildah + Skopeo approach, see `docs/design-decisions/oci-tooling-buildah-skopeo.md`. -- `docker` (default) — runs the stage inside a Docker container, providing a consistent build environment regardless of host OS. -- `native` — runs the stage directly on the host machine. Requires all build dependencies to be installed and configured correctly. -- `skip` — skips the stage entirely. +## Testing with QEMU + +`captain qemu` boots artifacts from `out/` and forwards additional arguments to the kernel cmdline. -### Setting modes +```bash +# boot with defaults +uv run captain --arch amd64 --flavor-id trixie-full qemu -| Mode | CLI flag | Env var | Example | -| --- | --- | --- | --- | -| `docker` | `--{stage}-mode docker` | `{STAGE}_MODE=docker` | `--kernel-mode docker` | -| `native` | `--{stage}-mode native` | `{STAGE}_MODE=native` | `--tools-mode native` | -| `skip` | `--{stage}-mode skip` | `{STAGE}_MODE=skip` | `--iso-mode skip` | +# pass extra kernel cmdline args after -- +uv run captain --arch amd64 --flavor-id trixie-full qemu -- tink_worker_image=ghcr.io/tinkerbell/tink-agent:latest +``` -### Included tools +You will need OVMF firmware for the architecture installed on the host system: -| Component | Version | Purpose | -| --- | --- | --- | -| containerd | 2.2.1 | Container runtime | -| nerdctl | 2.2.1 | Container CLI (Docker-compatible) | -| runc | 1.4.0 | OCI runtime | -| CNI plugins | 1.6.0 | Container networking (bridge, host-local, loopback, portmap, firewall, tuning) | +- On MacOS with brew, OVMF builds for both x86 and arm64 is included with the `qemu` package. +- On Linux, you can install `ovmf` from your package manager: + - Debian: x86: `ovmf`, arm64: `qemu-efi-aarch64` + - Fedora: `edk2-ovmf` (both architectures) -### Key design decisions +On native-host architecture runs, acceleration is enabled when available (`hvf` on macOS, `kvm` on Linux). -- **Custom `/init` instead of `MakeInitrd`** — systemd's initrd mode expects to switch-root to a real rootfs. CaptainOS runs entirely from RAM, so a custom init transitions rootfs → tmpfs before exec'ing systemd. This makes `pivot_root(2)` work for container runtimes. -- **No UPX compression** — the final image is compressed with zstd level 19. Raw ELF binaries compress better under zstd than UPX-packed ones (UPX output looks like random data to zstd). -- **iptables-nft backend** — uses the nftables-backed iptables for container networking, with the necessary `CONFIG_NF_TABLES_*` kernel options enabled. -- **IP forwarding via sysctl** — enabled at boot for container network traffic. +Exit QEMU with `Ctrl-a x` (that's `Ctrl` + `a`, release, then press `x`). ## Project layout -```bash +```text . -├── build.py # Main build entry point (Python >= 3.13; use `uv run build.py`) -├── captain/ # Build system package (stdlib only) -│ ├── __init__.py # Package init incl logging -│ ├── cli.py # CLI subcommands (argparse) -│ ├── config.py # Configuration from environment -│ ├── docker.py # Docker builder management -│ ├── kernel.py # Kernel compilation logic -│ ├── tools.py # Binary tool downloader -│ ├── artifacts.py # Artifact collection & checksums -│ ├── oci.py # OCI artifact publish/pull/tag -│ ├── buildah.py # buildah CLI wrapper (image construction) -│ ├── skopeo.py # skopeo CLI wrapper (inspect/copy/export) -│ ├── iso.py # ISO image assembly -│ ├── qemu.py # QEMU boot testing -│ └── util.py # Shared helpers & arch mapping -├── Dockerfile # Builder container definition -├── Dockerfile.release # Lightweight container for OCI release ops -├── mkosi.conf # mkosi image configuration -├── mkosi.postinst # Post-install hooks (symlinks, cleanup) -├── mkosi.finalize # Final image adjustments -├── config/ -│ ├── defconfig.amd64 # Kernel config for x86_64 -│ └── defconfig.arm64 # Kernel config for aarch64 -└── mkosi.extra/ # Files overlaid into the image - ├── init # Custom PID 1 (rootfs → tmpfs → systemd) - └── etc/ - ├── containerd/ # containerd configuration - ├── systemd/system/ # systemd units - ├── acpi/ # ACPI power button handler - ├── sysctl.d/ # Kernel tunables - └── os-release # OS identification +|- build.py +|- captain/ +| |- cli/ # click CLI groups/commands +| |- flavors/ # flavor definitions and mkosi overlays +| |- oci/ # OCI publish/pull/tag logic +| |- artifacts.py # copy outputs into out/ +| |- docker.py # builder image + docker relaunch helpers +| |- iso.py # ISO assembly +| |- kernel.py # kernel build logic +| |- qemu.py # qemu launcher +| `- tools.py # pinned tool downloads +|- custom/ # user customizations +|- docs/ # documentation +|- kernel.configs/ # kernel configuration (defconfig) for trixie-slim kernel builds +|- mkosi.input/ # build inputs (generated and cached) +|- mkosi.output/ # per-flavor hashed mkosi outputs +`- out/ # final artifacts for release/use ``` -## Testing with QEMU - -```bash -./build.py qemu-test -h - -usage: build.py qemu-test [flags] - -Boot the image in QEMU for testing - -options: - -h, --help show this help message and exit - -build configuration: - --arch {amd64,arm64} target architecture (default: amd64) - --builder-image IMAGE Docker builder image name (default: captainos-builder) - --no-cache rebuild builder image without Docker cache - -qemu: - --qemu-append ARGS extra kernel cmdline args for qemu-test - --qemu-mem SIZE QEMU RAM size (default: 2G) - --qemu-smp N QEMU CPU count (default: 2) - -tinkerbell: - --tink-worker-image IMAGE tink-agent container image reference (default: ghcr.io/tinkerbell/tink- - agent:latest) - --tink-docker-registry HOST registry host (triggers tink-agent services) - --tink-grpc-authority ADDR tink-server gRPC endpoint (host:port) - --tink-worker-id ID machine / worker ID - --tink-tls BOOL enable TLS to tink-server (default: false) - --tink-insecure-tls BOOL allow insecure TLS (default: true) - --tink-insecure-registries LIST comma-separated insecure registries - --tink-registry-username USER registry auth username - --tink-registry-password PASS registry auth password - --tink-syslog-host HOST remote syslog host - --tink-facility CODE facility code - --ipam PARAM static networking IPAM parameter -``` - -This boots the image in QEMU with a virtio NIC and serial console. `console=ttyS0 audit=0` is always appended to the kernel cmdline. Press `Ctrl-A X` to exit or run `poweroff` inside the VM. - ## License See [Tinkerbell](https://github.com/tinkerbell/captain/blob/main/LICENSE) for license information. diff --git a/build.py b/build.py index df5324fc..2375ec03 100755 --- a/build.py +++ b/build.py @@ -1,10 +1,12 @@ #!/usr/bin/env python3 -"""CaptainOS build system entry point. +"""CaptainOS build system — click-based CLI entry point. -Requires: Python >= 3.13, Rich, Docker (unless all stages use native or skip). -It is recommended to use Astral's uv to run this script, which will automatically - install dependencies in an isolated environment. -This will automatically use uv to re-launch itself when stages use Docker. +Requires: Python >= 3.13 and a lot of dependencies; Use Astral's ``uv`` to run:: + + uv run build.py --help + uv run build.py builder + uv run build.py build --arch arm64 + uv run build.py release-publish --target combined """ import sys diff --git a/captain/__init__.py b/captain/__init__.py index 0c2b4290..d9d8362f 100644 --- a/captain/__init__.py +++ b/captain/__init__.py @@ -8,46 +8,45 @@ import logging import os +import shutil +import click from rich.console import Console from rich.logging import RichHandler from rich.traceback import install as _install_rich_traceback -# Rich console — writes to stderr so log output never pollutes piped stdout. -# If running under GHA, force colors. -if os.environ.get("GITHUB_ACTIONS", "") == "": - console = Console(stderr=True) -else: - console = Console(stderr=True, color_system="standard", width=160, highlight=False) +# Obtain terminal width +env_columns = shutil.get_terminal_size(fallback=(161, 24)).columns +# Rich console — writes to stderr so log output never pollutes piped stdout. # Install Rich traceback handler globally (once, at import time). -_install_rich_traceback(console=console, show_locals=True, width=None) +if os.environ.get("FORCE_COLOR", "0") == "1": + console: Console = Console(stderr=True, color_system="standard", width=env_columns) + _install_rich_traceback(console=console, show_locals=False, width=env_columns, suppress=[click]) +else: + console: Console = Console(stderr=True) + _install_rich_traceback(console=console, show_locals=False, width=None, suppress=[click]) class _StageFormatter(logging.Formatter): - """Show the module path relative to the ``captain`` package as a prefix.""" - def format(self, record: logging.LogRecord) -> str: - name = record.name - stage = name.split(".", 1)[1] if name.startswith("captain.") else name - record.__dict__["stage"] = stage + name = record.name.replace("captain.", "") + record.__dict__["stage"] = name + if os.environ.get("CAPTAIN_IN_DOCKER", "") == "docker": + record.__dict__["stage"] = f"🐳 {name}" return super().format(record) -# Configure the ``captain`` logger hierarchy once. -_root = logging.getLogger("captain") - -if not _root.handlers: - _handler = RichHandler( - console=console, - show_time=False, - show_level=True, - show_path=False, - markup=False, - rich_tracebacks=True, - tracebacks_show_locals=True, - ) - _handler.setFormatter(_StageFormatter("[%(stage)s] %(message)s")) - _root.addHandler(_handler) - _root.setLevel(logging.DEBUG) - _root.propagate = False +_handler = RichHandler( + console=console, + show_time=False, + show_level=True, + show_path=True, + markup=True, # interprets [braket]stuff[/bracket] in log messages, beware + rich_tracebacks=True, + tracebacks_show_locals=False, + tracebacks_suppress=[click], # don't wanna see click infra code in traces +) +_handler.setFormatter(_StageFormatter("[bold][cyan]%(stage)s[/cyan][/bold]: %(message)s")) + +logging.basicConfig(level="INFO", datefmt="[%X]", handlers=[_handler]) diff --git a/captain/armbian.py b/captain/armbian.py new file mode 100644 index 00000000..046fd5c5 --- /dev/null +++ b/captain/armbian.py @@ -0,0 +1,49 @@ +"""Helpers for the Armbian-next apt repository. + +Armbian flavors pull stable meta-package names (``linux-image-edge-rockchip64`` +etc.) whose underlying kernel version changes whenever armbian-next republishes. +The repo's ``Release`` file ETag is a cheap proxy for "the repo changed": folding +it into the flavor hash makes CI rebuild (and ship the newer kernel) instead of +reusing a stale cached build. +""" + +from __future__ import annotations + +import hashlib +import logging +import urllib.error +import urllib.request + +log = logging.getLogger(__name__) + +# The `armbian` dist (where the linux-image kernel packages live). A single +# repo-wide token from this Release file applies to all armbian flavors. +ARMBIAN_NEXT_RELEASE_URL = "https://apt-test.next.armbian.com/dists/armbian/Release" + +_FETCH_TIMEOUT = 30 # seconds + + +def release_etag(url: str = ARMBIAN_NEXT_RELEASE_URL) -> str: + """Return a short, stable cache-bust token from *url*'s ETag response header. + + Returns ``""`` on any failure or when the server sends no ETag — an empty + token means "no cache-bust", preserving current behaviour. + """ + try: + req = urllib.request.Request(url) + with urllib.request.urlopen(req, timeout=_FETCH_TIMEOUT) as resp: + etag = resp.headers.get("ETag", "") + except (urllib.error.URLError, OSError) as exc: + log.warning("Could not fetch ETag from %s (%s); no armbian cache-bust", url, exc) + return "" + + # Normalise weak-validator prefix and surrounding quotes, then hash to a + # fixed-width token that is safe to embed in filenames / cache keys. + etag = etag.strip().removeprefix("W/").strip('"') + if not etag: + log.warning("No ETag header from %s; no armbian cache-bust", url) + return "" + + token = hashlib.sha256(etag.encode()).hexdigest()[:16] + log.info("Armbian repo Release ETag %r → cache-bust token %s", etag, token) + return token diff --git a/captain/artifacts.py b/captain/artifacts.py index 1a1c769d..d962e368 100644 --- a/captain/artifacts.py +++ b/captain/artifacts.py @@ -5,6 +5,8 @@ import hashlib import logging import shutil +from dataclasses import dataclass +from enum import StrEnum from pathlib import Path from captain.config import Config @@ -32,17 +34,19 @@ def _human_size(size: int) -> str: def collect_kernel(cfg: Config) -> None: - """Copy the kernel image from mkosi.output/kernel/{version}/{arch}/ to out/.""" + """Copy the kernel image produced by mkosi.""" out = ensure_dir(cfg.output_dir) - vmlinuz_dir = cfg.kernel_output - vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - if vmlinuz_files: - vmlinuz_src = vmlinuz_files[0] - vmlinuz_dst = out / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + log.debug("Looking for kernel image produced by mkosi in %s", cfg.initramfs_output) + vmlinu_files = sorted(cfg.initramfs_output.glob("*.vmlinu*")) + if vmlinu_files: + vmlinuz_src = vmlinu_files[0] + vmlinuz_dst = out / f"vmlinuz-{cfg.flavor_id}-{cfg.arch_info.output_arch}" shutil.copy2(vmlinuz_src, vmlinuz_dst) - log.info("kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size)) + log.info( + "mkosi supplied kernel: %s (%s)", vmlinuz_dst, _human_size(vmlinuz_dst.stat().st_size) + ) else: - log.warning("No kernel image found in %s", cfg.kernel_output) + log.error("No kernel image produced by mkosi in %s", cfg.initramfs_output) def collect_initramfs(cfg: Config) -> None: @@ -51,13 +55,29 @@ def collect_initramfs(cfg: Config) -> None: cpio_files = sorted(cfg.initramfs_output.glob("*.cpio*")) if cpio_files: initrd_src = cpio_files[0] - initrd_dst = out / f"initramfs-{cfg.kernel_version}-{cfg.arch_info.output_arch}" + initrd_dst = out / f"initramfs-{cfg.flavor_id}-{cfg.arch_info.output_arch}" shutil.copy2(initrd_src, initrd_dst) log.info("initramfs: %s (%s)", initrd_dst, _human_size(initrd_dst.stat().st_size)) else: log.warning("No initramfs CPIO found in %s", cfg.initramfs_output) +def collect_dtbs(cfg): + """Collect the dtb directory produced by mkosi's finalize script.""" + indir = ensure_dir(cfg.initramfs_output) + dtb_dir: Path = indir / "dtb" + if dtb_dir.exists(): + log.info("Found dtb directory in %s, copying to output...", dtb_dir) + out = ensure_dir(cfg.output_dir) + target_dtb_dir = out / f"dtb-{cfg.flavor_id}-{cfg.arch_info.output_arch}" + if target_dtb_dir.exists(): + shutil.rmtree(target_dtb_dir) + shutil.copytree(dtb_dir, target_dtb_dir) + log.info("Copied dtb directory: %s", target_dtb_dir) + else: + log.warning("No dtb directory found in %s", dtb_dir) + + def collect_iso(cfg: Config) -> None: """Copy the ISO image from mkosi.output/iso/{arch}/ to out/.""" out = ensure_dir(cfg.output_dir) @@ -65,7 +85,7 @@ def collect_iso(cfg: Config) -> None: iso_files = sorted(iso_dir.glob("*.iso")) if iso_dir.is_dir() else [] if iso_files: iso_src = iso_files[0] - iso_dst = out / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_dst = out / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" shutil.copy2(iso_src, iso_dst) log.info("iso: %s (%s)", iso_dst, _human_size(iso_dst.stat().st_size)) @@ -114,3 +134,15 @@ def collect(cfg: Config) -> None: collect_initramfs(cfg) collect_kernel(cfg) collect_iso(cfg) + + +class OutputArchArtifactType(StrEnum): + FILE = "file" + DIRECTORY = "directory" + + +@dataclass +class OutputArchArtifact: + # type is either File or Directory + type: OutputArchArtifactType + name: str diff --git a/captain/buildah.py b/captain/buildah.py index cbc3110e..ff400da0 100644 --- a/captain/buildah.py +++ b/captain/buildah.py @@ -13,6 +13,7 @@ from __future__ import annotations import logging +import os from pathlib import Path from captain.util import run @@ -25,13 +26,23 @@ def from_image( *, platform: str | None = None, ) -> str: - cmd: list[str] = ["buildah", "from"] + cmd: list[str] = [ + "buildah", + "from", + *(["--tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), + ] if platform: cmd += ["--platform", platform] cmd.append(image) - log.info("buildah from %s", image) + log.info("buildah from %s (platform %s)", image, platform) result = run(cmd, capture=True) - return result.stdout.strip() + + imageid = result.stdout.strip() + # Use buildah config to strip the annotation added by buildah from + log.debug("Stripping digest annotation from base image %s", imageid) + run(["buildah", "config", "--annotation", "org.opencontainers.image.base.digest=", imageid]) + + return imageid def add( @@ -73,7 +84,7 @@ def commit( timestamp: int | None = None, ) -> str: log.info("buildah commit %s", container) - cmd: list[str] = ["buildah", "commit", "--rm"] + cmd: list[str] = ["buildah", "commit", "--rm", "--omit-history=true"] if timestamp is not None: cmd += ["--timestamp", str(timestamp)] cmd.append(container) @@ -91,9 +102,22 @@ def push( def manifest_create( ref: str, + oci_metadata: dict[str, str], ) -> str: log.info("buildah manifest create %s", ref) - result = run(["buildah", "manifest", "create", ref], capture=True) + cmd = ["buildah", "manifest", "create"] + for key, value in oci_metadata.items(): + cmd += ["--annotation", f"{key}={value}"] + cmd += [ref] + result = run(cmd, capture=True, check=False) + # handle and show both stdout and stderr on failure + if result.returncode != 0: + log.error( + "buildah manifest create failed: stdout: '%s' stderr: '%s'", + result.stdout.strip(), + result.stderr.strip(), + ) + raise RuntimeError("buildah manifest create failed") return result.stdout.strip() @@ -119,7 +143,17 @@ def manifest_push( dest: str, ) -> None: log.info("buildah manifest push → %s", dest) - run(["buildah", "manifest", "push", "--all", manifest, f"docker://{dest}"]) + run( + [ + "buildah", + "manifest", + "push", + *(["--tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), + "--all", + manifest, + f"docker://{dest}", + ] + ) def rmi( diff --git a/captain/cli/__init__.py b/captain/cli/__init__.py index fa440c15..442221bd 100644 --- a/captain/cli/__init__.py +++ b/captain/cli/__init__.py @@ -1,22 +1,20 @@ -"""CLI entry point — single configargparse parser with pre-extracted subcommand. +"""Click-based CLI for CaptainOS. -Every configuration parameter is both a ``--cli-flag`` and an environment -variable, following the ff priority model: +Provides the ``captain`` console script with subcommands: - CLI args > environment variables > defaults +- ``builder`` — build the Docker builder image, optionally push it +- ``build`` — full build pipeline (tools → initramfs → iso → artifacts) +- ``release-publish`` — publish artifacts as OCI images via buildah -The subcommand (``build``, ``kernel``, ``tools``, …) is extracted from -``sys.argv`` *before* parsing so that flags work in any position:: +Shell completion is available for bash and zsh:: - ./build.py --arch=arm64 kernel # works - ./build.py kernel --arch=arm64 # also works - ARCH=arm64 ./build.py kernel # also works + # bash + eval "$(_CAPTAIN_COMPLETE=bash_source captain)" + + # zsh + eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" """ from captain.cli._main import main -from captain.cli._parser import COMMANDS -__all__ = [ - "COMMANDS", - "main", -] +__all__ = ["main"] diff --git a/captain/cli/_build.py b/captain/cli/_build.py new file mode 100644 index 00000000..080aba7c --- /dev/null +++ b/captain/cli/_build.py @@ -0,0 +1,146 @@ +"""``captain build`` — full build pipeline (tools → initramfs → iso → artifacts).""" + +from __future__ import annotations + +import logging + +import click + +import captain.flavor +from captain import artifacts +from captain.cli._main import CliContext, cli +from captain.cli.stages import ( + _build_iso_stage, + _build_mkosi_stage, + _build_tools_stage, + _prune_stale_output_dirs, +) + +log = logging.getLogger(__name__) + + +@cli.command( + "build", + short_help="Run the full build pipeline via mkosi.", +) +@click.option( + "--mkosi-mode", + envvar="MKOSI_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Mkosi stage execution mode (docker, native, skip).", +) +@click.option( + "--tools-mode", + envvar="TOOLS_MODE", + default="native", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Tools download stage execution mode (docker, native, skip).", +) +@click.option( + "--iso-mode", + envvar="ISO_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="ISO build stage execution mode (docker, native, skip).", +) +@click.option( + "--force", + "force_mkosi", + is_flag=True, + default=False, + help="Pass --force through to mkosi.", +) +@click.option( + "--force-tools", + envvar="FORCE_TOOLS", + is_flag=True, + default=False, + help="Re-download tools even if outputs already exist.", +) +@click.option( + "--force-iso", + envvar="FORCE_ISO", + is_flag=True, + default=False, + help="Force ISO rebuild even if outputs already exist.", +) +@click.pass_obj +def build_cmd( + cli_ctx: CliContext, + *, + mkosi_mode: str, + tools_mode: str, + iso_mode: str, + force_mkosi: bool, + force_tools: bool, + force_iso: bool, +) -> None: + """Run the full CaptainOS build pipeline. + + Stages executed in order: tools → initramfs (mkosi) → ISO → artifact + collection. Each stage can be independently set to run inside Docker, + natively on the host, or skipped entirely. + + \b + Examples + -------- + captain build + captain build --arch arm64 + captain build --flavor-id trixie-meson64 --arch arm64 + captain build --mkosi-mode native --tools-mode native + captain build --force --force-tools + """ + + cfg = cli_ctx.make_config( + tools_mode=tools_mode, + mkosi_mode=mkosi_mode, + iso_mode=iso_mode, + force_tools=force_tools, + force_iso=force_iso, + ) + + if force_mkosi: + cfg.mkosi_args = ["--force"] + + # Instantiate and generate the flavor. + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + flavor.generate() + + # Tools stage. + _build_tools_stage(cfg) + + # Delegate to flavor, for eg obtaining it's own dependencies. + flavor.pre_mkosi_stage() + + # Initramfs (mkosi) stage + artifact collection. + _build_mkosi_stage(cfg, list(cfg.mkosi_args)) + artifacts.collect_initramfs(cfg) + artifacts.collect_kernel(cfg) + artifacts.collect_dtbs(cfg) + log.info("Initramfs build complete.") + + flavor.post_mkosi_stage() + + # ISO stage (if the flavor supports it). + if flavor.has_iso(): + _build_iso_stage(cfg) + else: + log.info("Flavor '%s' does not produce an ISO — skipping.", flavor.id) + + # Final artifact collection. + artifacts.collect(cfg) + + # Flavor-driven artifact collection. + flavor.post_artifact_collect() + + # Keep caches/artifacts lean when asked (CI). + _prune_stale_output_dirs(cfg) + + log.info("Build complete!") diff --git a/captain/cli/_builder.py b/captain/cli/_builder.py new file mode 100644 index 00000000..8e8d3ea2 --- /dev/null +++ b/captain/cli/_builder.py @@ -0,0 +1,47 @@ +"""``captain builder`` — build (and optionally push) the Docker builder image.""" + +from __future__ import annotations + +import logging + +import click + +from captain.cli._main import CliContext, cli +from captain.docker import obtain_builder + +log = logging.getLogger(__name__) + + +@cli.command( + "builder", + short_help="Build the Docker builder image and optionally push it.", +) +@click.option( + "--push", + is_flag=True, + default=False, + help="Push the built image to a registry after building.", +) +@click.pass_obj +def builder_cmd( + cli_ctx: CliContext, + *, + push: bool, +) -> None: + """Build the Docker builder image used by other build stages. + + By default the image is built locally only. Pass --push to also push the image to a + remote container registry. + + \b + Examples + -------- + captain builder + captain builder --no-cache + captain builder --push + """ + cfg = cli_ctx.make_config(builder_push=push) + + # 1. Build the image. + obtain_builder(cfg) + log.info("Builder image '%s' is ready.", cfg.builder_image) diff --git a/captain/cli/_commands.py b/captain/cli/_commands.py deleted file mode 100644 index 576b51f2..00000000 --- a/captain/cli/_commands.py +++ /dev/null @@ -1,286 +0,0 @@ -"""Build and utility command handlers.""" - -from __future__ import annotations - -import logging -import shutil -import sys -from pathlib import Path - -from captain import artifacts, docker, qemu -from captain.config import Config -from captain.util import run - -from ._stages import ( - _build_iso_stage, - _build_kernel_stage, - _build_mkosi_stage, - _build_tools_stage, -) - -log = logging.getLogger(__name__) - - -def _cmd_kernel(cfg: Config, _extra_args: list[str]) -> None: - """Build only the kernel (no tools, no mkosi).""" - _build_kernel_stage(cfg) - artifacts.collect_kernel(cfg) - log.info("Kernel build stage complete!") - - -def _cmd_tools(cfg: Config, _extra_args: list[str]) -> None: - """Download tools (containerd, runc, nerdctl, CNI plugins).""" - _build_tools_stage(cfg) - log.info("Tools stage complete!") - - -def _check_kernel_modules(cfg: Config) -> None: - """Verify kernel modules exist before building the initramfs. - - The initramfs depends on pre-built kernel modules in the ExtraTrees - directory. If they are missing (e.g. due to an artifact download - issue) the build should fail immediately rather than silently - producing an initramfs without modules. - """ - modules_dir = cfg.modules_output / "usr" / "lib" / "modules" - if not modules_dir.is_dir(): - log.error("Kernel modules directory not found: %s", modules_dir) - log.error("Ensure the kernel build artifacts are downloaded correctly.") - raise SystemExit(1) - # Check that at least one module version directory exists with modules - version_dirs = [d for d in modules_dir.iterdir() if d.is_dir()] - if not version_dirs: - log.error("No kernel version directories found in %s", modules_dir) - raise SystemExit(1) - # Search all version directories for at least one kernel module - for version_dir in version_dirs: - if any(version_dir.rglob("*.ko*")): - log.info("Kernel modules found in %s (version: %s)", version_dir, version_dir.name) - return - searched = ", ".join(str(d) for d in version_dirs) - log.error("No kernel modules (.ko/.ko.zst) found in any kernel version directory.") - log.error("Searched directories: %s", searched) - raise SystemExit(1) - - -def _cmd_initramfs(cfg: Config, extra_args: list[str]) -> None: - """Build only the initramfs via mkosi, then collect artifacts.""" - _check_kernel_modules(cfg) - _build_mkosi_stage(cfg, extra_args) - artifacts.collect_initramfs(cfg) - artifacts.collect_kernel(cfg) - log.info("Initramfs build complete!") - - -def _cmd_iso(cfg: Config, _extra_args: list[str]) -> None: - """Build only the ISO image.""" - _build_iso_stage(cfg) - artifacts.collect_iso(cfg) - log.info("ISO build complete!") - - -def _cmd_build(cfg: Config, extra_args: list[str]) -> None: - """Full build: kernel → tools → initramfs → iso → artifacts.""" - _build_kernel_stage(cfg) - _build_tools_stage(cfg) - _build_mkosi_stage(cfg, extra_args) - _build_iso_stage(cfg) - artifacts.collect(cfg) - log.info("Build complete!") - - -def _cmd_shell(cfg: Config, _extra_args: list[str]) -> None: - """Interactive shell inside the builder container.""" - docker.build_builder(cfg) - log.info("Entering builder shell (type 'exit' to leave)...") - docker.run_in_builder( - cfg, - *(["-it"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "--entrypoint", - "/bin/bash", - cfg.builder_image, - ) - - -def _cmd_clean(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Remove build artifacts for the selected kernel version, or all.""" - clean_all = getattr(args, "clean_all", False) - - if clean_all: - _clean_all(cfg) - else: - _clean_version(cfg) - - -def _clean_version(cfg: Config) -> None: - """Remove build artifacts for a single kernel version.""" - kver = cfg.kernel_version - log.info("Cleaning build artifacts for kernel %s (%s)...", kver, cfg.arch) - mkosi_output = cfg.mkosi_output - - # Version-specific directories under mkosi.output/{stage}/{version}/{arch} - version_dirs = [ - mkosi_output / "kernel" / kver / cfg.arch, - mkosi_output / "initramfs" / kver / cfg.arch, - mkosi_output / "iso" / kver / cfg.arch, - ] - - has_docker = shutil.which("docker") is not None - existing = [d for d in version_dirs if d.exists()] - if existing and has_docker: - # Use Docker to remove root-owned files from mkosi. - # Invoke rm directly (no shell) to avoid injection via path components. - container_path_args = [ - f"/work/mkosi.output/{d.relative_to(mkosi_output)}" for d in existing - ] - run( - [ - "docker", - "run", - "--rm", - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "debian:trixie", - "rm", - "-rf", - "--", - *container_path_args, - ], - ) - elif existing: - for d in existing: - shutil.rmtree(d, ignore_errors=True) - - # Remove versioned artifacts from out/ - if cfg.output_dir.exists(): - for pattern in ( - f"vmlinuz-{kver}-*", - f"initramfs-{kver}-*", - f"captainos-{kver}-*", - f"sha256sums-{kver}-*", - ): - for p in cfg.output_dir.glob(pattern): - p.unlink(missing_ok=True) - - log.info("Clean complete for kernel %s.", kver) - - -def _clean_all(cfg: Config) -> None: - """Remove all build artifacts (all kernel versions).""" - log.info("Cleaning ALL build artifacts...") - mkosi_output = cfg.mkosi_output - mkosi_cache = cfg.project_dir / "mkosi.cache" - - has_docker = shutil.which("docker") is not None - if has_docker: - # Use Docker to remove root-owned files from mkosi - if mkosi_output.exists() or mkosi_cache.exists(): - run( - [ - "docker", - "run", - "--rm", - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "debian:trixie", - "sh", - "-c", - "rm -rf /work/mkosi.output/image*" - " /work/mkosi.output/initramfs" - " /work/mkosi.output/kernel" - " /work/mkosi.output/tools" - " /work/mkosi.output/iso" - " /work/mkosi.cache", - ], - ) - else: - # No Docker available — remove directly (may need sudo for root-owned mkosi files) - for pattern in ("image*", "initramfs", "kernel", "tools", "iso"): - for p in mkosi_output.glob(pattern): - if p.is_dir(): - shutil.rmtree(p, ignore_errors=True) - else: - p.unlink(missing_ok=True) - if mkosi_cache.exists(): - shutil.rmtree(mkosi_cache, ignore_errors=True) - - if cfg.output_dir.exists(): - shutil.rmtree(cfg.output_dir) - log.info("Clean complete.") - - -def _cmd_summary(cfg: Config, _extra_args: list[str]) -> None: - """Print mkosi configuration summary.""" - tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) - output_dir = str(cfg.initramfs_output) - match cfg.mkosi_mode: - case "docker": - docker.build_builder(cfg) - container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_modules = f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" - container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" - docker.run_mkosi( - cfg, - f"--extra-tree={container_tree}", - f"--extra-tree={container_modules}", - f"--output-dir={container_outdir}", - "summary", - ) - case "native": - run( - [ - "mkosi", - f"--architecture={cfg.arch_info.mkosi_arch}", - f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", - f"--output-dir={output_dir}", - "summary", - ], - cwd=cfg.project_dir, - ) - case "skip": - log.error("Cannot show mkosi summary when MKOSI_MODE=skip.") - raise SystemExit(1) - - -def _cmd_checksums(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Compute SHA-256 checksums for the specified files.""" - files = getattr(args, "files", None) or [] - output = getattr(args, "output", None) - - if files: - # Explicit mode: user provided specific files and output. - if not output: - log.error("--output is required when specifying files explicitly.") - raise SystemExit(1) - artifacts.collect_checksums( - [Path(f) for f in files], - Path(output), - ) - else: - # Default mode: produce checksums for the selected architecture. - out = cfg.output_dir - oarch = cfg.arch_info.output_arch - kver = cfg.kernel_version - arch_files = [ - out / f"vmlinuz-{kver}-{oarch}", - out / f"initramfs-{kver}-{oarch}", - out / f"captainos-{kver}-{oarch}.iso", - ] - existing = [f for f in arch_files if f.is_file()] - if not existing: - log.error("No artifacts found for %s-%s in %s", kver, oarch, out) - raise SystemExit(1) - dest = Path(output) if output else out / f"sha256sums-{kver}-{oarch}.txt" - artifacts.collect_checksums(existing, dest) - log.info("Checksums complete!") - - -def _cmd_qemu_test(cfg: Config, _extra_args: list[str], args: object = None) -> None: - """Boot the image in QEMU for testing.""" - qemu.run_qemu(cfg, args=args) # type: ignore[arg-type] diff --git a/captain/cli/_iso.py b/captain/cli/_iso.py new file mode 100644 index 00000000..56590b33 --- /dev/null +++ b/captain/cli/_iso.py @@ -0,0 +1,61 @@ +"""``captain iso`` — build a bootable ISO image for the specified flavor and architecture.""" + +from __future__ import annotations + +import logging + +import click + +import captain.flavor +from captain import artifacts +from captain.cli._main import CliContext, cli +from captain.cli.stages import ( + _build_iso_stage, + _prune_stale_output_dirs, +) + +log = logging.getLogger(__name__) + + +@cli.command( + "iso", + short_help="Build ISO image only. Part of build.", +) +@click.option( + "--iso-mode", + envvar="ISO_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="ISO build stage execution mode (docker, native, skip).", +) +@click.option( + "--force-iso", + envvar="FORCE_ISO", + is_flag=True, + default=False, + help="Force ISO rebuild even if outputs already exist.", +) +@click.pass_obj +def build_cmd( + cli_ctx: CliContext, + *, + iso_mode: str, + force_iso: bool, +) -> None: + """Run the CaptainOS ISO build.""" + + cfg = cli_ctx.make_config( + iso_mode=iso_mode, + force_iso=force_iso, + ) + + # Instantiate the flavor + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + flavor.generate(hash_only=True) # Just populates cfg.flavor_hash so we can find the initramfs + + _build_iso_stage(cfg) + artifacts.collect_iso(cfg) + _prune_stale_output_dirs(cfg) + log.info("ISO build complete!!!") diff --git a/captain/cli/_kernel.py b/captain/cli/_kernel.py new file mode 100644 index 00000000..1f25dffe --- /dev/null +++ b/captain/cli/_kernel.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import logging + +import click + +from captain.cli._main import CliContext, cli +from captain.cli.stages import build_kernel_stage + +log = logging.getLogger(__name__) + + +@cli.command( + "kernel", + short_help="Build Linux Kernel for CaptainOS trixie-slim flavor.", +) +@click.option( + "--kernel-mode", + envvar="KERNEL_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Kernel build stage execution mode (docker, native, skip).", +) +@click.option( + "--force-kernel", + envvar="FORCE_KERNEL", + is_flag=True, + default=False, + help="Build kernel even if outputs already exist.", +) +@click.option( + "--config", + envvar="CONFIG_KERNEL", + is_flag=True, + default=False, + help="Skip building and instead do interactive kernel configuration (menuconfig).", +) +@click.pass_obj +def kernel_cmd(cli_ctx: CliContext, *, kernel_mode: str, force_kernel: bool, config: bool) -> None: + log.debug("CLI kernel mode: %s", kernel_mode) + + cfg = cli_ctx.make_config( + force_kernel=force_kernel or config, + kernel_mode=kernel_mode, + build_kernel=True, + kernel_menuconfig=config, + ) + + build_kernel_stage(cfg) + log.info("Kernel build stage complete!") diff --git a/captain/cli/_main.py b/captain/cli/_main.py index 33ded5d0..da16de37 100644 --- a/captain/cli/_main.py +++ b/captain/cli/_main.py @@ -1,144 +1,243 @@ -"""CLI entry point — single configargparse parser with pre-extracted subcommand. - -Every configuration parameter is both a ``--cli-flag`` and an environment -variable, following the ff priority model: - - CLI args > environment variables > defaults - -The subcommand (``build``, ``kernel``, ``tools``, …) is extracted from -``sys.argv`` *before* parsing so that flags work in any position:: - - ./build.py --arch=arm64 kernel # works - ./build.py kernel --arch=arm64 # also works - ARCH=arm64 ./build.py kernel # also works -""" +"""Click CLI — main group with shared options and subcommand registration.""" from __future__ import annotations import logging import sys +from dataclasses import dataclass from pathlib import Path +from typing import Any -from captain import docker -from captain.config import Config -from captain.util import run - -from ._commands import ( - _cmd_build, - _cmd_checksums, - _cmd_clean, - _cmd_initramfs, - _cmd_iso, - _cmd_kernel, - _cmd_qemu_test, - _cmd_shell, - _cmd_summary, - _cmd_tools, -) -from ._parser import _build_parser, _extract_command -from ._release import _cmd_release +import click +from trogon import tui + +from captain.config import DEFAULT_FLAVOR_ID, DEFAULT_KERNEL_VERSION, Config +from captain.flavor import list_available_flavors +from captain.util import detect_current_machine_arch log = logging.getLogger(__name__) -def main(project_dir: Path | None = None) -> None: - """Main CLI entry point.""" - - # 1. Extract the subcommand from argv before parsing so flags - # work in any position (before or after the command name). - raw_argv = sys.argv[1:] - command, flag_argv = _extract_command(raw_argv) - - # For release subcommands, defer -h/--help to _cmd_release so it - # can print subcommand-specific help instead of the generic release help. - # We defer whenever there's any positional token (not just valid ones), - # so that invalid subcommands like "push" show the proper error instead - # of the parent help. - help_deferred = False - if command == "release": - has_positional = any(not tok.startswith("-") for tok in flag_argv) - has_help = "-h" in flag_argv or "--help" in flag_argv - if has_positional and has_help: - flag_argv = [t for t in flag_argv if t not in ("-h", "--help")] - help_deferred = True - - # 2. Build the parser (TINK flags added only for qemu-test). - parser = _build_parser(command) - - # 3. Parse known args — anything unrecognised passes through to mkosi. - args, extra = parser.parse_known_args(flag_argv) - if help_deferred: - extra.append("--help") - - # 4. Separate --force (mkosi passthrough) from the rest. - mkosi_args: list[str] = [] - if getattr(args, "force", False): - mkosi_args.append("--force") - - # 5. Determine project directory. - if project_dir is None: - project_dir = Path(__file__).resolve().parent.parent.parent - - # 6. Build Config from the parsed namespace. - cfg = Config.from_args(args, project_dir) - cfg.mkosi_args = mkosi_args - - # 7. Dispatch. - dispatch: dict[str, object] = { - "build": _cmd_build, - "kernel": _cmd_kernel, - "tools": _cmd_tools, - "initramfs": _cmd_initramfs, - "iso": _cmd_iso, - "checksums": _cmd_checksums, - "shell": _cmd_shell, - "clean": _cmd_clean, - "release": _cmd_release, - "summary": _cmd_summary, - "qemu-test": _cmd_qemu_test, - } - - handler = dispatch.get(command) - if handler is not None: - if command in ("qemu-test", "checksums", "release", "clean"): - handler(cfg, extra, args=args) # type: ignore[operator] - else: - handler(cfg, extra) # type: ignore[operator] - else: - # Pass through to mkosi (shouldn't happen with _extract_command - # but kept as a safety net). - tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) - output_dir = str(cfg.initramfs_output) - match cfg.mkosi_mode: - case "docker": - docker.build_builder(cfg) - container_tree = f"/work/mkosi.output/tools/{cfg.arch}" - container_modules = ( - f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" - ) - container_outdir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" - docker.run_mkosi( - cfg, - f"--extra-tree={container_tree}", - f"--extra-tree={container_modules}", - f"--output-dir={container_outdir}", - command, - *extra, - ) - case "native": - run( - [ - "mkosi", - f"--architecture={cfg.arch_info.mkosi_arch}", - f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", - f"--output-dir={output_dir}", - command, - *extra, - ], - cwd=cfg.project_dir, - ) - case "skip": - log.error("Cannot pass '%s' to mkosi when MKOSI_MODE=skip.", command) - raise SystemExit(1) +# --------------------------------------------------------------------------- +# CLI context object — shared state for all subcommands +# --------------------------------------------------------------------------- + + +@dataclass(slots=True) +class CliContext: + """Resolved common CLI options, passed to subcommands via ``@click.pass_obj``.""" + + project_dir: Path + arch: str + flavor_id: str + builder_registry: str | None + builder_repository: str | None + builder_image: str + verbose_docker: bool + drop_old_caches: bool + kernel_version: str + armbian_version: str | None + + def make_config(self, **overrides: Any) -> Config: + """Build a :class:`Config` from the common options plus per-command *overrides*.""" + return Config( + project_dir=self.project_dir, + output_dir=self.project_dir / "out", + arch=self.arch, + flavor_id=self.flavor_id, + builder_registry=self.builder_registry, + builder_repository=self.builder_repository, + builder_image=self.builder_image, + verbose_docker=self.verbose_docker, + drop_old_caches=self.drop_old_caches, + kernel_version=self.kernel_version, + armbian_version=self.armbian_version, + **overrides, + ) + + +# --------------------------------------------------------------------------- +# Resolve project directory +# --------------------------------------------------------------------------- + + +def resolve_project_dir(project_dir: str | None) -> Path: + """Return an absolute ``Path`` for the project root.""" + if project_dir is not None: + return Path(project_dir) + # Walk upward from this file until we find pyproject.toml. + candidate = Path(__file__).resolve().parent.parent.parent + if (candidate / "pyproject.toml").is_file(): + return candidate + click.echo("Error: cannot auto-detect project directory. Pass --project-dir.", err=True) + sys.exit(1) + + +# --------------------------------------------------------------------------- +# Top-level Click group +# --------------------------------------------------------------------------- +# Important: decorator order matters here. The @tui() decorator must be outermost +# to properly wrap the entire CLI, including subcommands. + +CONTEXT_SETTINGS = dict( + help_option_names=["-h", "--help"], + max_content_width=120, +) + + +@tui() +@click.group( + context_settings=CONTEXT_SETTINGS, + invoke_without_command=True, + help=( + "CaptainOS build system.\n\n" + "Run 'captain COMMAND --help' for details on each subcommand.\n\n" + "Shell completion (bash/zsh):\n\n" + ' eval "$(_CAPTAIN_COMPLETE=bash_source captain)" # bash\n\n' + ' eval "$(_CAPTAIN_COMPLETE=zsh_source captain)" # zsh' + ), +) +@click.version_option(package_name="captain") +@click.option( + "-v", + "--verbose", + is_flag=True, + default=False, + envvar="CAPTAIN_VERBOSE", + help="Enable verbose (DEBUG-level) logging.", +) +@click.option( + "--arch", + envvar="ARCH", + default=(detect_current_machine_arch()), + show_default=True, + type=click.Choice(["amd64", "arm64"], case_sensitive=False), + metavar="ARCH", + help="Target architecture (amd64, arm64).", +) +@click.option( + "--flavor-id", + envvar="FLAVOR_ID", + default=DEFAULT_FLAVOR_ID, + show_default=True, + type=click.Choice(list_available_flavors(), case_sensitive=False), + help="Flavor (kernel/board config) to build.", +) +@click.option( + "--project-dir", + envvar="CAPTAIN_PROJECT_DIR", + default=None, + type=click.Path(exists=True, file_okay=False, resolve_path=True), + help="Project root directory (auto-detected when omitted).", +) +@click.option( + "--builder-registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname for the Docker builder image", +) +@click.option( + "--builder-repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name) for the Docker builder image", +) +@click.option( + "--builder-image", + envvar="BUILDER_IMAGE", + default="captainos-builder", + show_default=True, + help="Local name/tag of Docker builder image name", +) +@click.option( + "--drop-old-caches", + envvar="CAPTAIN_DROP_OLD_CACHES", + is_flag=True, + default=False, + help=( + "After a build, remove sibling flavor-hash output dirs so caches/artifacts " + "only keep the freshly-built version (env: CAPTAIN_DROP_OLD_CACHES)." + ), +) +@click.option( + "--kernel-version", + envvar="KERNEL_VERSION", + default=DEFAULT_KERNEL_VERSION, + show_default=True, + help="Kernel version to build/reference. Must match an official tarball (env: KERNEL_VERSION).", +) +@click.option( + "--armbian-version", + envvar="ARMBIAN_VERSION", + default=None, + help=( + "Armbian repo version token folded into armbian flavors' hash so they rebuild " + "when armbian-next updates (env: ARMBIAN_VERSION). Empty = no cache-bust." + ), +) +@click.pass_context +def cli( + ctx: click.Context, + *, + verbose: bool, + arch: str, + flavor_id: str, + project_dir: str | None, + builder_registry: str | None, + builder_repository: str | None, + builder_image: str, + drop_old_caches: bool, + kernel_version: str, + armbian_version: str | None, +) -> None: + """CaptainOS build system — click CLI.""" + # Configure log level based on --verbose. + # Configure the root logger (configged via basicLogging() in captain __init__) + # since subcommands and imported modules will use it. + log_level = logging.DEBUG if verbose else logging.INFO + logging.getLogger().setLevel(log_level) + logging.getLogger("captain").setLevel(log_level) + + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + return + + # Build the shared context object for subcommands. + log.debug("Main CLI creating CliContext...") + ctx.obj = CliContext( + project_dir=resolve_project_dir(project_dir), + arch=arch, + flavor_id=flavor_id, + builder_registry=builder_registry, + builder_repository=builder_repository, + builder_image=builder_image, + verbose_docker=verbose, + drop_old_caches=drop_old_caches, + kernel_version=kernel_version, + armbian_version=armbian_version, + ) + + +# --------------------------------------------------------------------------- +# Register subcommands (imported lazily to avoid circular imports) +# --------------------------------------------------------------------------- + + +def main() -> None: + """Console-script entry point.""" + # Import subcommand modules to register them on the group. + from captain.cli import ( # noqa: F401 + _build, + _builder, + _iso, + _kernel, + _prepare, + _qemu, + _release, + _shell, + _tools, + ) + + cli() diff --git a/captain/cli/_parser.py b/captain/cli/_parser.py deleted file mode 100644 index 4e6d1297..00000000 --- a/captain/cli/_parser.py +++ /dev/null @@ -1,583 +0,0 @@ -"""CLI parser infrastructure — formatter, constants, and flag definitions.""" - -from __future__ import annotations - -import argparse -import shutil -import sys -from collections.abc import Callable, Iterable - -import configargparse - -from captain.config import DEFAULT_KERNEL_VERSION - -# --------------------------------------------------------------------------- -# Known subcommands (order matters for help text) -# --------------------------------------------------------------------------- - -COMMANDS: dict[str, str] = { - "build": "Run all build stages: kernel → tools → initramfs → iso (default)", - "kernel": "Build only the kernel + modules", - "tools": "Download tools (containerd, runc, nerdctl, CNI)", - "initramfs": "Build only the initramfs via mkosi", - "iso": "Build a UEFI-bootable ISO image", - "checksums": "Compute SHA-256 checksums for specified files", - "release": "OCI artifact operations (publish, pull, tag)", - "shell": "Interactive shell inside the builder container", - "clean": "Remove build artifacts (per kernel version or all)", - "summary": "Print mkosi configuration summary", - "qemu-test": "Boot the image in QEMU for testing", -} - -VALID_MODES = ("docker", "native", "skip") - -# Boolean (store_true) flags — these do NOT consume the next token as a value. -# Used by _extract_command to avoid treating a flag value as a subcommand. -_BOOLEAN_FLAGS = frozenset( - { - "--all", - "--force-kernel", - "--force-tools", - "--force-iso", - "--force", - "--no-cache", - "-h", - "--help", - } -) - - -class _HelpFormatter(argparse.RawDescriptionHelpFormatter): - """Clean help: word-wrapped text, raw epilog, no env-var refs, - no defaults when the value is empty / None / False, and a short - usage line.""" - - def _get_help_string(self, action: argparse.Action) -> str: - """Append ``(default: X)`` only when X is meaningful.""" - text = action.help or "" - if action.default in (None, "", False, argparse.SUPPRESS): - return text - if "%(default)" not in text: - text += " (default: %(default)s)" - return text - - def _format_usage( - self, - usage: str | None, - actions: Iterable[argparse.Action], - groups: Iterable[argparse._MutuallyExclusiveGroup], - prefix: str | None, - ) -> str: - """Show a short usage line with the command placeholder.""" - prog = self._prog - # Top-level ("build.py") and release ("build.py release") have subcommands. - if prog in ("build.py", "build.py release"): - return f"usage: {prog} [command] [flags]\n\n" - if prog == "build.py release tag": - return f"usage: {prog} [flags]\n\n" - return f"usage: {prog} [flags]\n\n" - - -def _extract_command(argv: list[str]) -> tuple[str, list[str]]: - """Remove and return the first recognised subcommand from *argv*. - - Returns ``("build", argv)`` when no subcommand is found. - - The scanner skips tokens that are likely flag *values* (the token - immediately after a ``--flag`` that is not boolean and does not use - ``=`` syntax). This prevents ``--builder-image build`` from - incorrectly extracting ``build`` as the subcommand. - """ - prev_was_value_flag = False - for i, tok in enumerate(argv): - if tok.startswith("-"): - # Boolean flags don't consume the next token. - prev_was_value_flag = False if "=" in tok else tok not in _BOOLEAN_FLAGS - continue - if prev_was_value_flag: - # This token is the value of the preceding flag — skip it. - prev_was_value_flag = False - continue - # Standalone positional token — check if it's a command. - if tok in COMMANDS: - return tok, argv[:i] + argv[i + 1 :] - # Unknown positional token — not a recognised command. - valid = ", ".join(COMMANDS) - print( - f"error: unknown command '{tok}'\nvalid commands: {valid}", - file=sys.stderr, - ) - raise SystemExit(2) - return "build", list(argv) - - -def _build_parser(command: str) -> configargparse.ArgParser: - """Construct a command-specific CLI parser. - - Only the flags relevant to *command* are added, so ``--help`` - shows a focused help message. - """ - - # -- description & epilog ------------------------------------------ - if command == "build": - desc = "Build CaptainOS images. Stages: kernel → tools → initramfs → iso." - commands_list = "\n".join(f" {name:14s} {d}" for name, d in COMMANDS.items()) - epilog = f"""\ -commands: -{commands_list} -""" - elif command == "release": - desc = "OCI release workflow: pull (or build) → publish → tag" - release_cmds = { - "publish": "Publish artifacts as a multi-arch OCI image", - "pull": "Pull and extract artifacts (amd64, arm64, or combined)", - "tag": "Tag all artifact images with a version", - } - commands_list = "\n".join(f" {name:14s} {d}" for name, d in release_cmds.items()) - epilog = f"""\ -commands: -{commands_list} -""" - else: - desc = COMMANDS.get(command, command) - epilog = None - - # Adapt to the real terminal width so argparse wraps at word - # boundaries instead of letting the terminal hard-wrap mid-word. - # max_help_position=38 accommodates the widest flag+metavar. - columns = shutil.get_terminal_size().columns - - parser = configargparse.ArgParser( - prog=f"build.py {command}" if command != "build" else "build.py", - description=desc, - epilog=epilog, - add_env_var_help=False, - formatter_class=lambda prog: _HelpFormatter( - prog, - max_help_position=38, - width=columns, - ), - ) - - # -- Add only the flag groups relevant to this command ------------- - for adder in _COMMAND_FLAGS.get(command, []): - adder(parser) - - return parser - - -# --------------------------------------------------------------------------- -# Flag-group helpers — each adds one argument group to the parser -# --------------------------------------------------------------------------- - - -def _add_common_flags(parser: configargparse.ArgParser) -> None: - """--arch, --builder-image, --no-cache""" - g = parser.add_argument_group("build configuration") - g.add_argument( - "--arch", - env_var="ARCH", - default="amd64", - choices=["amd64", "arm64"], - help="target architecture", - ) - g.add_argument( - "--builder-image", - env_var="BUILDER_IMAGE", - metavar="IMAGE", - default="captainos-builder", - help="Docker builder image name", - ) - g.add_argument( - "--no-cache", - env_var="NO_CACHE", - action="store_true", - default=False, - help="rebuild builder image without Docker cache", - ) - - -def _add_kernel_flags(parser: configargparse.ArgParser) -> None: - """--kernel-version, --kernel-src, --kernel-mode, --force-kernel""" - g = parser.add_argument_group("kernel") - g.add_argument( - "--kernel-version", - env_var="KERNEL_VERSION", - metavar="VER", - default=DEFAULT_KERNEL_VERSION, - help="kernel version to build", - ) - g.add_argument( - "--kernel-config", - env_var="KERNEL_CONFIG", - metavar="PATH", - default=None, - help="path to kernel config file (overrides auto-detection from kernel.configs/)", - ) - g.add_argument( - "--kernel-src", - env_var="KERNEL_SRC", - metavar="PATH", - default=None, - help="path to local kernel source tree", - ) - g.add_argument( - "--kernel-mode", - env_var="KERNEL_MODE", - default="docker", - choices=list(VALID_MODES), - help="kernel stage execution mode", - ) - g.add_argument( - "--force-kernel", - env_var="FORCE_KERNEL", - action="store_true", - default=False, - help="force kernel rebuild even if outputs exist", - ) - - -def _add_tools_flags(parser: configargparse.ArgParser) -> None: - """--tools-mode, --force-tools""" - g = parser.add_argument_group("tools") - g.add_argument( - "--tools-mode", - env_var="TOOLS_MODE", - default="docker", - choices=list(VALID_MODES), - help="tools stage execution mode", - ) - g.add_argument( - "--force-tools", - env_var="FORCE_TOOLS", - action="store_true", - default=False, - help="re-download tools even if outputs exist", - ) - - -def _add_mkosi_flags(parser: configargparse.ArgParser) -> None: - """--mkosi-mode, --force (mkosi passthrough)""" - g = parser.add_argument_group("initramfs (mkosi)") - g.add_argument( - "--mkosi-mode", - env_var="MKOSI_MODE", - default="docker", - choices=list(VALID_MODES), - help="mkosi stage execution mode", - ) - g.add_argument( - "--force", - action="store_true", - default=False, - help="passed through to mkosi as --force", - ) - - -def _add_summary_flags(parser: configargparse.ArgParser) -> None: - """--mkosi-mode only (no --force).""" - g = parser.add_argument_group("mkosi") - g.add_argument( - "--mkosi-mode", - env_var="MKOSI_MODE", - default="docker", - choices=list(VALID_MODES), - help="mkosi stage execution mode", - ) - - -def _add_iso_flags(parser: configargparse.ArgParser) -> None: - """--iso-mode, --force-iso""" - g = parser.add_argument_group("iso") - g.add_argument( - "--iso-mode", - env_var="ISO_MODE", - default="docker", - choices=list(VALID_MODES), - help="iso stage execution mode", - ) - g.add_argument( - "--force-iso", - env_var="FORCE_ISO", - action="store_true", - default=False, - help="force ISO rebuild even if outputs exist", - ) - - -def _add_mode_flags(parser: configargparse.ArgParser) -> None: - """All four --*-mode flags (used by 'shell' which checks needs_docker).""" - g = parser.add_argument_group("stage modes") - for stage in ("kernel", "tools", "mkosi", "iso"): - g.add_argument( - f"--{stage}-mode", - env_var=f"{stage.upper()}_MODE", - default="docker", - choices=list(VALID_MODES), - help=f"{stage} stage execution mode", - ) - - -def _add_clean_flags(parser: configargparse.ArgParser) -> None: - """--all flag for the clean command.""" - g = parser.add_argument_group("clean") - g.add_argument( - "--all", - env_var="CLEAN_ALL", - action="store_true", - default=False, - dest="clean_all", - help="remove ALL build artifacts instead of just the selected kernel version", - ) - - -def _add_checksums_flags(parser: configargparse.ArgParser) -> None: - """--output and positional file arguments for the checksums command.""" - g = parser.add_argument_group("checksums") - g.add_argument( - "--output", - "-o", - metavar="FILE", - default=None, - help="path to write the checksum file (default: out/sha256sums-{arch}.txt)", - ) - g.add_argument( - "files", - nargs="*", - metavar="FILE", - help="files to checksum (default: standard release artifacts in out/)", - ) - - -def _add_release_flags(parser: configargparse.ArgParser) -> None: - """--release-mode and OCI registry flags for the release command.""" - _add_release_base_flags(parser) - _add_release_target_flag(parser) - _add_release_pull_output(parser) - - -def _add_release_base_flags(parser: configargparse.ArgParser) -> None: - """Core release flags shared by all release subcommands.""" - g = parser.add_argument_group("release") - g.add_argument( - "--release-mode", - env_var="RELEASE_MODE", - default="native", - choices=list(VALID_MODES), - metavar="MODE", - help="release stage execution mode", - ) - - g = parser.add_argument_group("OCI registry") - g.add_argument( - "--registry", - env_var="REGISTRY", - metavar="HOST", - default="ghcr.io", - help="OCI registry hostname", - ) - g.add_argument( - "--repository", - env_var="GITHUB_REPOSITORY", - metavar="OWNER/NAME", - default="tinkerbell/captain", - help="repository (owner/name)", - ) - g.add_argument( - "--oci-artifact-name", - env_var="OCI_ARTIFACT_NAME", - metavar="NAME", - default="artifacts", - help="OCI artifact image name", - ) - - -def _add_release_target_flag(parser: configargparse.ArgParser) -> None: - """--target flag for publish and pull (not tag).""" - g = parser.add_argument_group("target") - g.add_argument( - "--target", - env_var="TARGET", - default=None, - choices=["amd64", "arm64", "combined"], - help="artifact target (amd64, arm64, or combined; default: --arch value)", - ) - g.add_argument( - "--git-sha", - env_var="GITHUB_SHA", - metavar="SHA", - default=None, - help="git commit SHA (default: from git rev-parse HEAD)", - ) - g.add_argument( - "--version-exclude", - env_var="VERSION_EXCLUDE", - metavar="TAG", - default=None, - help="tag to exclude from git-describe version lookup", - ) - g.add_argument( - "--force", - env_var="FORCE", - action="store_true", - default=False, - help="publish even if the image already exists in the registry", - ) - - -def _add_release_pull_output(parser: configargparse.ArgParser) -> None: - """--pull-output flag (only relevant for 'release pull').""" - g = parser.add_argument_group("pull") - g.add_argument( - "--pull-output", - metavar="DIR", - default=None, - help="output directory for pulled artifacts", - ) - - -def _add_release_tag_version(parser: configargparse.ArgParser) -> None: - """Positional argument for 'release tag'.""" - parser.add_argument( - "version", - nargs="?", - default=None, - help="version tag to apply (e.g. v1.0.0)", - ) - - -def _add_qemu_flags(parser: configargparse.ArgParser) -> None: - """--qemu-append, --qemu-mem, --qemu-smp""" - g = parser.add_argument_group("qemu") - g.add_argument( - "--qemu-append", - env_var="QEMU_APPEND", - metavar="ARGS", - default="", - help="extra kernel cmdline args for qemu-test", - ) - g.add_argument( - "--qemu-mem", - env_var="QEMU_MEM", - metavar="SIZE", - default="2G", - help="QEMU RAM size", - ) - g.add_argument( - "--qemu-smp", - env_var="QEMU_SMP", - metavar="N", - default="2", - help="QEMU CPU count", - ) - - -def _add_tink_flags(parser: configargparse.ArgParser) -> None: - """Tinkerbell kernel cmdline flags + --ipam.""" - g = parser.add_argument_group("tinkerbell") - g.add_argument( - "--tink-worker-image", - env_var="TINK_WORKER_IMAGE", - metavar="IMAGE", - default="ghcr.io/tinkerbell/tink-agent:latest", - help="tink-agent container image reference", - ) - g.add_argument( - "--tink-docker-registry", - env_var="TINK_DOCKER_REGISTRY", - metavar="HOST", - default="", - help="registry host (triggers tink-agent services)", - ) - g.add_argument( - "--tink-grpc-authority", - env_var="TINK_GRPC_AUTHORITY", - metavar="ADDR", - default="", - help="tink-server gRPC endpoint (host:port)", - ) - g.add_argument( - "--tink-worker-id", - env_var="TINK_WORKER_ID", - metavar="ID", - default="", - help="machine / worker ID", - ) - g.add_argument( - "--tink-tls", - env_var="TINK_TLS", - metavar="BOOL", - default="false", - help="enable TLS to tink-server", - ) - g.add_argument( - "--tink-insecure-tls", - env_var="TINK_INSECURE_TLS", - metavar="BOOL", - default="true", - help="allow insecure TLS", - ) - g.add_argument( - "--tink-insecure-registries", - env_var="TINK_INSECURE_REGISTRIES", - metavar="LIST", - default="", - help="comma-separated insecure registries", - ) - g.add_argument( - "--tink-registry-username", - env_var="TINK_REGISTRY_USERNAME", - metavar="USER", - default="", - help="registry auth username", - ) - g.add_argument( - "--tink-registry-password", - env_var="TINK_REGISTRY_PASSWORD", - metavar="PASS", - default="", - help="registry auth password", - ) - g.add_argument( - "--tink-syslog-host", - env_var="TINK_SYSLOG_HOST", - metavar="HOST", - default="", - help="remote syslog host", - ) - g.add_argument( - "--tink-facility", - env_var="TINK_FACILITY", - metavar="CODE", - default="", - help="facility code", - ) - g.add_argument( - "--ipam", - env_var="IPAM", - metavar="PARAM", - default="", - help="static networking IPAM parameter", - ) - - -# Map command → list of flag-group adders. -_COMMAND_FLAGS: dict[str, list[Callable[..., None]]] = { - "build": [ - _add_common_flags, - _add_kernel_flags, - _add_tools_flags, - _add_mkosi_flags, - _add_iso_flags, - ], - "kernel": [_add_common_flags, _add_kernel_flags], - "tools": [_add_common_flags, _add_tools_flags], - "initramfs": [_add_common_flags, _add_kernel_flags, _add_mkosi_flags], - "iso": [_add_common_flags, _add_kernel_flags, _add_iso_flags], - "checksums": [_add_common_flags, _add_kernel_flags, _add_checksums_flags], - "release": [_add_common_flags, _add_kernel_flags, _add_release_flags], - "shell": [_add_common_flags], - "clean": [_add_common_flags, _add_kernel_flags, _add_clean_flags], - "summary": [_add_common_flags, _add_kernel_flags, _add_summary_flags], - "qemu-test": [_add_common_flags, _add_kernel_flags, _add_qemu_flags, _add_tink_flags], -} diff --git a/captain/cli/_prepare.py b/captain/cli/_prepare.py new file mode 100644 index 00000000..6484ea1c --- /dev/null +++ b/captain/cli/_prepare.py @@ -0,0 +1,33 @@ +"""``captain prepare-versions`` — resolve dynamic version tokens for CI. + +Fetches the latest kernel.org point release of the configured branch and the +armbian-next repo Release ETag, then emits them as ``name=value`` lines on +stdout (logging goes to stderr) suitable for appending to ``$GITHUB_OUTPUT``: + + kernel_version=6.18.40 + armbian_version= +""" + +from __future__ import annotations + +import logging + +import click + +from captain import armbian, kernel +from captain.cli._main import cli + +log = logging.getLogger(__name__) + + +@cli.command( + "prepare-versions", + short_help="Emit latest kernel/armbian version tokens as name=value (for $GITHUB_OUTPUT).", +) +def prepare_versions_cmd() -> None: + """Resolve dynamic version tokens and print them to stdout.""" + kernel_version = kernel.latest_branch_version() + armbian_version = armbian.release_etag() + + click.echo(f"kernel_version={kernel_version}") + click.echo(f"armbian_version={armbian_version}") diff --git a/captain/cli/_qemu.py b/captain/cli/_qemu.py new file mode 100644 index 00000000..44913b01 --- /dev/null +++ b/captain/cli/_qemu.py @@ -0,0 +1,36 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click +from click import Context + +from captain.cli._main import CliContext, cli +from captain.qemu import run_qemu + +log = logging.getLogger(__name__) + + +@cli.command( + "qemu", + context_settings={ + "ignore_unknown_options": True, + "allow_extra_args": True, + }, + short_help="Run the built captainos kernel+initramfs in QEMU.", +) +@click.pass_obj +@click.pass_context +def shell_cmd(ctx: Context, cli_ctx: CliContext) -> None: + log.debug("Running captainos in QEMU...") + + cfg = cli_ctx.make_config() + + extra_args: list[str] = ctx.args + log.debug(f"Extra args passed to qemu: {extra_args}") + + run_qemu(cfg, extra_args) + + log.info("qemu exited.") diff --git a/captain/cli/_release.py b/captain/cli/_release.py index 41f4700b..f75f0b3a 100644 --- a/captain/cli/_release.py +++ b/captain/cli/_release.py @@ -1,236 +1,258 @@ -"""Release subcommand — publish, pull, tag.""" +"""``captain release-publish`` — publish artifacts as OCI images via buildah.""" from __future__ import annotations import logging -import shutil import subprocess from pathlib import Path -import configargparse +import click -from captain import docker, oci +import captain.flavor +from captain import oci +from captain.cli._main import CliContext, cli from captain.config import Config from captain.util import check_release_dependencies -from ._parser import ( - _add_common_flags, - _add_kernel_flags, - _add_release_base_flags, - _add_release_pull_output, - _add_release_tag_version, - _add_release_target_flag, - _HelpFormatter, +log = logging.getLogger(__name__) + + +@cli.group() +@click.option( + "--release-mode", + envvar="RELEASE_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Release stage execution mode (docker, native, skip).", +) +@click.option( + "--registry", + envvar="REGISTRY", + default="ghcr.io", + show_default=True, + help="OCI registry hostname.", +) +@click.option( + "--repository", + envvar="GITHUB_REPOSITORY", + default="tinkerbell/captain", + show_default=True, + help="Repository path (owner/name).", +) +@click.option( + "--oci-artifact-name", + envvar="OCI_ARTIFACT_NAME", + default="artifacts", + show_default=True, + help="OCI artifact image name.", +) +@click.option( + "--target", + envvar="TARGET", + default=None, + type=click.Choice(["amd64", "arm64", "combined"], case_sensitive=False), + metavar="TARGET", + help="Artifact target: 'amd64', 'arm64', or 'combined' (default: value of --arch); " + "'combined' requires an ACPI flavor with both arch's outputs present.", ) +@click.option( + "--git-sha", + envvar="GIT_SHA", + default=None, + help="Git commit SHA (default: auto-detected via git rev-parse HEAD).", +) +@click.option( + "--force-release", + envvar="FORCE_RELEASE", + is_flag=True, + default=False, + help="Publish even if the image already exists in the registry.", +) +@click.option( + "--src-tag", + envvar="SRC_TAG", + default=None, + help="Override specific tag to use; auto-determined if omitted.", +) +@click.pass_context +def release_group( + ctx: click.Context, + *, + release_mode: str, + registry: str, + repository: str, + oci_artifact_name: str, + target: str | None, + git_sha: str | None, + force_release: bool, + src_tag: str | None, +) -> None: + """Release-related operations: publish, pull, tag. Operates on OCI images in a registry.""" + cli_ctx: CliContext = ctx.obj -log = logging.getLogger(__name__) + if target is None: + target = cli_ctx.arch + assert isinstance(target, str) -_RELEASE_SUBCOMMANDS = ("publish", "pull", "tag") - -_RELEASE_SUBCMD_INFO: dict[str, tuple[str, list]] = { - "publish": ( - "Publish artifacts as a multi-arch OCI image", - [_add_common_flags, _add_kernel_flags, _add_release_base_flags, _add_release_target_flag], - ), - "pull": ( - "Pull and extract artifacts (amd64, arm64, or combined)", - [ - _add_common_flags, - _add_kernel_flags, - _add_release_base_flags, - _add_release_target_flag, - _add_release_pull_output, - ], - ), - "tag": ( - "Tag all artifact images with a version", - [_add_common_flags, _add_kernel_flags, _add_release_base_flags, _add_release_tag_version], - ), -} - - -def _print_release_subcmd_help(sub: str, *, exit_code: int = 0) -> None: - """Print help for a release subcommand and exit.""" - desc, adders = _RELEASE_SUBCMD_INFO[sub] - columns = shutil.get_terminal_size().columns - parser = configargparse.ArgParser( - prog=f"build.py release {sub}", - description=desc, - add_env_var_help=False, - formatter_class=lambda prog: _HelpFormatter( - prog, - max_help_position=38, - width=columns, - ), + log.debug("Creating Release group Config and putting into Context...") + + cfg = cli_ctx.make_config( + release_mode=release_mode, + release_registry=registry, + release_repository=repository, + release_oci_artifact_name=oci_artifact_name, + release_target=target, + release_git_sha=git_sha, + force_release=force_release, + release_src_tag=src_tag, ) - for adder in adders: - adder(parser) - parser.print_help() - raise SystemExit(exit_code) + # resolve the SHA before launching Docker, as we've the .git here and not there. + if cfg.release_git_sha is None: + log.debug( + "Auto-detecting git SHA for release since not provided via --git-sha or GITHUB_SHA..." + ) + cfg.release_git_sha = _autodetect_git_sha(cfg.project_dir) + log.debug("Git SHA at group level: %s", cfg.release_git_sha) -def _resolve_git_sha(args: object, project_dir: Path) -> str: - """Return the git SHA from args or by running git rev-parse.""" - sha = getattr(args, "git_sha", None) - if sha: - return sha + if cfg.release_src_tag is None: + log.debug( + "Auto-computing version tag for release since not provided via --src-tag or SRC_TAG..." + ) + tag = f"v0.0.0-{str(cfg.release_git_sha)[:7]}" + cfg.release_src_tag = f"{tag}-{cfg.flavor_id}" + log.debug("Tag at group level: %s", cfg.release_src_tag) - result = subprocess.run( - ["git", "rev-parse", "HEAD"], - capture_output=True, - text=True, - check=True, - cwd=project_dir, + # pass it down via Context // pass_obj + ctx.obj = cfg + + +@release_group.command("pull") +@click.option( + "--pull-output", + envvar="PULL_OUTPUT", + required=True, + type=click.Path(), + help="Output directory for pulled artifacts, relative to 'out/'.", +) +@click.pass_obj +def pull_command(cfg: Config, pull_output: Path) -> None: + """Pull release artifacts as OCI images from a registry and extract to disk.""" + log.debug("Release Pull with Config %s and pull_output %s", cfg, pull_output) + + cfg.release_pull_output = pull_output + + if skip_or_relaunch_in_docker(cfg, "pull"): + return + + oci.pull( + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + tag=str(cfg.release_src_tag), + target=str(cfg.release_target), + output_dir=cfg.output_dir / cfg.release_pull_output, + ) + + +@release_group.command("tag") +@click.option( + "--new-tag", + envvar="NEW_TAG", + required=True, + help="New tag to apply to the existing release image (e.g. v1.0.0).", +) +@click.pass_obj +def tag_command(cfg: Config, new_tag: str) -> None: + """Tag an existing release image with a new version.""" + log.debug("Release tag with Config: %s and new_tag %s", cfg, new_tag) + + cfg.release_new_tag = new_tag + + if skip_or_relaunch_in_docker(cfg, "tag"): + return + + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + oci.tag_all( + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + src_tag=str(cfg.release_src_tag), + new_tag=str(cfg.release_new_tag), + arches=list(flavor.supported_architectures), ) - return result.stdout.strip() -def _cmd_release(cfg: Config, extra_args: list[str], args: object = None) -> None: - """OCI artifact operations: publish, pull, tag.""" +@release_group.command("publish") +@click.pass_obj +def publish_command(cfg: Config) -> None: + """Publish build artifacts as a multi-arch OCI image to a registry. - # Peel the release subcommand from extra_args. - if not extra_args: - log.error( - "Missing release subcommand.\n usage: build.py release {%s}\n", - ",".join(_RELEASE_SUBCOMMANDS), - ) - raise SystemExit(2) + Uses buildah to construct OCI images from the build artifacts (kernel, + initramfs, ISO, DTBs) and pushes them to the specified registry. - sub = extra_args[0] - rest = extra_args[1:] + Each artifact file becomes its own layer. Deterministic tar generation + ensures byte-identical layers across runs so that registries can + deduplicate blobs. + """ + log.debug("Got into release publish with Config: %s", cfg) - if sub not in _RELEASE_SUBCOMMANDS: - log.error( - "Unknown release subcommand '%s'.\n valid: %s\n", - sub, - ", ".join(_RELEASE_SUBCOMMANDS), - ) - raise SystemExit(2) + if skip_or_relaunch_in_docker(cfg, "publish"): + return + + flavor = captain.flavor.create_and_setup_flavor_for_id(cfg.flavor_id, cfg) + + oci.publish( + cfg, + flavor, + target=str(cfg.release_target), + registry=str(cfg.release_registry), + repository=str(cfg.release_repository), + artifact_name=str(cfg.release_oci_artifact_name), + tag=str(cfg.release_src_tag), + force=cfg.force_release, + ) - # Handle --help / -h for the subcommand. - if "-h" in rest or "--help" in rest: - _print_release_subcmd_help(sub) - # --- validate required args early --------------------------------- - if sub == "tag" and not rest: - log.error("Missing version argument.") - _print_release_subcmd_help(sub, exit_code=2) - if sub == "pull" and not getattr(args, "pull_output", None): - log.error("--pull-output is required for 'release pull'.") - _print_release_subcmd_help(sub, exit_code=2) +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- - # --- skip --------------------------------------------------------- + +def skip_or_relaunch_in_docker(cfg: Config, subcommand: str) -> bool: + # --- skip mode -------------------------------------------------------- if cfg.release_mode == "skip": - log.info("RELEASE_MODE=skip — skipping release operation") - return + log.info("RELEASE_MODE=skip — nothing to do.") + return True - # --- docker ------------------------------------------------------- + # --- docker mode ------------------------------------------------------ if cfg.release_mode == "docker": - docker.build_release_image(cfg) - log.info("Running release %s (docker)...", sub) - # Forward release-specific env vars into the container. - registry = getattr(args, "registry", "ghcr.io") - repository = getattr(args, "repository", "tinkerbell/captain") - artifact_name = getattr(args, "oci_artifact_name", "artifacts") - sha = _resolve_git_sha(args, cfg.project_dir) - env_args: list[str] = [ - "-e", - f"KERNEL_VERSION={cfg.kernel_version}", - "-e", - f"REGISTRY={registry}", - "-e", - f"GITHUB_REPOSITORY={repository}", - "-e", - f"OCI_ARTIFACT_NAME={artifact_name}", - "-e", - f"GITHUB_SHA={sha}", - ] - exclude = getattr(args, "version_exclude", None) - if exclude: - env_args += ["-e", f"VERSION_EXCLUDE={exclude}"] - if sub in ("publish", "pull"): - target = getattr(args, "target", None) or cfg.arch - env_args += ["-e", f"TARGET={target}"] - if getattr(args, "force", False): - env_args += ["-e", "FORCE=true"] - pull_output = getattr(args, "pull_output", None) - - # Build the inner command. - inner_cmd = ["/work/build.py", "release", sub] - if pull_output: - inner_cmd += ["--pull-output", pull_output] - inner_cmd += list(rest) - - try: - docker.run_in_release( - cfg, - *env_args, - "--entrypoint", - "/usr/bin/uv", - docker.RELEASE_IMAGE, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), - "run", - *inner_cmd, - ) - except subprocess.CalledProcessError as exc: - raise SystemExit(exc.returncode) from None - paths_to_fix = ["/work/out"] - if pull_output: - container_pull_output = f"/work/{pull_output.lstrip('/')}" - paths_to_fix.append(container_pull_output) - docker.fix_docker_ownership(cfg, paths_to_fix) - return + from captain import docker - # --- native ------------------------------------------------------- - if cfg.release_mode == "native": - missing = check_release_dependencies() - if missing: - log.error("Missing release tools: %s", ", ".join(missing)) - log.error("Install them or set --release-mode=docker.") - raise SystemExit(1) - # Common OCI parameters. - registry = getattr(args, "registry", "ghcr.io") - repository = getattr(args, "repository", "tinkerbell/captain") - artifact_name = getattr(args, "oci_artifact_name", "artifacts") - exclude = getattr(args, "version_exclude", None) - sha = _resolve_git_sha(args, cfg.project_dir) - tag = oci.compute_version_tag(cfg.project_dir, sha, exclude=exclude) - - if sub == "publish": - target = getattr(args, "target", None) or cfg.arch - force = getattr(args, "force", False) - oci.publish( - cfg, - target=target, - registry=registry, - repository=repository, - artifact_name=artifact_name, - tag=tag, - sha=sha, - force=force, - ) + docker.obtain_builder(cfg) + docker.run_captain_in_builder(cfg, ["release", subcommand]) + docker.fix_docker_ownership(cfg, ["/work/out"]) + return True - elif sub == "pull": - target = getattr(args, "target", None) or cfg.arch - pull_output = getattr(args, "pull_output", None) - if pull_output is None: - log.error("--pull-output is required for 'release pull'.") - raise SystemExit(2) - oci.pull( - registry=registry, - repository=repository, - artifact_name=artifact_name, - tag=tag, - target=target, - output_dir=Path(pull_output), - ) + # --- native mode ------------------------------------------------------ + missing = check_release_dependencies() + if missing: + log.error("Missing release tools: %s", ", ".join(missing)) + log.error("Install them or set --release-mode=docker.") + raise SystemExit(1) - elif sub == "tag": - version = rest[0] - oci.tag_all( - registry=registry, - repository=repository, - artifact_name=artifact_name, - src_tag=tag, - new_tag=version, - ) + return False + + +def _autodetect_git_sha(project_dir: Path) -> str: + """Auto-detect via ``git rev-parse HEAD``.""" + return subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + text=True, + check=True, + cwd=project_dir, + ).stdout.strip() diff --git a/captain/cli/_shell.py b/captain/cli/_shell.py new file mode 100644 index 00000000..29c543cd --- /dev/null +++ b/captain/cli/_shell.py @@ -0,0 +1,28 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click + +import captain.docker as docker +from captain.cli._main import CliContext, cli + +log = logging.getLogger(__name__) + + +@cli.command( + "shell", + short_help="Get a shell in the builder environment.", +) +@click.pass_obj +def shell_cmd(cli_ctx: CliContext) -> None: + log.debug("Starting shell in docker...") + + cfg = cli_ctx.make_config() + + docker.obtain_builder(cfg) + docker.run_in_builder(cfg, command_and_args=["bash"], extra_docker_args=["-i", "-t"]) + + log.info("Shell stage complete!") diff --git a/captain/cli/_tools.py b/captain/cli/_tools.py new file mode 100644 index 00000000..7e5968e0 --- /dev/null +++ b/captain/cli/_tools.py @@ -0,0 +1,63 @@ +"""``captain tools`` — download tools (containerd, runc, nerdctl, CNI plugins).""" + +from __future__ import annotations + +import logging + +import click + +from captain.cli._main import CliContext, cli +from captain.cli.stages import _build_tools_stage + +log = logging.getLogger(__name__) + + +@cli.command( + "tools", + short_help="Download tools (containerd, runc, nerdctl, CNI).", +) +@click.option( + "--tools-mode", + envvar="TOOLS_MODE", + default="docker", + show_default=True, + type=click.Choice(["docker", "native", "skip"], case_sensitive=False), + metavar="MODE", + help="Tools download stage execution mode (docker, native, skip).", +) +@click.option( + "--force-tools", + envvar="FORCE_TOOLS", + is_flag=True, + default=False, + help="Re-download tools even if outputs already exist.", +) +@click.pass_obj +def tools_cmd( + cli_ctx: CliContext, + *, + tools_mode: str, + force_tools: bool, +) -> None: + """Download tools (containerd, runc, nerdctl, CNI plugins). + + Fetches pre-built binaries for the target architecture and stages + them under ``mkosi.input/tools/{arch}/``. The tools are later + merged into the initramfs by mkosi via ``--extra-tree``. + + \b + Examples + -------- + captain tools + captain tools --arch arm64 + captain tools --tools-mode native + captain tools --force-tools + """ + + cfg = cli_ctx.make_config( + tools_mode=tools_mode, + force_tools=force_tools, + ) + + _build_tools_stage(cfg) + log.info("Tools stage complete!") diff --git a/captain/cli/_stages.py b/captain/cli/stages.py similarity index 56% rename from captain/cli/_stages.py rename to captain/cli/stages.py index d15284e7..7eea15b4 100644 --- a/captain/cli/_stages.py +++ b/captain/cli/stages.py @@ -1,8 +1,10 @@ -"""Build stage orchestration — kernel, tools, mkosi, ISO.""" +"""Build stage orchestration — tools, mkosi, ISO.""" from __future__ import annotations import logging +import shutil +from pathlib import Path from captain import docker, iso, kernel, tools from captain.config import Config @@ -11,8 +13,9 @@ log = logging.getLogger(__name__) -def _build_kernel_stage(cfg: Config) -> None: +def build_kernel_stage(cfg: Config) -> None: """Run the kernel build stage according to *cfg.kernel_mode*.""" + log.debug("Building kernel stage in mode %s", cfg.kernel_mode) # --- skip --------------------------------------------------------- if cfg.kernel_mode == "skip": @@ -20,16 +23,15 @@ def _build_kernel_stage(cfg: Config) -> None: return # --- idempotency -------------------------------------------------- - modules_dir = cfg.modules_output / "usr" / "lib" / "modules" - vmlinuz_dir = cfg.kernel_output - has_vmlinuz = vmlinuz_dir.is_dir() and any(vmlinuz_dir.glob("vmlinuz-*")) - - if modules_dir.is_dir() and has_vmlinuz and not cfg.force_kernel: + image_deb_package = kernel.obtain_target_artifact_path(cfg) + log.debug("Checking for existing kernel artifact at %s", image_deb_package) + if image_deb_package.is_file() and not cfg.force_kernel: log.info("Kernel already built (use --force-kernel to rebuild)") return - if modules_dir.is_dir() and not has_vmlinuz: - log.warning("Modules exist but vmlinuz is missing — rebuilding kernel") + # Cleanup any existing outputs so that caches don't grow forever. + if cfg.kernel_output.is_dir(): + shutil.rmtree(cfg.kernel_output) # --- native ------------------------------------------------------- if cfg.kernel_mode == "native": @@ -43,22 +45,17 @@ def _build_kernel_stage(cfg: Config) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) - log.info("Building kernel (docker)...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "kernel", - ) + docker.obtain_builder(cfg) + log.info("Building kernel (docker relaunch)...") + if cfg.kernel_menuconfig: + log.info("Launching interactive kernel configuration (menuconfig)...") + docker.run_captain_in_builder(cfg, ["kernel"], extra_docker_args=["-i", "-t"]) + else: + docker.run_captain_in_builder(cfg, ["kernel"]) docker.fix_docker_ownership( cfg, [ - f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}", + f"/work/mkosi.input/kernel/{cfg.kernel_version}/{cfg.arch}", "/work/out", ], ) @@ -66,6 +63,7 @@ def _build_kernel_stage(cfg: Config) -> None: def _build_tools_stage(cfg: Config) -> None: """Run the tools download stage according to *cfg.tools_mode*.""" + log.debug("Building tools stage in mode %s", cfg.tools_mode) # --- skip --------------------------------------------------------- if cfg.tools_mode == "skip": @@ -74,24 +72,15 @@ def _build_tools_stage(cfg: Config) -> None: # --- native ------------------------------------------------------- if cfg.tools_mode == "native": - log.info("Downloading tools (nerdctl, containerd, etc.)...") + log.info("Downloading tools (nerdctl, containerd, etc.) native...") tools.download_all(cfg) return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) - log.info("Downloading tools (nerdctl, containerd, etc.)...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "tools", - ) - docker.fix_docker_ownership(cfg, ["/work/mkosi.output"]) + docker.obtain_builder(cfg) + log.info("Downloading tools (nerdctl, containerd, etc.) docker...") + docker.run_captain_in_builder(cfg, ["tools"]) + docker.fix_docker_ownership(cfg, ["/work/mkosi.input"]) def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: @@ -111,6 +100,27 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: mkosi_args = list(cfg.mkosi_args) + list(extra_args) + # Use a Rich Syntax and Rich Panel to display mkosi.conf before running it if debugging: + if log.isEnabledFor(logging.DEBUG): + from rich import print + from rich.panel import Panel + from rich.syntax import Syntax + + mkosi_conf_path = cfg.project_dir / "mkosi.conf" + if mkosi_conf_path.is_file(): + mkosi_conf_content = mkosi_conf_path.read_text() + syntax = Syntax( + mkosi_conf_content, + "ini", + theme="monokai", + line_numbers=True, + background_color="default", + ) + panel = Panel(syntax, title="mkosi.conf", border_style="blue") + print(panel) + else: + log.debug("No mkosi.conf found at %s", mkosi_conf_path) + # --- native ------------------------------------------------------- if cfg.mkosi_mode == "native": missing = check_mkosi_dependencies() @@ -120,14 +130,12 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: raise SystemExit(1) log.info("Building initrd with mkosi (native)...") tools_tree = str(cfg.tools_output) - modules_tree = str(cfg.modules_output) output_dir = str(cfg.initramfs_output) run( [ "mkosi", f"--architecture={cfg.arch_info.mkosi_arch}", f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", f"--output-dir={output_dir}", "build", *mkosi_args, @@ -137,15 +145,13 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) + docker.obtain_builder(cfg) log.info("Building initrd with mkosi (docker)...") - tools_tree = f"/work/mkosi.output/tools/{cfg.arch}" - modules_tree = f"/work/mkosi.output/kernel/{cfg.kernel_version}/{cfg.arch}/modules" - output_dir = f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}" - docker.run_mkosi( + tools_tree = f"/work/mkosi.input/tools/{cfg.arch}" # @TODO ooops + output_dir = cfg.calc_initramfs_output(Path("/work"), "initramfs") + docker.run_mkosi_in_builder( cfg, f"--extra-tree={tools_tree}", - f"--extra-tree={modules_tree}", f"--output-dir={output_dir}", "--package-cache-dir=/cache/packages", "build", @@ -154,12 +160,38 @@ def _build_mkosi_stage(cfg: Config, extra_args: list[str]) -> None: docker.fix_docker_ownership( cfg, [ - f"/work/mkosi.output/initramfs/{cfg.kernel_version}/{cfg.arch}", + f"/work/mkosi.output/initramfs/{cfg.flavor_id}/{cfg.arch}", "/work/out", ], ) +def _prune_stale_output_dirs(cfg: Config) -> None: + """Drop sibling flavor-hash output dirs, keeping only the current hash. + + Caches (``mkosi.output/initramfs`` / ``mkosi.output/iso``) are keyed on the + whole tree but outputs live under ``...///``. + A restore-key (prefix) cache hit brings in the *previous* hash's dir, which + would otherwise be re-saved alongside the freshly-built one and accumulate + forever. When enabled, prune everything but the current hash so the cache + only contains the just-built version. + + Opt-in via ``--drop-old-caches`` / ``CAPTAIN_DROP_OLD_CACHES``; a no-op + otherwise so local builds keep prior versions around. + """ + if not cfg.drop_old_caches: + return + + for current in (cfg.initramfs_output, cfg.iso_output): + parent = current.parent # .../// + if not parent.is_dir(): + continue + for child in parent.iterdir(): + if child.is_dir() and child.name != current.name: + log.info("Dropping stale output dir %s", child) + shutil.rmtree(child) + + def _build_iso_stage(cfg: Config) -> None: """Run the ISO build stage according to *cfg.iso_mode*.""" @@ -169,7 +201,7 @@ def _build_iso_stage(cfg: Config) -> None: return # --- idempotency -------------------------------------------------- - iso_path = cfg.iso_output / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_path = cfg.iso_output / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" if iso_path.is_file() and not cfg.force_iso: log.info("ISO already built: %s (use --force-iso to rebuild)", iso_path) return @@ -181,18 +213,9 @@ def _build_iso_stage(cfg: Config) -> None: return # --- docker ------------------------------------------------------- - docker.build_builder(cfg) + docker.obtain_builder(cfg) log.info("Building ISO (docker)...") - docker.run_in_builder( - cfg, - "--entrypoint", - "/usr/bin/uv", - cfg.builder_image, - *(["--verbose"] if logging.getLogger().isEnabledFor(logging.DEBUG) else []), - "run", - "/work/build.py", - "iso", - ) + docker.run_captain_in_builder(cfg, ["iso"]) docker.fix_docker_ownership( cfg, [ diff --git a/captain/config.py b/captain/config.py index a8ad0619..690b91a3 100644 --- a/captain/config.py +++ b/captain/config.py @@ -2,9 +2,7 @@ from __future__ import annotations -import argparse import logging -import os import sys from dataclasses import dataclass, field from pathlib import Path @@ -16,9 +14,15 @@ # Valid values for KERNEL_MODE and MKOSI_MODE. VALID_MODES = ("docker", "native", "skip") -# The single source of truth for the default kernel version. +# The single source of truth for the default flavor. +# Override at runtime via --flavor-id or FLAVOR_ID env var. +DEFAULT_FLAVOR_ID = "trixie-full" + +# Fallback for the default kernel version. +# The latest version can be obtained and passed as --kernel-version/KERNEL_VERSION +# via <@TODO cmd for listing from cached releases.json # Override at runtime via --kernel-version or KERNEL_VERSION env var. -DEFAULT_KERNEL_VERSION = "6.18.16" +DEFAULT_KERNEL_VERSION = "6.18.39" @dataclass(slots=True) @@ -27,129 +31,87 @@ class Config: # Paths project_dir: Path - output_dir: Path + + # "across invocations" / "GHA interface dir"; project_dir / "out" + output_dir: Path # target of collect_xx() stuff, coming from mkosi.output # Target arch: str = "amd64" + flavor_id: str = DEFAULT_FLAVOR_ID + flavor_hash: str | None = None # resolved at runtime + + # For kernel build + build_kernel: bool = False kernel_version: str = DEFAULT_KERNEL_VERSION - kernel_config: str | None = None - kernel_src: str | None = None + force_kernel: bool = False + kernel_clean: bool = False + kernel_menuconfig: bool = False + + # Armbian repo version token (ETag proxy) folded into armbian flavors' hash + # so they rebuild when armbian-next publishes a newer kernel. Empty/None = + # no cache-bust (default; stable hash for local builds). + armbian_version: str | None = None # Docker + builder_registry: str | None = None + builder_repository: str | None = None builder_image: str = "captainos-builder" - no_cache: bool = False + builder_push: bool = False + verbose_docker: bool = False # Pass --verbose to captain when re-launched in Docker # Per-stage mode: "docker" | "native" | "skip" - kernel_mode: str = "docker" - tools_mode: str = "docker" + tools_mode: str = "native" mkosi_mode: str = "docker" iso_mode: str = "docker" release_mode: str = "docker" + kernel_mode: str = "docker" # Force flags - force_kernel: bool = False force_tools: bool = False force_iso: bool = False + force_release: bool = False + + # Prune sibling flavor_hash output dirs after a build, so caches/artifacts + # only contain the freshly-built version (opt-in; used in CI). + drop_old_caches: bool = False # QEMU - qemu_append: str = "" qemu_mem: str = "2G" qemu_smp: str = "2" + # Release related + release_registry: str | None = None + release_repository: str | None = None + release_oci_artifact_name: str | None = None + release_target: str | None = None + release_git_sha: str | None = None + release_src_tag: str | None = None + release_pull_output: Path | None = None + release_new_tag: str | None = None + # mkosi passthrough mkosi_args: list[str] = field(default_factory=list) # Derived (set in __post_init__) arch_info: ArchInfo = field(init=False) + # Call uv (eg in Docker) using its own --verbose flag + verbose_uv: bool = False + def __post_init__(self) -> None: self.arch_info = get_arch_info(self.arch) self.arch = self.arch_info.arch # normalise aliases (x86_64 → amd64, etc.) for name, value in ( - ("KERNEL_MODE", self.kernel_mode), ("TOOLS_MODE", self.tools_mode), ("MKOSI_MODE", self.mkosi_mode), ("ISO_MODE", self.iso_mode), ("RELEASE_MODE", self.release_mode), + ("KERNEL_MODE", self.kernel_mode), ): if value not in VALID_MODES: log.error("%s=%r is invalid. Valid values: %s", name, value, ", ".join(VALID_MODES)) sys.exit(1) - @property - def needs_docker(self) -> bool: - """True if any stage requires Docker.""" - return ( - self.kernel_mode == "docker" - or self.tools_mode == "docker" - or self.mkosi_mode == "docker" - or self.iso_mode == "docker" - or self.release_mode == "docker" - ) - - @classmethod - def from_args(cls, args: argparse.Namespace, project_dir: Path) -> Config: - """Create a Config from a parsed :class:`argparse.Namespace`. - - The *args* namespace is produced by :mod:`configargparse` which - has already resolved the priority chain: - CLI flags > environment variables > defaults. - - ``getattr`` with fallbacks is used because per-subcommand - parsers only define the flags relevant to that subcommand. - """ - return cls( - project_dir=project_dir, - output_dir=project_dir / "out", - arch=getattr(args, "arch", "amd64"), - kernel_version=getattr(args, "kernel_version", DEFAULT_KERNEL_VERSION), - kernel_config=getattr(args, "kernel_config", None) or None, - kernel_src=getattr(args, "kernel_src", None) or None, - builder_image=getattr(args, "builder_image", "captainos-builder"), - no_cache=getattr(args, "no_cache", False), - kernel_mode=getattr(args, "kernel_mode", "docker"), - tools_mode=getattr(args, "tools_mode", "docker"), - mkosi_mode=getattr(args, "mkosi_mode", "docker"), - iso_mode=getattr(args, "iso_mode", "docker"), - release_mode=getattr(args, "release_mode", "docker"), - force_kernel=getattr(args, "force_kernel", False), - force_tools=getattr(args, "force_tools", False), - force_iso=getattr(args, "force_iso", False), - qemu_append=getattr(args, "qemu_append", ""), - qemu_mem=getattr(args, "qemu_mem", "2G"), - qemu_smp=getattr(args, "qemu_smp", "2"), - ) - - @classmethod - def from_env(cls, project_dir: Path) -> Config: - """Create a Config from environment variables (legacy helper). - - Prefer :meth:`from_args` in the CLI path. This method remains - for any non-CLI callers (e.g. tests, scripts) that need a - ``Config`` without going through argparse. - """ - return cls( - project_dir=project_dir, - output_dir=project_dir / "out", - arch=os.environ.get("ARCH", "amd64"), - kernel_version=os.environ.get("KERNEL_VERSION", DEFAULT_KERNEL_VERSION), - kernel_config=os.environ.get("KERNEL_CONFIG") or None, - kernel_src=os.environ.get("KERNEL_SRC") or None, - builder_image=os.environ.get("BUILDER_IMAGE", "captainos-builder"), - no_cache=os.environ.get("NO_CACHE") == "1", - kernel_mode=os.environ.get("KERNEL_MODE", "docker"), - tools_mode=os.environ.get("TOOLS_MODE", "docker"), - mkosi_mode=os.environ.get("MKOSI_MODE", "docker"), - iso_mode=os.environ.get("ISO_MODE", "docker"), - release_mode=os.environ.get("RELEASE_MODE", "docker"), - force_kernel=os.environ.get("FORCE_KERNEL") == "1", - force_tools=os.environ.get("FORCE_TOOLS") == "1", - force_iso=os.environ.get("FORCE_ISO") == "1", - qemu_append=os.environ.get("QEMU_APPEND", ""), - qemu_mem=os.environ.get("QEMU_MEM", "2G"), - qemu_smp=os.environ.get("QEMU_SMP", "2"), - ) - @property def tools_output(self) -> Path: """Per-arch staging directory for downloaded tools. @@ -158,7 +120,7 @@ def tools_output(self) -> Path: Passed to mkosi via ``--extra-tree=`` to merge into the initramfs. Kernel modules are stored separately under :attr:`kernel_output`. """ - return self.project_dir / "mkosi.output" / "tools" / self.arch + return self.project_dir / "mkosi.input" / "tools" / self.arch @property def kernel_output(self) -> Path: @@ -169,33 +131,38 @@ def kernel_output(self) -> Path: (``usr/lib/modules/{kver}/``) so it can be passed directly as an ``--extra-tree=`` to mkosi. """ - return self.project_dir / "mkosi.output" / "kernel" / self.kernel_version / self.arch - - @property - def modules_output(self) -> Path: - """Per-version, per-arch root for kernel modules. - - Returns ``kernel/{version}/{arch}/modules`` which contains a - merged-usr tree (``usr/lib/modules/{kver}/``) suitable for - passing as ``--extra-tree=`` to mkosi. - """ - return self.kernel_output / "modules" - - @property - def mkosi_output(self) -> Path: - return self.project_dir / "mkosi.output" + return self.project_dir / "mkosi.input" / "kernel" / self.arch @property def initramfs_output(self) -> Path: """Per-version, per-arch directory for mkosi initramfs output.""" - return self.project_dir / "mkosi.output" / "initramfs" / self.kernel_version / self.arch + base_dir: Path = self.project_dir + return self.calc_initramfs_output(base_dir, "initramfs") @property def iso_output(self) -> Path: """Per-version, per-arch directory for the built ISO image.""" - return self.project_dir / "mkosi.output" / "iso" / self.kernel_version / self.arch + base_dir: Path = self.project_dir + return self.calc_initramfs_output(base_dir, "iso") + + def calc_initramfs_output(self, base_dir: Path, which: str) -> Path: + log.debug("Asked for %s output dir; flavor_hash=%r", which, self.flavor_hash) + # To know the output dir, we must know the flavor_hash; make sure. + if self.flavor_hash is None: + log.error("flavor_hash is not set; cannot determine initramfs output directory") + raise ValueError("flavor_hash is not set; cannot determine initramfs output directory") + + flavor_id = ( + base_dir / "mkosi.output" / which / self.flavor_id / self.arch / self.flavor_hash + ) + return flavor_id @property def iso_staging(self) -> Path: """Per-version, per-arch staging directory for assembling the ISO filesystem.""" return self.iso_output / "staging" + + @property + def custom_dir(self) -> Path: + """Directory for custom user files""" + return self.project_dir / "custom" diff --git a/captain/docker.py b/captain/docker.py index 0aaf9211..be933fb7 100644 --- a/captain/docker.py +++ b/captain/docker.py @@ -6,11 +6,15 @@ import logging import os import platform -import sys from pathlib import Path +from subprocess import CalledProcessError +from rich.table import Table + +import captain from captain.config import Config -from captain.util import run +from captain.kernel import KERNEL_BUILD_BASE_PATH +from captain.util import detect_current_machine_arch, run log = logging.getLogger(__name__) @@ -29,165 +33,216 @@ def _dockerfile_hash(cfg: Config) -> str: """Return the SHA-256 hex digest of the Dockerfile content. This is used as an image tag so that Dockerfile changes are detected - automatically. The value intentionally matches what GitHub Actions - ``hashFiles('Dockerfile')`` produces, allowing the CI - ``docker/build-push-action`` step to pre-load an image with the same - tag that ``build_builder`` will look for. + automatically. """ dockerfile = cfg.project_dir / "Dockerfile" - return hashlib.sha256(dockerfile.read_bytes()).hexdigest() + local_arch = detect_current_machine_arch() + hex_digest = hashlib.sha256(dockerfile.read_bytes()).hexdigest() + return f"{local_arch}-{hex_digest}" -def build_builder(cfg: Config) -> None: +def obtain_builder(cfg: Config) -> None: """Build the Docker builder image when the Dockerfile has changed. The image is tagged with a content hash of the Dockerfile so that changes are detected even when the base image name stays the same. - When the matching tag already exists locally (e.g. pre-loaded by a CI - ``docker/build-push-action`` step with ``load: true``), we skip the - build entirely. Use ``NO_CACHE=1`` to force a full rebuild. """ tag = _dockerfile_hash(cfg) - tagged_image = f"{cfg.builder_image}:{tag}" + remote_tagged_image = f"{cfg.builder_registry}/{cfg.builder_repository}/builder:{tag}" + local_tagged_image = f"{cfg.builder_image}:{tag}" + + log.debug( + "Checking for existing builder image with tag '%s' or remote image '%s'", + local_tagged_image, + remote_tagged_image, + ) - if not cfg.no_cache and _image_exists(tagged_image): - log.info("Docker image '%s' is up to date.", cfg.builder_image) + if _image_exists(local_tagged_image): + log.info("Docker image '%s' is up to date with %s.", cfg.builder_image, local_tagged_image) # Ensure the un-hashed tag exists so later docker-run calls that # reference cfg.builder_image (without the hash suffix) succeed. - # This matters when the hashed tag was pre-loaded by CI. - run(["docker", "tag", tagged_image, cfg.builder_image], check=False) + run(["docker", "tag", local_tagged_image, cfg.builder_image], check=False) return - log.info("Building Docker image '%s'...", cfg.builder_image) - cmd = ["docker", "buildx", "build", "--load"] - if cfg.no_cache: - cmd.append("--no-cache") - cmd.extend( - ["--progress=plain", "-t", tagged_image, "-t", cfg.builder_image, str(cfg.project_dir)] - ) - run(cmd) - - -RELEASE_IMAGE = "captainos-release" - - -def _release_dockerfile_hash(cfg: Config) -> str: - """Return the SHA-256 hex digest of the Dockerfile.release content.""" - dockerfile = cfg.project_dir / "Dockerfile.release" - return hashlib.sha256(dockerfile.read_bytes()).hexdigest() - - -def build_release_image(cfg: Config) -> None: - """Build the release Docker image from ``Dockerfile.release``.""" - tag = _release_dockerfile_hash(cfg) - tagged_image = f"{RELEASE_IMAGE}:{tag}" - - if not cfg.no_cache and _image_exists(tagged_image): - log.info("Docker image '%s' is up to date.", RELEASE_IMAGE) - run(["docker", "tag", tagged_image, RELEASE_IMAGE]) + # Check if the remote name exists locally... (was pre-pulled somehow) + if _image_exists(remote_tagged_image): + log.info( + "Docker image '%s' already exists locally (pre-pulled). Tagging as '%s'.", + remote_tagged_image, + cfg.builder_image, + ) + run(["docker", "tag", remote_tagged_image, cfg.builder_image], check=False) return - log.info("Building Docker image '%s'...", RELEASE_IMAGE) - cmd = ["docker", "buildx", "build", "--load", "-f", str(cfg.project_dir / "Dockerfile.release")] - if cfg.no_cache: - cmd.append("--no-cache") - cmd.extend(["--progress=plain"]) - cmd.extend(["-t", tagged_image, "-t", RELEASE_IMAGE, str(cfg.project_dir)]) - run(cmd) - + # Check if we can pull the remote image (exists in registry and matches our Dockerfile hash) + if ( + run( + ["docker", "pull", remote_tagged_image], + check=False, + capture=False, + ).returncode + == 0 + ): + log.info( + "Pulled Docker image '%s' from registry. Tagging as '%s'.", + remote_tagged_image, + cfg.builder_image, + ) + run(["docker", "tag", remote_tagged_image, cfg.builder_image], check=False) + return -def run_in_release(cfg: Config, *extra_args: str) -> None: - """Run a command inside the release container. + # build locally if no existing image was found. + log.info("Building Docker image '%s'...", cfg.builder_image) + run( + [ + "docker", + "buildx", + "build", + "--load", # for older Docker versions + "--progress=plain", + "-t", + local_tagged_image, + "-t", + cfg.builder_image, + str(cfg.project_dir), + ] + ) - Similar to :func:`run_in_builder` but uses the lightweight release - image which has buildah, skopeo, Python, and git. - """ - docker_args: list[str] = [ - "docker", - "run", - "--rm", - # Buildah needs mount/remount capabilities for layer operations. - "--privileged", - # interactive if running in a terminal - *(["-i"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "-t", # terminal - "-v", - f"{cfg.project_dir}:/work", - "-w", - "/work", - "-e", - f"ARCH={cfg.arch}", - "-e", - "RELEASE_MODE=native", - # Chroot isolation lets buildah work inside an unprivileged container - # (no user namespaces needed — we only assemble scratch images). - "-e", - "BUILDAH_ISOLATION=chroot", - "-e", - f"TERM={os.environ.get('TERM', 'xterm-256color')}", - "-e", - f"COLUMNS={os.environ.get('COLUMNS', '200')}", - "-e", - f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", - ] - # Forward host registry credentials so buildah/skopeo can authenticate. - # The caller sets these env vars on the host (e.g. via docker login or - # CI secrets); they are passed through to the container as-is. - for var in ("REGISTRY_AUTH_FILE", "REGISTRY_USERNAME", "REGISTRY_PASSWORD"): - val = os.environ.get(var) - if val: - docker_args += ["-e", f"{var}={val}"] - docker_args.extend(extra_args) - run(docker_args) - - -def run_in_builder(cfg: Config, *extra_args: str) -> None: + # Show the layer size distribution for the built image to help with debugging and optimization. + log.info("Docker image '%s' built successfully. Layer size distribution:", local_tagged_image) + layer_sizes_lines = run( + [ + "docker", + "history", + "--no-trunc", + "--format", + "-> {{.Size}} :: '{{.CreatedBy}}'", + local_tagged_image, + ], + capture=True, + check=True, + ) + layers = [] + for line in layer_sizes_lines.stdout.strip().splitlines(): + line = line.strip() + if line.startswith("-> "): + # remove double whitespace chars to make it easier to read + line = " ".join(line.split()) + layers.append(line) + # reverse the array to match the order + layers.reverse() + for layer in layers: + log.info("Layer info: %s", layer) + + # Optionally push the image after building it + if cfg.builder_push: + log.info( + "Pushing Docker image '%s' to registry as '%s'...", + cfg.builder_image, + remote_tagged_image, + ) + run(["docker", "tag", local_tagged_image, remote_tagged_image], check=False) + run(["docker", "push", remote_tagged_image]) + + +def run_in_builder( + cfg: Config, command_and_args: list[str], extra_docker_args: list[str] | None = None +) -> None: """Run a command inside the Docker builder container. *extra_args* are appended after the docker run flags and image name. """ + + docker_envs: dict[str, str] = { + # common to all commands + "ARCH": cfg.arch, + "FLAVOR_ID": cfg.flavor_id, + "KERNEL_VERSION": cfg.kernel_version, + "FORCE_TOOLS": str(int(cfg.force_tools)), + "FORCE_ISO": str(int(cfg.force_iso)), + "FORCE_KERNEL": f"{int(cfg.force_kernel)!s}", + "FORCE_RELEASE": str(cfg.force_release), + "CAPTAIN_VERBOSE": "1" if cfg.verbose_docker else "0", + "CONFIG_KERNEL": "1" if cfg.kernel_menuconfig else "0", + # publish-related env vars + "BUILDAH_ISOLATION": "chroot", + "REGISTRY_INSECURE": os.environ.get("REGISTRY_INSECURE", ""), + "REGISTRY": str(cfg.release_registry), + "GITHUB_REPOSITORY": str(cfg.release_repository), + "OCI_ARTIFACT_NAME": str(cfg.release_oci_artifact_name), + "TARGET": str(cfg.release_target), + "GIT_SHA": str(cfg.release_git_sha), + "SRC_TAG": str(cfg.release_src_tag), + "PULL_OUTPUT": str(cfg.release_pull_output), + "NEW_TAG": str(cfg.release_new_tag), + # set all modes to native under Docker + "KERNEL_MODE": "native", + "RELEASE_MODE": "native", + "TOOLS_MODE": "native", + "MKOSI_MODE": "native", + "ISO_MODE": "native", + # terminal control / gha stuff etc + "TERM": os.environ.get("TERM", "xterm-256color"), + "FORCE_COLOR": "1", + "COLUMNS": str(captain.env_columns), + "GITHUB_ACTIONS": os.environ.get("GITHUB_ACTIONS", ""), + "CAPTAIN_IN_DOCKER": "docker", + # Forward host registry credentials so buildah/skopeo can authenticate. + # The caller sets these env vars on the host (e.g. via docker login or + # CI secrets); they are passed through to the container as-is. + "REGISTRY_AUTH_FILE": os.environ.get("REGISTRY_AUTH_FILE", ""), + "REGISTRY_USERNAME": os.environ.get("REGISTRY_USERNAME", ""), + "REGISTRY_PASSWORD": os.environ.get("REGISTRY_PASSWORD", ""), + } + docker_args: list[str] = [ "docker", "run", "--rm", - "--privileged", - # interactive if running in a terminal - *(["-i"] if sys.stdout.isatty() and sys.stdin.isatty() else []), - "-t", # terminal + "--privileged", # yes, this is required for both buildah and mkosi in the container "-w", "/work", - "-e", - f"ARCH={cfg.arch}", - "-e", - f"KERNEL_VERSION={cfg.kernel_version}", - "-e", - f"FORCE_TOOLS={int(cfg.force_tools)}", - "-e", - f"FORCE_KERNEL={int(cfg.force_kernel)}", - "-e", - f"FORCE_ISO={int(cfg.force_iso)}", - "-e", - "KERNEL_MODE=native", - "-e", - "TOOLS_MODE=native", - "-e", - "MKOSI_MODE=native", - "-e", - "ISO_MODE=native", - "-e", - "RELEASE_MODE=native", - "-e", - f"TERM={os.environ.get('TERM', 'xterm-256color')}", - "-e", - f"COLUMNS={os.environ.get('COLUMNS', '200')}", - "-e", - f"GITHUB_ACTIONS={os.environ.get('GITHUB_ACTIONS', '')}", ] + if extra_docker_args is not None: + log.debug("Adding extra Docker args: %s", extra_docker_args) + docker_args.extend(extra_docker_args) + + if log.isEnabledFor(logging.DEBUG): + table = Table( + title="Docker Environment Variables", show_header=True, header_style="bold cyan" + ) + table.add_column("Environment Variable", style="green") + table.add_column("Value", style="yellow") + for key, value in sorted(docker_envs.items()): + table.add_row(key, value) + captain.console.print(table) + + for k, v in docker_envs.items(): + docker_args += ["-e", f"{k}={v}"] + + if os.environ.get("GITHUB_ACTIONS") == "true": + log.debug("Running in GitHub Actions, adding bind mount for Docker credentials...") + github_docker_config = Path.home() / ".docker" / "config.json" + if github_docker_config.exists() and github_docker_config.is_file(): + log.debug( + "Under GHA, Found Docker config file at %s, adding to bind mounts.", + github_docker_config, + ) + docker_args += ["-v", f"{github_docker_config}:/root/.docker/config.json"] + + if cfg.custom_dir.exists() and cfg.custom_dir.is_dir(): + docker_args += ["-v", f"{cfg.custom_dir}:/work/custom"] + + docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.output:/work/mkosi.output"] - docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.input:/work/mkosi.input"] docker_args += ["-v", f"{cfg.project_dir}/out:/work/out"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.extra:/work/mkosi.extra"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.sandbox:/work/mkosi.sandbox"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.skeleton:/work/mkosi.skeleton"] + docker_args += ["-v", f"{cfg.project_dir}/mkosi.conf:/work/mkosi.conf"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.finalize:/work/mkosi.finalize"] docker_args += ["-v", f"{cfg.project_dir}/mkosi.postinst:/work/mkosi.postinst"] @@ -196,45 +251,53 @@ def run_in_builder(cfg: Config, *extra_args: str) -> None: docker_args += ["-v", f"{cfg.project_dir}/pyproject.toml:/work/pyproject.toml"] docker_args += ["-v", f"{cfg.project_dir}/build.py:/work/build.py"] - docker_args += ["-v", f"{cfg.project_dir}/kernel.configs:/work/kernel.configs"] - docker_args += ["--mount", "type=volume,source=captain-cache-packages,target=/cache/packages"] + docker_args += [ + "--mount", + f"type=volume,source=captain-kernel-build,target={KERNEL_BUILD_BASE_PATH}", + ] + docker_args += [cfg.builder_image] + + docker_args.extend(command_and_args) + + # Handle exceptions: when re-launching in Docker, the inner instance already showed + # the full error with traceback, so we can suppress the redundant outer traceback. + try: + run(docker_args) + except CalledProcessError as e: + log.error( + "Command '%s' failed with exit code %d.", " ".join(command_and_args), e.returncode + ) + raise SystemExit(e.returncode) from None + - # Mount kernel source if provided - if cfg.kernel_src is not None: - kernel_src_path = Path(cfg.kernel_src).resolve() - if not kernel_src_path.is_dir(): - log.error("KERNEL_SRC=%s does not exist", cfg.kernel_src) - raise SystemExit(1) - docker_args.extend(["-v", f"{kernel_src_path}:/work/kernel-src:ro"]) - docker_args.extend(["-e", "KERNEL_SRC=/work/kernel-src"]) - - # Mount kernel config override and point KERNEL_CONFIG to the container path - if cfg.kernel_config is not None: - kernel_cfg_path = Path(cfg.kernel_config) - if not kernel_cfg_path.is_absolute(): - kernel_cfg_path = (cfg.project_dir / kernel_cfg_path).resolve() - else: - kernel_cfg_path = kernel_cfg_path.resolve() - if not kernel_cfg_path.is_file(): - log.error("KERNEL_CONFIG=%s does not exist", cfg.kernel_config) - raise SystemExit(1) - docker_args.extend(["-v", f"{kernel_cfg_path}:/work/kernel-config:ro"]) - docker_args.extend(["-e", "KERNEL_CONFIG=/work/kernel-config"]) - - docker_args.extend(extra_args) - log.debug("Docker args (builder): %s", docker_args) - run(docker_args) - - -def run_mkosi(cfg: Config, *mkosi_args: str) -> None: +def run_captain_in_builder( + cfg: Config, command_and_args: list[str], extra_docker_args: list[str] | None = None +): + log.debug("Running 'captain %s' in builder container...", command_and_args) + run_in_builder( + cfg, + [ + "/usr/bin/uv", + *(["--verbose"] if cfg.verbose_uv else ["--quiet"]), + "run", + "captain", + *command_and_args, + ], + extra_docker_args=extra_docker_args, + ) + + +def run_mkosi_in_builder(cfg: Config, *mkosi_args: str) -> None: """Run mkosi inside the builder container.""" ensure_binfmt(cfg) run_in_builder( cfg, - cfg.builder_image, - f"--architecture={cfg.arch_info.mkosi_arch}", - *mkosi_args, + command_and_args=[ + "/usr/local/bin/mkosi", + f"--architecture={cfg.arch_info.mkosi_arch}", + *mkosi_args, + ], ) diff --git a/captain/flavor.py b/captain/flavor.py new file mode 100644 index 00000000..ce63a441 --- /dev/null +++ b/captain/flavor.py @@ -0,0 +1,314 @@ +"""Flavor-specific configuration.""" + +from __future__ import annotations + +import hashlib +import logging +import shutil +from abc import abstractmethod +from pathlib import Path +from typing import Protocol, runtime_checkable + +import jinja2 +from rich.table import Table + +import captain +from captain.artifacts import OutputArchArtifact, OutputArchArtifactType +from captain.config import Config + +log = logging.getLogger(__name__) + + +@runtime_checkable +class BaseFlavor(Protocol): + cfg: Config + id: str + name: str + description: str + flavor_dir: Path + supported_architectures: frozenset[str] + template_map: dict[str, list[Path]] + static_map: dict[str, Path] + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + if cfg is None: + raise ValueError("cfg (Config) cannot be None") + self.cfg = cfg + + if flavor_dir is None: + raise ValueError("flavor_dir (Path) cannot be None") + if not flavor_dir.is_dir(): + raise ValueError(f"flavor_dir {flavor_dir} does not exist or is not a directory") + self.flavor_dir = flavor_dir + + self.template_map = {} + self.static_map = {} + log.debug("Called BaseFlavor.setup()...") + pass + + def generate(self, hash_only: bool = False): + log.debug("Called BaseFlavor.generate()...") + # Before generating, cleanup known targets. @TODO make dir disposable instead + log.debug("Cleaning up old generated files in %s", self.cfg.project_dir) + shutil.rmtree(self.cfg.project_dir / "mkosi.conf", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.postinst", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.finalize", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.extra", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.sandbox", ignore_errors=True) + shutil.rmtree(self.cfg.project_dir / "mkosi.skeleton", ignore_errors=True) + + hashes: dict[str, str] = {} + self.copy_static_files(self.cfg.project_dir, hash_only, hashes) + self.render_templates(self.cfg.project_dir, hash_only, hashes) # For compatibility + + self.cfg.flavor_hash = hashlib.sha256( + "".join(f"{k}={v}" for k, v in (sorted(hashes.items()))).encode() + ).hexdigest()[:16] + log.info( + "Generated flavor_hash: %s (based on %s hashes)", self.cfg.flavor_hash, len(hashes) + ) + + # Emit a Rich Table of the generated files and their hashes + if log.isEnabledFor(logging.DEBUG): + table = Table( + title="Generated Files and Hashes", show_header=True, header_style="bold magenta" + ) + table.add_column("Relative Path", style="dim", width=40) + table.add_column("SHA256 Hash", style="dim", width=64) + for relative_path, hash_value in hashes.items(): + table.add_row(relative_path, hash_value) + captain.console.print(table) + + pass + + def specific_flavor_dir(self, flavor_id: str) -> Path: + flavor_id_underscore = flavor_id.replace("-", "_") + flavor_dir = self.cfg.project_dir / "captain" / "flavors" / flavor_id_underscore + + if not flavor_dir.is_dir(): + log.error( + "Specific Flavor dir '%s' not found. Expected to find directory %s", + flavor_id, + flavor_dir, + ) + raise SystemExit(1) + return flavor_dir + + def add_static_dir(self, dir_to_include: str, flavor_dir: Path, prefix_with: str | None = None): + extra_dir = flavor_dir / dir_to_include + if extra_dir.exists() and extra_dir.is_dir(): + for extra_file in extra_dir.rglob("*"): + if extra_file.is_file(): + relative_path = extra_file.relative_to(flavor_dir) + if prefix_with is not None: + relative_path = Path(prefix_with) / extra_file.relative_to(extra_dir) + self.static_map[str(relative_path)] = extra_file + + def render_templates( + self, output_dir: Path, hash_only: bool = False, hashes: dict[str, str] | None = None + ): + log.debug("Called BaseFlavor.render_templates() with output_dir: %s", output_dir) + # Use jinja2 to render all templates in self.template_map, writing output to output_dir + # The keys of self.template_map are the relative output paths (e.g. "mkosi.conf"), and the + # values are lists of Path objects pointing to Jinja2 template files. + # If more than one template is provided for a given output path, they should be rendered + # in order and concatenated together to produce the final output file. + for relative_output_path, template_paths in self.template_map.items(): + log.debug( + "Rendering templates for output path '%s': %s", + relative_output_path, + template_paths, + ) + rendered_content = "" + for template_path in template_paths: + log.debug("Rendering template %s", template_path) + # Here you would load the template file, render it with the appropriate context + # (e.g. using Jinja2), and append the rendered content to rendered_content. + # For example: + template = jinja2.Environment( + loader=jinja2.FileSystemLoader(template_path.parent), + undefined=jinja2.StrictUndefined, + ).get_template(template_path.name) + rendered_content += template.render(cfg=self.cfg, flavor=self) + "\n" + + if hashes is not None: + hashes[relative_output_path] = hashlib.sha256(rendered_content.encode()).hexdigest() + if hash_only: + continue # Skip writing if we're only interested in hashes + + output_file_path = output_dir / relative_output_path + log.debug("Writing rendered content to %s", output_file_path) + output_file_path.parent.mkdir(parents=True, exist_ok=True) + output_file_path.write_text(rendered_content) + + # Make output_file executable @TODO: we will need a way to tell + output_file_path.chmod(output_file_path.stat().st_mode | 0o111) + + def copy_static_files( + self, project_dir, hash_only: bool = False, hashes: dict[str, str] | None = None + ): + # Do a plain copy of all files in self.static_map to project_dir / relative_path, where + # relative_path is the key in self.static_map + for relative_path, source_path in self.static_map.items(): + if hashes is not None: + hashes[relative_path] = hashlib.sha256(source_path.read_bytes()).hexdigest() + if hash_only: + continue # Skip copying if we're only interested in hashes + destination_path = project_dir / relative_path + log.debug("Copying static file from '%s' to '%s'", source_path, destination_path) + destination_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_path, destination_path) + + def cache_bust_token(self) -> str: + """Token rendered into mkosi.conf so a flavor's hash changes on demand. + + Base flavors return an empty string (stable hash). Flavors whose upstream + packages move under a stable name (e.g. armbian meta-packages) override + this to fold a repo version token in, forcing a rebuild when it changes. + """ + return "" + + @abstractmethod + def has_iso(self) -> bool: + return False + + def pre_mkosi_stage(self): + pass + + def post_mkosi_stage(self): + pass + + def post_artifact_collect(self): + pass + + def list_arch_artifacts(self, output_arch: str) -> list[OutputArchArtifact]: + artifacts: list[OutputArchArtifact] = [ + OutputArchArtifact( + type=OutputArchArtifactType.FILE, name=f"vmlinuz-{self.cfg.flavor_id}-{output_arch}" + ), + OutputArchArtifact( + type=OutputArchArtifactType.FILE, + name=f"initramfs-{self.cfg.flavor_id}-{output_arch}", + ), + ] + # include .iso if the flavor supports it. + if self.has_iso(): + artifacts += [ + OutputArchArtifact( + type=OutputArchArtifactType.FILE, + name=f"captainos-{self.cfg.flavor_id}-{output_arch}.iso", + ) + ] + + self.add_arch_dtb_artifacts(artifacts, output_arch) + + log.info( + "Artifacts for flavor %s for arch '%s': %s", self.cfg.flavor_id, output_arch, artifacts + ) + return artifacts + + def add_arch_dtb_artifacts(self, artifacts: list[OutputArchArtifact], output_arch: str): + # if arm64, include DTBs. + if output_arch == "aarch64": + artifacts.append( + OutputArchArtifact( + type=OutputArchArtifactType.DIRECTORY, + name=f"dtb-{self.cfg.flavor_id}-{output_arch}", + ) + ) + + +def list_available_flavors() -> list[str]: + import importlib + import pkgutil + + package = importlib.import_module("captain.flavors") + # iter_modules finds immediate children; walk_packages recurses + ret = [] + for _finder, module_name, _is_pkg in pkgutil.walk_packages( + package.__path__, prefix=f"{package.__name__}." + ): + try: + module = importlib.import_module(module_name) + except Exception as exc: + log.debug(f"Skipping {module_name}: {exc}") + continue + + fn = getattr(module, "create_flavor", None) + if fn is not None and callable(fn): + flavor_id = module_name.rsplit(".", 1)[-1].replace("_", "-") + ret.append(flavor_id) + log.debug("Discovered flavor '%s' via module %s", flavor_id, module_name) + + return sorted(ret) + + +def create_and_setup_flavor_for_id(flavor_id: str, cfg: Config) -> BaseFlavor: + log.debug("Creating and setting up flavor for id '%s'", flavor_id) + flavor_id_underscore = flavor_id.replace("-", "_") + flavor_dir = cfg.project_dir / "captain" / "flavors" / flavor_id_underscore + + if not flavor_dir.is_dir(): + log.error( + "Flavor '%s' not found. Expected to find directory %s", + flavor_id, + flavor_dir, + ) + raise SystemExit(1) + + wanted_module = f"captain.flavors.{flavor_id_underscore}" + log.debug("Attempting to import flavor module %s from directory %s", wanted_module, flavor_dir) + + try: + module = __import__(wanted_module, fromlist=["create_flavor"]) + except ImportError as e: + log.error( + "Failed to import flavor module %s from directory %s: %s", + wanted_module, + flavor_dir, + e, + ) + raise e + + # Validate API explicitly + if not hasattr(module, "create_flavor"): + log.error("Flavor module %s does not define create_flavor()", wanted_module) + raise SystemExit(1) + + log.debug("Executing %s.create_flavor()", wanted_module) + flavor: BaseFlavor = module.create_flavor() + + if not isinstance(flavor, BaseFlavor): + log.error( + "create_flavor() in %s did not return BaseFlavor (got %r)", + wanted_module, + type(flavor), + ) + raise SystemExit(1) + + log.debug("Calling setup() on flavor %s with config: %s", flavor, cfg) + flavor.setup(cfg, flavor_dir) + + # Ensure the current arch is supported by the flavor + if cfg.arch_info.arch not in flavor.supported_architectures: + log.error( + "Flavor '%s' does not support architecture '%s'. Supported architectures: %s", + flavor.id, + cfg.arch_info.arch, + flavor.supported_architectures, + ) + raise SystemExit(1) + else: + log.debug( + "Flavor '%s' supports architecture '%s'", + flavor.id, + cfg.arch_info.arch, + ) + + log.debug( + "Flavor is setup; description: %s; supported_architectures: %s", + flavor.description, + flavor.supported_architectures, + ) + + return flavor diff --git a/captain/flavors/common_acpi/__init__.py b/captain/flavors/common_acpi/__init__.py new file mode 100644 index 00000000..89946165 --- /dev/null +++ b/captain/flavors/common_acpi/__init__.py @@ -0,0 +1,31 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavors.common_debian import DebianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class TrixieACPIFlavor(DebianCommonFlavor): + id = "trixie-acpi" + name = "Trixie ACPI Common" + description = "Debian Trixie based on UEFI+ACPI machines" + supported_architectures = frozenset(["amd64", "arm64"]) + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + + this_flavor_dir = self.specific_flavor_dir("common-acpi") + + # Static files + self.add_static_dir("mkosi.extra", this_flavor_dir) + + # This flavor can produce working ISO images (generic UEFI/ACPI) + def has_iso(self) -> bool: + return True + + def flavor_packages(self) -> set[str]: + return {"acpid", "efibootmgr"}.union(super().flavor_packages()) diff --git a/mkosi.extra/etc/acpi/events/powerbtn b/captain/flavors/common_acpi/mkosi.extra/etc/acpi/events/powerbtn similarity index 100% rename from mkosi.extra/etc/acpi/events/powerbtn rename to captain/flavors/common_acpi/mkosi.extra/etc/acpi/events/powerbtn diff --git a/mkosi.extra/etc/acpi/powerbtn.sh b/captain/flavors/common_acpi/mkosi.extra/etc/acpi/powerbtn.sh similarity index 100% rename from mkosi.extra/etc/acpi/powerbtn.sh rename to captain/flavors/common_acpi/mkosi.extra/etc/acpi/powerbtn.sh diff --git a/mkosi.extra/etc/modules-load.d/ipmi.conf b/captain/flavors/common_acpi/mkosi.extra/etc/modules-load.d/ipmi.conf similarity index 100% rename from mkosi.extra/etc/modules-load.d/ipmi.conf rename to captain/flavors/common_acpi/mkosi.extra/etc/modules-load.d/ipmi.conf diff --git a/captain/flavors/common_armbian/__init__.py b/captain/flavors/common_armbian/__init__.py new file mode 100644 index 00000000..29d9a8c2 --- /dev/null +++ b/captain/flavors/common_armbian/__init__.py @@ -0,0 +1,38 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavors.common_debian import DebianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class ArmbianCommonFlavor(DebianCommonFlavor): + id = "common-armbian" + name = "Armbian Common" + description = "Base flavor for Armbian-based distros" + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + + this_flavor_dir = self.specific_flavor_dir("common-armbian") + self.add_static_dir("mkosi.sandbox", this_flavor_dir) + + def has_iso(self) -> bool: + return False + + def cache_bust_token(self) -> str: + # Fold the armbian-next repo version token in so these flavors rebuild + # when armbian publishes a newer kernel under the same package name. + return self.cfg.armbian_version or "" + + def flavor_packages(self) -> set[str]: + return {"tiny-initramfs"}.union(super().flavor_packages()) + + def include_hwdb(self) -> bool: + return True + + def include_working_apt(self) -> bool: + return True diff --git a/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list b/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list new file mode 100644 index 00000000..0b1c08db --- /dev/null +++ b/captain/flavors/common_armbian/mkosi.sandbox/etc/apt/sources.list.d/armbian-next.list @@ -0,0 +1,2 @@ +deb [signed-by=/usr/share/keyrings/armbian-next.gpg] https://apt-test.next.armbian.com armbian main +deb [signed-by=/usr/share/keyrings/armbian-next.gpg] https://apt-test.next.armbian.com armbian-trixie main diff --git a/captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg b/captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg new file mode 100644 index 00000000..1e1b9eed Binary files /dev/null and b/captain/flavors/common_armbian/mkosi.sandbox/usr/share/keyrings/armbian-next.gpg differ diff --git a/captain/flavors/common_debian/__init__.py b/captain/flavors/common_debian/__init__.py new file mode 100644 index 00000000..65099d24 --- /dev/null +++ b/captain/flavors/common_debian/__init__.py @@ -0,0 +1,95 @@ +import logging +from abc import abstractmethod +from dataclasses import dataclass +from pathlib import Path + +from captain.config import Config +from captain.flavor import BaseFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +@dataclass +class DebianCommonFlavor(BaseFlavor): + id = "common-debian" + name = "Debian Common" + description = "Base flavor for Debian-based distros" + + def setup(self, cfg: Config, flavor_dir: Path) -> None: + super().setup(cfg, flavor_dir) + + this_flavor_dir = self.specific_flavor_dir("common-debian") + + # Templates + self.template_map["mkosi.conf"] = [this_flavor_dir / "mkosi.conf.j2"] + + self.template_map["mkosi.postinst"] = [ + this_flavor_dir / "bash.header.sh", + this_flavor_dir / "mkosi.postinst.sh.j2", + ] + + self.template_map["mkosi.finalize"] = [ + this_flavor_dir / "bash.header.sh", + this_flavor_dir / "mkosi.finalize.sh.j2", + ] + + # Static files + self.add_static_dir("mkosi.extra", this_flavor_dir) + self.add_static_dir("mkosi.sandbox", this_flavor_dir) + self.add_static_dir("mkosi.skeleton", this_flavor_dir) + + # Allow for easily user-customizable dirs and scripts + # those apply to all flavors that inherit from this common-debian base. + # Use-cases: + # - add CA root certificate to global trust store (via ca-certificates package) + # - add some Debian package (eg firmware) - [handled in flavor_packages() below] + # - configure containerd to use specific mirrors + # - run some postinst or finalize script to do some custom configuration + self.add_static_dir("files", cfg.custom_dir, "mkosi.extra") + self.add_custom_scripts(cfg, "mkosi-postinst", "mkosi.postinst") + self.add_custom_scripts(cfg, "mkosi-finalize", "mkosi.finalize") + + def add_custom_scripts(self, cfg: Config, in_dir: str, out_script: str): + custom_postinst_dir = cfg.custom_dir / in_dir + if custom_postinst_dir.exists() and custom_postinst_dir.is_dir(): + files_in_order = sorted(custom_postinst_dir.iterdir()) + log.debug( + "Adding custom %s scripts from %s: %s", + out_script, + custom_postinst_dir, + files_in_order, + ) + for file in files_in_order: + if file.is_file(): + log.debug("Adding custom %s script: %s", out_script, file) + self.template_map[out_script].append(file) + + @abstractmethod + def flavor_packages(self) -> set[str]: + packages = set({}) + # look into cfg.custom_dir/packages.txt, if it exists, read each non-comment line + custom_packages_file = self.cfg.custom_dir / "packages.txt" + if custom_packages_file.exists(): + log.debug("Reading custom packages from %s", custom_packages_file) + packages = set( + { + line.strip() + for line in (custom_packages_file.read_text().splitlines()) + if line.strip() and not line.strip().startswith("#") + } + ) + if self.include_working_apt(): + packages.add("apt") + return packages + + def sorted_flavor_packages(self) -> list[str]: + return sorted(self.flavor_packages()) + + def package_directories(self) -> set[str]: + return set({}) + + def include_working_apt(self) -> bool: + return False + + def include_hwdb(self) -> bool: + return False diff --git a/captain/flavors/common_debian/bash.header.sh b/captain/flavors/common_debian/bash.header.sh new file mode 100644 index 00000000..afde1faa --- /dev/null +++ b/captain/flavors/common_debian/bash.header.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +# logger utility, output ANSI-colored messages to stderr; first argument is level (debug/info/warn/error), all other arguments are the message. +declare -A log_colors=(["debug"]="0;36" ["info"]="0;32" ["notice"]="1;32" ["warn"]="1;33" ["warning"]="1;33" ["error"]="1;31") +declare -A log_emoji=(["debug"]="🐛" ["info"]="🌿" ["notice"]="🌱" ["warn"]="🚸" ["warning"]="🚸" ["error"]="🚨") +function log() { + declare level="${1}" + shift + [[ "${level}" == "debug" && "${DEBUG}" != "yes" ]] && return # Skip debugs unless DEBUG=yes is set in the environment + # Normal output + declare color="\033[${log_colors[${level}]}m" + declare emoji="${log_emoji[${level}]}" + declare ansi_reset="\033[0m" + level=$(printf "%-5s" "${level}") # pad to 5 characters before printing + echo -e "${emoji} ${ansi_reset}[${color}${level}${ansi_reset}] ${color}${*}${ansi_reset}" >&2 +} + diff --git a/mkosi.conf b/captain/flavors/common_debian/mkosi.conf.j2 similarity index 75% rename from mkosi.conf rename to captain/flavors/common_debian/mkosi.conf.j2 index 10a57027..4501935b 100644 --- a/mkosi.conf +++ b/captain/flavors/common_debian/mkosi.conf.j2 @@ -1,24 +1,34 @@ +# cache-bust: {{ flavor.cache_bust_token() }} + [Distribution] Distribution=debian Release=trixie [Output] +ManifestFormat=json Format=cpio CompressOutput=zstd CompressLevel=19 OutputDirectory=mkosi.output [Build] -ToolsTree=yes Incremental=yes -CacheDirectory=mkosi.cache +WithNetwork=yes +CacheDirectory=mkosi.cache.for.incremental [Content] Bootable=no +{% if flavor.include_working_apt() %} +CleanPackageMetadata=no +{% else %} CleanPackageMetadata=yes +{% endif %} Autologin=yes # Skips installing documentation/man pages WithDocs=no +# Adds .deb's found in those directories to a local apt repo that can be installed from. +PackageDirectories= + {{ ' '.join(flavor.package_directories()) }} # Do NOT set MakeInitrd=yes — that adds /etc/initrd-release which causes # systemd to enter initrd mode and try switch-root to a real rootfs. @@ -26,14 +36,9 @@ WithDocs=no # rootfs → tmpfs via switch_root before exec'ing systemd. This makes # pivot_root(2) work for container runtimes (runc). -# Pre-built kernel modules and tools are injected via separate --extra-tree= -# flags on the CLI: -# --extra-tree=mkosi.output/tools/{arch}/ (tools) -# --extra-tree=mkosi.output/kernel/{ver}/{arch}/modules (kernel modules) -# This keeps mkosi.conf architecture-neutral so both amd64 and arm64 builds -# can coexist under mkosi.output/. - Packages= + # flavor-specific packages + {{ "\n ".join(flavor.sorted_flavor_packages()) }} # systemd and core systemd systemd-sysv # Provides poweroff, shutdown, reboot, halt, etc. @@ -43,7 +48,6 @@ Packages= dbus kmod dmsetup - acpid polkitd # Logging rsyslog @@ -76,6 +80,9 @@ Packages= #zstd #xz-utils fastfetch + pciutils # for listing PCI devices + curl + pv RemoveFiles= /etc/initrd-release @@ -85,11 +92,15 @@ RemoveFiles= /usr/share/locale /usr/share/i18n /usr/share/zoneinfo + {% if not flavor.include_working_apt() %} /var/cache + {% endif %} /var/log /usr/share/bash-completion /usr/share/zsh + {% if not flavor.include_hwdb() %} /usr/lib/udev/hwdb.d + {% endif %} /usr/share/lintian /usr/share/bug /usr/share/pixmaps diff --git a/mkosi.extra/etc/cni/net.d/10-bridge.conflist b/captain/flavors/common_debian/mkosi.extra/etc/cni/net.d/10-bridge.conflist similarity index 100% rename from mkosi.extra/etc/cni/net.d/10-bridge.conflist rename to captain/flavors/common_debian/mkosi.extra/etc/cni/net.d/10-bridge.conflist diff --git a/mkosi.extra/etc/containerd/config.toml b/captain/flavors/common_debian/mkosi.extra/etc/containerd/config.toml similarity index 100% rename from mkosi.extra/etc/containerd/config.toml rename to captain/flavors/common_debian/mkosi.extra/etc/containerd/config.toml diff --git a/mkosi.extra/etc/locale.conf b/captain/flavors/common_debian/mkosi.extra/etc/locale.conf similarity index 100% rename from mkosi.extra/etc/locale.conf rename to captain/flavors/common_debian/mkosi.extra/etc/locale.conf diff --git a/mkosi.extra/etc/machine-id b/captain/flavors/common_debian/mkosi.extra/etc/machine-id similarity index 100% rename from mkosi.extra/etc/machine-id rename to captain/flavors/common_debian/mkosi.extra/etc/machine-id diff --git a/mkosi.extra/etc/motd b/captain/flavors/common_debian/mkosi.extra/etc/motd similarity index 100% rename from mkosi.extra/etc/motd rename to captain/flavors/common_debian/mkosi.extra/etc/motd diff --git a/mkosi.extra/etc/nerdctl/nerdctl.toml b/captain/flavors/common_debian/mkosi.extra/etc/nerdctl/nerdctl.toml similarity index 100% rename from mkosi.extra/etc/nerdctl/nerdctl.toml rename to captain/flavors/common_debian/mkosi.extra/etc/nerdctl/nerdctl.toml diff --git a/mkosi.extra/etc/os-release b/captain/flavors/common_debian/mkosi.extra/etc/os-release similarity index 100% rename from mkosi.extra/etc/os-release rename to captain/flavors/common_debian/mkosi.extra/etc/os-release diff --git a/mkosi.extra/etc/rsyslog.conf b/captain/flavors/common_debian/mkosi.extra/etc/rsyslog.conf similarity index 100% rename from mkosi.extra/etc/rsyslog.conf rename to captain/flavors/common_debian/mkosi.extra/etc/rsyslog.conf diff --git a/mkosi.extra/etc/sysctl.d/99-ip-forward.conf b/captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-ip-forward.conf similarity index 100% rename from mkosi.extra/etc/sysctl.d/99-ip-forward.conf rename to captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-ip-forward.conf diff --git a/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf b/captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf similarity index 100% rename from mkosi.extra/etc/sysctl.d/99-quiet-audit.conf rename to captain/flavors/common_debian/mkosi.extra/etc/sysctl.d/99-quiet-audit.conf diff --git a/mkosi.extra/etc/systemd/network/80-dhcp.network b/captain/flavors/common_debian/mkosi.extra/etc/systemd/network/80-dhcp.network similarity index 100% rename from mkosi.extra/etc/systemd/network/80-dhcp.network rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/network/80-dhcp.network diff --git a/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf similarity index 100% rename from mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/resolved.conf.d/no-stub.conf diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset new file mode 100644 index 00000000..3970f43b --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset @@ -0,0 +1,6 @@ +# CaptainOS preset: Tink-agent and related CaptainOS services +enable tink-agent.service +enable tink-agent-setup.service +enable captainos-static-network.service +enable captainos-banner.service + diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset new file mode 100644 index 00000000..80632451 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset @@ -0,0 +1,19 @@ +# CaptainOS preset: Base system services and cleanup +enable systemd-networkd.service +enable systemd-resolved.service +enable systemd-timesyncd.service +enable systemd-time-wait-sync.service +enable containerd.service +enable rsyslog.service +enable rsyslog-hostname-reload.path + +disable apt-daily.timer +disable apt-daily-upgrade.timer +disable e2scrub_all.timer +disable e2scrub_reap.service +disable fstrim.timer +disable logrotate.timer +disable man-db.timer +disable remote-fs.target +disable systemd-firstboot.service + diff --git a/mkosi.extra/etc/systemd/system/captainos-banner.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-banner.service similarity index 100% rename from mkosi.extra/etc/systemd/system/captainos-banner.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-banner.service diff --git a/mkosi.extra/etc/systemd/system/captainos-static-network.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-static-network.service similarity index 100% rename from mkosi.extra/etc/systemd/system/captainos-static-network.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/captainos-static-network.service diff --git a/mkosi.extra/etc/systemd/system/containerd.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/containerd.service similarity index 100% rename from mkosi.extra/etc/systemd/system/containerd.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/containerd.service diff --git a/mkosi.extra/etc/systemd/system/default.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/default.target similarity index 100% rename from mkosi.extra/etc/systemd/system/default.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/default.target diff --git a/mkosi.extra/etc/systemd/system/initrd-cleanup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-cleanup.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-cleanup.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-cleanup.service diff --git a/mkosi.extra/etc/systemd/system/initrd-parse-etc.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-parse-etc.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-parse-etc.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-parse-etc.service diff --git a/mkosi.extra/etc/systemd/system/initrd-switch-root.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-switch-root.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.service diff --git a/mkosi.extra/etc/systemd/system/initrd-switch-root.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.target similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-switch-root.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-switch-root.target diff --git a/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd-udevadm-cleanup-db.service diff --git a/mkosi.extra/etc/systemd/system/initrd.target b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd.target similarity index 100% rename from mkosi.extra/etc/systemd/system/initrd.target rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/initrd.target diff --git a/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.path diff --git a/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog-hostname-reload.service diff --git a/mkosi.extra/etc/systemd/system/rsyslog.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog.service similarity index 100% rename from mkosi.extra/etc/systemd/system/rsyslog.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/rsyslog.service diff --git a/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf similarity index 100% rename from mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/serial-getty@.service.d/autologin.conf diff --git a/mkosi.extra/etc/systemd/system/systemd-firstboot.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-firstboot.service similarity index 100% rename from mkosi.extra/etc/systemd/system/systemd-firstboot.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-firstboot.service diff --git a/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf new file mode 100644 index 00000000..8437ae74 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/systemd-networkd-wait-online.service.d/override.conf @@ -0,0 +1,5 @@ +[Service] +ExecStart= +ExecStart=/usr/lib/systemd/systemd-networkd-wait-online --any --timeout=15 +TimeoutStartSec=15 + diff --git a/mkosi.extra/etc/systemd/system/tink-agent-setup.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service similarity index 88% rename from mkosi.extra/etc/systemd/system/tink-agent-setup.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service index 69540149..d7242a36 100644 --- a/mkosi.extra/etc/systemd/system/tink-agent-setup.service +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent-setup.service @@ -1,7 +1,7 @@ [Unit] Description=Tink Agent Setup (pull image and extract binary) -After=containerd.service network-online.target systemd-resolved.service -Wants=network-online.target +After=containerd.service network-online.target systemd-resolved.service time-sync.target +Wants=network-online.target time-sync.target Requires=containerd.service # Only start when Tinkerbell parameters are on the kernel command line ConditionKernelCommandLine=|tink_worker_image diff --git a/mkosi.extra/etc/systemd/system/tink-agent.service b/captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent.service similarity index 100% rename from mkosi.extra/etc/systemd/system/tink-agent.service rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/system/tink-agent.service diff --git a/mkosi.extra/etc/systemd/timesyncd.conf b/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf similarity index 76% rename from mkosi.extra/etc/systemd/timesyncd.conf rename to captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf index ff03bad4..42d82897 100644 --- a/mkosi.extra/etc/systemd/timesyncd.conf +++ b/captain/flavors/common_debian/mkosi.extra/etc/systemd/timesyncd.conf @@ -1,2 +1,3 @@ [Time] FallbackNTP=time.cloudflare.com time.google.com time.nist.gov +ConnectionRetrySec=2 diff --git a/mkosi.extra/etc/timezone b/captain/flavors/common_debian/mkosi.extra/etc/timezone similarity index 100% rename from mkosi.extra/etc/timezone rename to captain/flavors/common_debian/mkosi.extra/etc/timezone diff --git a/mkosi.extra/init b/captain/flavors/common_debian/mkosi.extra/init similarity index 100% rename from mkosi.extra/init rename to captain/flavors/common_debian/mkosi.extra/init diff --git a/mkosi.extra/root/.bashrc b/captain/flavors/common_debian/mkosi.extra/root/.bashrc similarity index 100% rename from mkosi.extra/root/.bashrc rename to captain/flavors/common_debian/mkosi.extra/root/.bashrc diff --git a/mkosi.extra/root/.profile b/captain/flavors/common_debian/mkosi.extra/root/.profile similarity index 100% rename from mkosi.extra/root/.profile rename to captain/flavors/common_debian/mkosi.extra/root/.profile diff --git a/mkosi.extra/usr/local/bin/captainos-banner b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-banner similarity index 100% rename from mkosi.extra/usr/local/bin/captainos-banner rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-banner diff --git a/mkosi.extra/usr/local/bin/captainos-static-network b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-static-network similarity index 100% rename from mkosi.extra/usr/local/bin/captainos-static-network rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/captainos-static-network diff --git a/mkosi.extra/usr/local/bin/rsyslog-start b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/rsyslog-start similarity index 100% rename from mkosi.extra/usr/local/bin/rsyslog-start rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/rsyslog-start diff --git a/mkosi.extra/usr/local/bin/tink-agent-setup b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-setup similarity index 100% rename from mkosi.extra/usr/local/bin/tink-agent-setup rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-setup diff --git a/mkosi.extra/usr/local/bin/tink-agent-start b/captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-start similarity index 100% rename from mkosi.extra/usr/local/bin/tink-agent-start rename to captain/flavors/common_debian/mkosi.extra/usr/local/bin/tink-agent-start diff --git a/mkosi.finalize b/captain/flavors/common_debian/mkosi.finalize.sh.j2 similarity index 56% rename from mkosi.finalize rename to captain/flavors/common_debian/mkosi.finalize.sh.j2 index 1057bcdc..5888729d 100755 --- a/mkosi.finalize +++ b/captain/flavors/common_debian/mkosi.finalize.sh.j2 @@ -3,7 +3,7 @@ # The root filesystem is writable here. set -euo pipefail -echo "==> CaptainOS finalize: unlocking root account..." +log info "CaptainOS finalize: unlocking root account..." # Unlock root with empty password # Replace the locked root entry (! or * in password field) with empty hash @@ -13,38 +13,38 @@ if [[ -f "$BUILDROOT/etc/shadow" ]]; then fi # Ensure /etc/initrd-release does not exist -rm -f "$BUILDROOT/etc/initrd-release" -echo " /etc/initrd-release removed" +log info "Ensure no /etc/initrd-release exists..." +rm -fv "$BUILDROOT/etc/initrd-release" # Remove systemd-firstboot completely -rm -f "$BUILDROOT/usr/lib/systemd/system/systemd-firstboot.service" -rm -f "$BUILDROOT/usr/lib/systemd/system/sysinit.target.wants/systemd-firstboot.service" -rm -f "$BUILDROOT/usr/bin/systemd-firstboot" -echo " systemd-firstboot removed" +log info "Completely remove systemd-firstboot..." +rm -fv "$BUILDROOT/usr/lib/systemd/system/systemd-firstboot.service" +rm -fv "$BUILDROOT/usr/lib/systemd/system/sysinit.target.wants/systemd-firstboot.service" +rm -fv "$BUILDROOT/usr/bin/systemd-firstboot" # Point /etc/resolv.conf at the non-stub resolv.conf written by # systemd-resolved. This file contains upstream (DHCP-provided) # nameservers instead of 127.0.0.53. tink-agent bind-mounts the host's # /etc/resolv.conf into action containers so they need real, routable # nameservers (the stub address is unreachable from container namespaces). -ln -sf /run/systemd/resolve/resolv.conf "$BUILDROOT/etc/resolv.conf" -echo " /etc/resolv.conf -> /run/systemd/resolve/resolv.conf" +log info "Handle resolv.conf..." +ln -sfv /run/systemd/resolve/resolv.conf "$BUILDROOT/etc/resolv.conf" # Ensure /init (switch_root script) is executable +log info "Ensure /init is executable..." if [[ -f "$BUILDROOT/init" ]]; then - chmod +x "$BUILDROOT/init" - echo " /init made executable" + chmod -v +x "$BUILDROOT/init" fi # Ensure tool binaries are executable. # GitHub Actions artifact upload/download and some archive tools strip # the execute bit. Re-apply +x to every tool directory so containerd, # runc, nerdctl, and CNI plugins can actually run. +log info "Ensure tool binaries are executable..." for dir in usr/local/bin opt/cni/bin; do target="$BUILDROOT/$dir" if [[ -d "$target" ]]; then - find "$target" -type f -exec chmod +x {} + - echo " +x restored on $dir/*" + find "$target" -type f -exec chmod -v +x {} + fi done @@ -55,10 +55,12 @@ done # the initramfs stays small without requiring a kernel recompile. # --------------------------------------------------------------------------- MODDIR="$BUILDROOT/usr/lib/modules" + if [[ -d "$MODDIR" ]]; then - echo "==> Trimming unnecessary kernel modules..." + log info "Trimming unnecessary kernel modules..." BEFORE=$(du -sb "$MODDIR" | awk '{print $1}') + # @TODO: this has to be templated, for flavors like fat or full we should keep most modules. # Module directory patterns to remove (relative to kernel/ inside the # modules tree). Each entry is passed to 'find -path'. EXCLUDE_PATTERNS=( @@ -78,16 +80,23 @@ if [[ -d "$MODDIR" ]]; then "*/kernel/virt" # Niche NIC driver "*/kernel/drivers/net/ethernet/netronome" + # Sound stuff + "*/kernel/sound" + # Media stuff + "*/kernel/drivers/media" + # Wireless net drivers + "*/kernel/drivers/net/wireless" ) for pattern in "${EXCLUDE_PATTERNS[@]}"; do - find "$MODDIR" -path "$pattern" -type d -exec rm -rf {} + 2>/dev/null || true + log info " Removing modules matching pattern: '${pattern}'" + find "$MODDIR" -path "$pattern" -type d -exec rm -rf {} + || true done AFTER=$(du -sb "$MODDIR" | awk '{print $1}') SAVED=$(( (BEFORE - AFTER) / 1024 / 1024 )) [[ $SAVED -lt 0 ]] && SAVED=0 - echo " Removed ${SAVED}MB of unnecessary kernel modules" + log info " Removed ${SAVED}MB of unnecessary kernel modules" # Regenerate module dependency files after removal for all kernel versions for kdir in "$MODDIR"/*; do @@ -97,4 +106,27 @@ if [[ -d "$MODDIR" ]]; then done fi -echo "==> CaptainOS finalize complete." +log info "Listing 20 biggest remaining kernel modules du: ${MODDIR}" +du -h -x -d 5 "$MODDIR" | sort -h | tail -n20 || true + +log info "Removing /boot from the image to save space" +rm -rf "${BUILDROOT:?}"/boot + +DTB_SRC_DIR=$(echo -n "${BUILDROOT}"/usr/lib/linux-image-*) +if [[ -d "$DTB_SRC_DIR" ]]; then + log info "Exporting kernel DTBs; DTB_DIR: ${DTB_SRC_DIR}" + mkdir -pv "${BUILDROOT}/root/dtb" + # Move every .dtb* file in the DTB_SRC_DIR to "${BUILDROOT}/root/dtb" -- maintain the directory structure + find "${DTB_SRC_DIR}" -type f -name "*.dtb*" -exec sh -c 'for f; do mkdir -p "${0}/$(dirname "${f#'"${DTB_SRC_DIR}"'/}")" && mv "$f" "${0}/${f#'"${DTB_SRC_DIR}"'/}" ; done' "${BUILDROOT}/root/dtb" {} + || true + + # Now simply output the dtb directory to mkosi's OUTPUTDIR + log info "Copying DTBs to mkosi output directory: ${OUTPUTDIR}/dtb" + mv "${BUILDROOT}/root/dtb" "${OUTPUTDIR}/dtb" +else + log info "No DTB source directory found at ${DTB_SRC_DIR}; skipping DTB export" +fi + +log info "Final contents of root filesystem (up to depth 4, top 30):" +du -h -x -d 4 "$BUILDROOT" | sort -h | tail -n30 || true + +log info "Regular CaptainOS finalize complete." diff --git a/captain/flavors/common_debian/mkosi.postinst.sh.j2 b/captain/flavors/common_debian/mkosi.postinst.sh.j2 new file mode 100755 index 00000000..627bc223 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.postinst.sh.j2 @@ -0,0 +1,27 @@ +#!/bin/bash +# mkosi.postinst — Post-installation script for the CaptainOS initrd. +# Runs *outside* the image chroot after packages are installed. +# The target filesystem is at /buildroot; use 'mkosi-chroot' to run commands inside the chroot environment. +# You can also use mkosi.extra/ for creating directories and dropping config files. +set -euo pipefail + +log info "Regular CaptainOS post-install starting..." + +# Ensure CA certificate bundle is generated. +log info "Generating fresh CA certificate bundle..." +mkosi-chroot update-ca-certificates --fresh + +# Service enablement/disablement is handled via systemd preset files: +# - mkosi.extra/etc/systemd/system-preset/10-captainos-tink.preset (tink-agent and CaptainOS services) +# - mkosi.extra/etc/systemd/system-preset/20-captainos-base.preset (base system services and cleanup) +# mkosi applies presets automatically after post-install, so no manual +# systemctl enable/disable calls are needed here. + +# Root password is set via mkosi.conf RootPassword= setting + + +# Set default target to multi-user (no graphical) +log info "Setting default systemd target to multi-user.target (no graphical)..." +mkosi-chroot systemctl set-default multi-user.target || true + +log info "Regular CaptainOS post-install complete." diff --git a/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty new file mode 100644 index 00000000..551c0a65 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.sandbox/etc/apt/apt.conf.d/01nopty @@ -0,0 +1,4 @@ +Dpkg::Use-Pty "0"; +APT::Quiet "2"; +Dpkg::Progress-Fancy "false"; +APT::Color "true"; diff --git a/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty b/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty new file mode 100644 index 00000000..551c0a65 --- /dev/null +++ b/captain/flavors/common_debian/mkosi.skeleton/etc/apt/apt.conf.d/01nopty @@ -0,0 +1,4 @@ +Dpkg::Use-Pty "0"; +APT::Quiet "2"; +Dpkg::Progress-Fancy "false"; +APT::Color "true"; diff --git a/captain/flavors/trixie_armbian_rpi/__init__.py b/captain/flavors/trixie_armbian_rpi/__init__.py new file mode 100644 index 00000000..006cff0f --- /dev/null +++ b/captain/flavors/trixie_armbian_rpi/__init__.py @@ -0,0 +1,153 @@ +import logging +import shutil +from dataclasses import dataclass +from pathlib import Path + +from captain.artifacts import OutputArchArtifact, OutputArchArtifactType +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor +from captain.tools import _download_binary +from captain.util import ensure_dir, symlink_relative + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieArmbianRPiFlavor() + + +@dataclass +class TrixieArmbianRPiFlavor(ArmbianCommonFlavor): + id = "trixie-armbian-rpi" + name = "Trixie for Raspberry Pi - Armbian bcm2711-current Kernel" + description = "Debian Trixie based on Armbian's rockchip64-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + rpi_fw_tag = "1.20260408" + + def flavor_packages(self) -> set[str]: + return {"linux-image-current-bcm2711"}.union(super().flavor_packages()) + + def list_arch_artifacts(self, output_arch: str) -> list[OutputArchArtifact]: + artifacts = super().list_arch_artifacts(output_arch) + artifacts.append( + OutputArchArtifact( + type=OutputArchArtifactType.DIRECTORY, name=self.firmware_out_dirname() + ) + ) + return artifacts + + def post_mkosi_stage(self): + self.download_rpi_firmware() + + def post_artifact_collect(self): + out: Path = ensure_dir(self.cfg.output_dir) + log.debug("Copying firmware directory from %s to %s", self.firmware_output(), out) + target_fw_dir = out / self.firmware_out_dirname() + if target_fw_dir.exists(): + shutil.rmtree(target_fw_dir) + shutil.copytree(self.firmware_output(), target_fw_dir) + log.info("Copied firmware directory: %s", target_fw_dir) + + ## Symlink all dtbs directly in the fw dir + out_dtbs = out / f"dtb-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + broadcom_dtbs = out_dtbs / "broadcom" + if not broadcom_dtbs.is_dir(): + log.error( + "Expected dtb directory %s does not exist, skipping dtb symlinks", broadcom_dtbs + ) + raise ValueError(f"Expected dtb directory {broadcom_dtbs} does not exist") + for dtb in broadcom_dtbs.glob("*.dtb"): + target_dtb = target_fw_dir / dtb.name + log.debug("Symlinking dtb: %s to %s", dtb, target_dtb) + symlink_relative(target_dtb, dtb) + + # Symlink all overlays (which are in the same level as "broadcom") into an "overlays" subdir + broadcom_overlays = out_dtbs / "overlays" + if not broadcom_overlays.is_dir(): + log.error("Expected dtb overlays directory %s does not exist", broadcom_overlays) + raise ValueError(f"Expected dtb overlays directory {broadcom_overlays} does not exist") + target_overlays = ensure_dir(target_fw_dir / "overlays") + for overlay in broadcom_overlays.glob("*.dtbo"): + log.debug("Symlinking dtb overlay %s to firmware overlays directory", overlay) + target_overlay = target_overlays / overlay.name + symlink_relative(target_overlay, overlay) + + ## Symlink the kernel and initramfs + # kernel as kernel8.img + # see https://www.raspberrypi.com/documentation/computers/config_txt.html#kernel + kernel_src = out / f"vmlinuz-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + if not kernel_src.is_file(): + log.error( + "Expected kernel image %s does not exist, cannot symlink to firmware dir", + kernel_src, + ) + raise ValueError(f"Expected kernel image {kernel_src} does not exist") + kernel_dst = target_fw_dir / "kernel8.img" + log.debug("Symlinking kernel from %s to %s", kernel_src, kernel_dst) + symlink_relative(kernel_dst, kernel_src) + + # initramfs as initramfs8 + # see https://www.raspberrypi.com/documentation/computers/config_txt.html#initramfs + initramfs_src = out / f"initramfs-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + if not initramfs_src.is_file(): + log.error( + "Expected initramfs image %s does not exist, cannot symlink to firmware dir", + initramfs_src, + ) + raise ValueError(f"Expected initramfs image {initramfs_src} does not exist") + initramfs_dst = target_fw_dir / "initramfs8" + log.debug("Symlinking initramfs from %s to %s", initramfs_src, initramfs_dst) + symlink_relative(initramfs_dst, initramfs_src) + + def firmware_out_dirname(self) -> str: + # return f"firmware-{self.cfg.flavor_id}-{self.cfg.arch_info.output_arch}" + return "firmware-rpi" + + def firmware_output(self) -> Path: + return ensure_dir(self.cfg.initramfs_output / "firmware") + + def rpi_firmware_base_url(self) -> str: + return ( + "https://raw.githubusercontent.com/raspberrypi/firmware/refs/tags/" + + self.rpi_fw_tag + + "/boot/" + ) + + def download_rpi_firmware(self): + rpi_fw_files = [ + "bootcode.bin", + "fixup4cd.dat", + "fixup4.dat", + "fixup4db.dat", + "fixup4x.dat", + "fixup_cd.dat", + "fixup.dat", + "fixup_db.dat", + "fixup_x.dat", + "LICENCE.broadcom", + "start4cd.elf", + "start4db.elf", + "start4.elf", + "start4x.elf", + "start_cd.elf", + "start_db.elf", + "start.elf", + "start_x.elf", + ] + for filename in rpi_fw_files: + url = self.rpi_firmware_base_url() + filename + output_path = self.firmware_output() / filename + if output_path.is_file(): + log.debug( + "Raspberry Pi firmware file %s already exists at %s, skipping download", + url, + output_path, + ) + continue + log.info( + "Raspberry Pi firmware file %s does not exist at %s, downloading...", + url, + output_path, + ) + log.info("Downloading Raspberry Pi firmware file %s to %s", url, output_path) + _download_binary(url, output_path) diff --git a/captain/flavors/trixie_full/__init__.py b/captain/flavors/trixie_full/__init__.py new file mode 100644 index 00000000..713a8ae7 --- /dev/null +++ b/captain/flavors/trixie_full/__init__.py @@ -0,0 +1,32 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_acpi import TrixieACPIFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieFullFlavor() + + +@dataclass +class TrixieFullFlavor(TrixieACPIFlavor): + id = "trixie-full" + name = "Trixie Full" + description = "Debian Trixie based with linux-image-generic standard Debian kernel" + supported_architectures = frozenset(["amd64", "arm64"]) + + def flavor_packages(self) -> set[str]: + pkgs = { + "linux-image-generic", # Debian's standard kernel (arm64/amd64) + "tiny-initramfs", # A tiny initramfs builder; required for kernel image + } + return pkgs.union(super().flavor_packages()) + + def include_hwdb(self) -> bool: + return True + + def include_working_apt(self) -> bool: + return True diff --git a/captain/flavors/trixie_genio/__init__.py b/captain/flavors/trixie_genio/__init__.py new file mode 100644 index 00000000..494fcfe7 --- /dev/null +++ b/captain/flavors/trixie_genio/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieGenioFlavor() + + +@dataclass +class TrixieGenioFlavor(ArmbianCommonFlavor): + id = "trixie-genio" + name = "Trixie for Mediatek Genio 64-bit ARM machines" + description = "Debian Trixie with Armbian's genio-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def flavor_packages(self) -> set[str]: + return {"linux-image-edge-genio"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_meson64/__init__.py b/captain/flavors/trixie_meson64/__init__.py new file mode 100644 index 00000000..6f5a3176 --- /dev/null +++ b/captain/flavors/trixie_meson64/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieMeson64Flavor() + + +@dataclass +class TrixieMeson64Flavor(ArmbianCommonFlavor): + id = "trixie-meson64" + name = "Trixie for Meson (Amlogic) 64-bit ARM machines" + description = "Debian Trixie based with Armbian's meson64-oldlts kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def flavor_packages(self) -> set[str]: + return {"linux-image-oldlts-meson64"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_rockchip64/__init__.py b/captain/flavors/trixie_rockchip64/__init__.py new file mode 100644 index 00000000..7e5fa946 --- /dev/null +++ b/captain/flavors/trixie_rockchip64/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieRockchip64Flavor() + + +@dataclass +class TrixieRockchip64Flavor(ArmbianCommonFlavor): + id = "trixie-rockchip64" + name = "Trixie for Rockchip 64-bit ARM machines" + description = "Debian Trixie based with Armbian's rockchip64-edge kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def flavor_packages(self) -> set[str]: + return {"linux-image-edge-rockchip64"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_rockchip64_vendor/__init__.py b/captain/flavors/trixie_rockchip64_vendor/__init__.py new file mode 100644 index 00000000..30eceb8f --- /dev/null +++ b/captain/flavors/trixie_rockchip64_vendor/__init__.py @@ -0,0 +1,22 @@ +import logging +from dataclasses import dataclass + +from captain.flavor import BaseFlavor +from captain.flavors.common_armbian import ArmbianCommonFlavor + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieRockchip64VendorFlavor() + + +@dataclass +class TrixieRockchip64VendorFlavor(ArmbianCommonFlavor): + id = "trixie-rockchip64-vendor" + name = "Trixie for Rockchip 64-bit ARM machines - Rockchip Vendor Kernel" + description = "Debian Trixie based with Armbian's rk35xx-vendor kernel" + supported_architectures = frozenset(["arm64"]) # does NOT support amd64 + + def flavor_packages(self) -> set[str]: + return {"linux-image-vendor-rk35xx"}.union(super().flavor_packages()) diff --git a/captain/flavors/trixie_slim/__init__.py b/captain/flavors/trixie_slim/__init__.py new file mode 100644 index 00000000..881160ba --- /dev/null +++ b/captain/flavors/trixie_slim/__init__.py @@ -0,0 +1,37 @@ +import logging +from dataclasses import dataclass + +from captain.artifacts import OutputArchArtifact +from captain.cli import stages +from captain.flavor import BaseFlavor +from captain.flavors.common_acpi import TrixieACPIFlavor +from captain.kernel import obtain_target_artifact_path + +log: logging.Logger = logging.getLogger(__name__) + + +def create_flavor() -> BaseFlavor: + return TrixieSlimFlavor() + + +@dataclass +class TrixieSlimFlavor(TrixieACPIFlavor): + id = "trixie-slim" + name = "Trixie Slim" + description = "Debian Trixie based with captainos slim kernel" + supported_architectures = frozenset(["amd64", "arm64"]) + + def pre_mkosi_stage(self): + log.debug("Flavor delegating to build_kernel_stage to ensure kernel .deb is present") + # call the kernel build stage, to ensure kernel .deb is in mkosi.input/kernel/ + stages.build_kernel_stage(self.cfg) + + def flavor_packages(self) -> set[str]: + return {f"linux-image-{self.cfg.kernel_version}-captainos"}.union(super().flavor_packages()) + + def package_directories(self) -> set[str]: + return {str(obtain_target_artifact_path(self.cfg).parent.relative_to(self.cfg.project_dir))} + + def add_arch_dtb_artifacts(self, artifacts: list[OutputArchArtifact], output_arch: str): + # no dtb artifacts for trixie-slim flavor + pass diff --git a/captain/iso.py b/captain/iso.py index 5aa20445..fb577778 100644 --- a/captain/iso.py +++ b/captain/iso.py @@ -41,13 +41,11 @@ def _grub_cfg(arch: str) -> str: def _find_vmlinuz(cfg: Config) -> Path: """Locate the vmlinuz kernel image.""" - vmlinuz_dir = cfg.kernel_output - candidates = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - if not candidates: - log.error("No vmlinuz found in %s", vmlinuz_dir) - log.error("Build the kernel first: ./build.py kernel") + vmlinuz_files = sorted(cfg.initramfs_output.glob("*.vmlinuz*")) + if not vmlinuz_files: + log.error("No vmlinuz found in %s", cfg.initramfs_output) raise SystemExit(1) - return candidates[0] + return vmlinuz_files[0] def _find_initramfs(cfg: Config) -> Path: @@ -98,7 +96,7 @@ def build(cfg: Config) -> None: (grub_dir / "grub.cfg").write_text(_grub_cfg(cfg.arch)) iso_dir = ensure_dir(cfg.iso_output) - iso_path = iso_dir / f"captainos-{cfg.kernel_version}-{cfg.arch_info.output_arch}.iso" + iso_path = iso_dir / f"captainos-{cfg.flavor_id}-{cfg.arch_info.output_arch}.iso" log.info("Building ISO with grub-mkrescue (%s)...", grub_platform) grub_mkrescue = shutil.which("grub-mkrescue") diff --git a/captain/kernel.py b/captain/kernel.py index a528b0c3..1456f5d2 100644 --- a/captain/kernel.py +++ b/captain/kernel.py @@ -1,13 +1,15 @@ -"""Kernel download, configuration, compilation, and installation. +"""Kernel download, configuration, compilation, and packaging. -Heavy lifting (make, strip) is still done via subprocess — only the -orchestration is in Python. Called directly by ``cli._build_kernel_stage`` -in both native and Docker modes (inside the container ``build.py kernel`` -re-enters via the CLI with all modes forced to native). +Heavy lifting (make) is still done via subprocess — only the +orchestration is in Python. Called directly by ``cli.build_kernel_stage`` +in both native and Docker modes (inside the container we re-enter +the CLI with all modes forced to native). """ from __future__ import annotations +import hashlib +import json import logging import os import re @@ -27,14 +29,71 @@ ) from captain import console -from captain.config import Config +from captain.config import DEFAULT_KERNEL_VERSION, Config from captain.util import ensure_dir, run, safe_extractall +KERNEL_BUILD_BASE_PATH = "/work/kernel-build" + +RELEASES_JSON_URL = "https://www.kernel.org/releases.json" + log = logging.getLogger(__name__) _DOWNLOAD_TIMEOUT = 60 # seconds +def _version_tuple(version: str) -> tuple[int, ...]: + """Parse a dotted kernel version into an int tuple for comparison.""" + parts: list[int] = [] + for piece in version.split("."): + # stop at the first non-numeric component (e.g. rc suffixes) + if not piece.isdigit(): + break + parts.append(int(piece)) + return tuple(parts) + + +def latest_branch_version(branch: str | None = None) -> str: + """Return the newest kernel.org point release of *branch* (e.g. ``"6.18"``). + + Consults kernel.org's ``releases.json``, which lists the latest release of + each active branch. *branch* defaults to the ``.`` of + :data:`DEFAULT_KERNEL_VERSION`. Falls back to ``DEFAULT_KERNEL_VERSION`` on + any network/parse failure or when the branch is not listed — never returns + an empty string, so callers/CI always get a buildable version. + """ + if branch is None: + branch = ".".join(DEFAULT_KERNEL_VERSION.split(".")[:2]) + + try: + req = urllib.request.Request(RELEASES_JSON_URL) + with urllib.request.urlopen(req, timeout=_DOWNLOAD_TIMEOUT) as resp: + data = json.loads(resp.read().decode()) + except (urllib.error.URLError, json.JSONDecodeError, OSError) as exc: + log.warning( + "Could not fetch %s (%s); using default %s", + RELEASES_JSON_URL, + exc, + DEFAULT_KERNEL_VERSION, + ) + return DEFAULT_KERNEL_VERSION + + prefix = f"{branch}." + matches = [ + r["version"] + for r in data.get("releases", []) + if isinstance(r.get("version"), str) and r["version"].startswith(prefix) + ] + if not matches: + log.warning( + "No kernel.org release for branch %s; using default %s", branch, DEFAULT_KERNEL_VERSION + ) + return DEFAULT_KERNEL_VERSION + + latest = max(matches, key=_version_tuple) + log.info("Latest kernel for branch %s is %s", branch, latest) + return latest + + def _download_with_progress(url: str, filename: Path) -> None: """Download *url* to *filename* with a Rich progress bar.""" req = urllib.request.Request(url) @@ -58,18 +117,18 @@ def _download_with_progress(url: str, filename: Path) -> None: progress.update(task, advance=len(buf)) -def download_kernel(version: str, dest_dir: Path) -> Path: +def download_kernel(cfg: Config, dest_dir: Path) -> Path: """Download and extract a kernel tarball. Returns the source directory.""" - src_dir = dest_dir / f"linux-{version}" + src_dir = dest_dir / f"linux-{cfg.kernel_version}" # predict kernel tarball 1st-level dir name if src_dir.is_dir(): log.info("Using cached kernel source at %s", src_dir) return src_dir - major = version.split(".")[0] - url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz" - tarball = dest_dir / f"linux-{version}.tar.xz" + major = cfg.kernel_version.split(".")[0] + url = f"https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{cfg.kernel_version}.tar.xz" + tarball = dest_dir / f"linux-{cfg.kernel_version}.tar.xz" - log.info("Downloading kernel %s...", version) + log.info("Downloading kernel %s...", cfg.kernel_version) log.info(" URL: %s", url) ensure_dir(dest_dir) try: @@ -100,15 +159,6 @@ def _kernel_branch(version: str) -> str: def _find_defconfig(cfg: Config) -> Path: """Locate the defconfig for the current kernel version and architecture.""" - if cfg.kernel_config: - explicit = Path(cfg.kernel_config) - if not explicit.is_absolute(): - explicit = cfg.project_dir / explicit - if explicit.is_file(): - return explicit - log.error("Kernel config not found: %s", explicit) - raise SystemExit(1) - ai = cfg.arch_info branch = _kernel_branch(cfg.kernel_version) defconfig = cfg.project_dir / "kernel.configs" / f"{branch}.{ai.arch}" @@ -147,6 +197,26 @@ def configure_kernel(cfg: Config, src_dir: Path) -> None: log.info("Using defconfig: %s", defconfig) shutil.copy2(defconfig, src_dir / ".config") run(["make", "olddefconfig"], env=make_env, cwd=src_dir) + + log.debug( + "Considering whether to launch menuconfig for manual config tweaks... %s", + cfg.kernel_menuconfig, + ) + + if cfg.kernel_menuconfig: + log.info("Launching menuconfig...") + run(["make", "menuconfig"], env=make_env, cwd=src_dir) + + # run `make savedefconfig` + log.info("Saving modified config...") + run(["make", "savedefconfig"], env=make_env, cwd=src_dir) + + # copy the 'defconfig' back to the source - ready to commit + shutil.copy(src_dir / "defconfig", defconfig) + log.info("Updated defconfig saved to %s", defconfig) + + raise SystemExit(0) # exit after menuconfig - no build + branch = _kernel_branch(cfg.kernel_version) resolved = cfg.project_dir / "kernel.configs" / f".config.resolved.{branch}.{ai.arch}" shutil.copy2(src_dir / ".config", resolved) @@ -176,87 +246,81 @@ def build_kernel(cfg: Config, src_dir: Path) -> str: make_env = {"ARCH": ai.kernel_arch} if ai.cross_compile: make_env["CROSS_COMPILE"] = ai.cross_compile + make_env["DPKG_DEB_COMPRESSOR_TYPE"] = "none" # don't waste time compressing .deb + + if cfg.kernel_clean: + log.info("Cleaning kernel...") + run( + ["make", f"-j{nproc}", "clean"], + env=make_env, + cwd=src_dir, + ) - log.info("Building kernel with %d jobs...", nproc) - run( - ["make", f"-j{nproc}", ai.image_target, "modules"], - env=make_env, - cwd=src_dir, - ) - - result = run( + built_kver = run( ["make", "-s", "kernelrelease"], env={"ARCH": ai.kernel_arch}, capture=True, cwd=src_dir, + ).stdout.strip() + + log.info("Building kernel '%s' with %d jobs...", built_kver, nproc) + run( + ["make", f"-j{nproc}", "bindeb-pkg"], + env=make_env, + cwd=src_dir, ) - built_kver = result.stdout.strip() + log.info("Built kernel version: %s", built_kver) return built_kver -def install_kernel(cfg: Config, src_dir: Path, built_kver: str) -> None: - """Install modules and vmlinuz into mkosi.output/kernel/{version}/{arch}/.""" - ai = cfg.arch_info - modules_root = cfg.modules_output +def obtain_target_artifact_path(cfg, ensure_parent: bool = False) -> Path: + if ensure_parent: + ensure_dir(cfg.kernel_output) - make_env = {"ARCH": ai.kernel_arch} - if ai.cross_compile: - make_env["CROSS_COMPILE"] = ai.cross_compile - - log.info("Installing modules...") - run( - ["make", f"INSTALL_MOD_PATH={modules_root}", "modules_install"], - env=make_env, - cwd=src_dir, + # let's hash: + # 1) the input defconfig + defconfig = _find_defconfig(cfg) + hex_digest_defconfig = hashlib.sha256(defconfig.read_bytes()).hexdigest() + # 2) the contents of this script (as an imperfect proxy for the build logic) + script_path = Path(__file__) + hex_digest_script = hashlib.sha256(script_path.read_bytes()).hexdigest() + # Combine and shorten to 8 chars for the final version hash + version_hash = hashlib.sha256((hex_digest_defconfig + hex_digest_script).encode()).hexdigest() + version_hash_shorten = version_hash[:8] + + return ( + cfg.kernel_output / f"linux-image-{cfg.kernel_version}-captainos" + f"_{cfg.kernel_version}-1-{version_hash_shorten}_" + f"{cfg.arch_info.kernel_arch}.deb" ) - log.info("Stripping debug symbols from modules...") - strip_cmd = f"{ai.strip_prefix}strip" - for ko in modules_root.rglob("*.ko"): - run([strip_cmd, "--strip-unneeded", str(ko)], check=False) - - log.info("Compressing kernel modules with zstd...") - for ko in modules_root.rglob("*.ko"): - run(["zstd", "--rm", "-q", "-19", str(ko)], check=True) - - mod_base = modules_root / "lib" / "modules" / built_kver - (mod_base / "build").unlink(missing_ok=True) - (mod_base / "source").unlink(missing_ok=True) - - usr_moddir = ensure_dir(modules_root / "usr" / "lib" / "modules" / built_kver) - if mod_base.is_dir(): - for item in mod_base.iterdir(): - dest = usr_moddir / item.name - if dest.exists(): - if dest.is_dir(): - shutil.rmtree(dest) - else: - dest.unlink() - shutil.move(str(item), str(dest)) - shutil.rmtree(modules_root / "lib", ignore_errors=True) - - log.info("Running depmod for compressed modules...") - run( - ["depmod", "-a", "-b", str(modules_root / "usr"), built_kver], - check=True, - ) - kernel_image = src_dir / ai.kernel_image_path - vmlinuz_dir = ensure_dir(cfg.kernel_output) +def deploy_deb_to_output(cfg: Config, build_dir: Path, built_kver: str) -> None: + # Find the newest file in build_dir matching linux-image-{built_kver}*.deb + image_deb_package = build_dir.glob(f"linux-image-{built_kver}*.deb") + image_deb_package = sorted(image_deb_package, key=lambda p: p.stat().st_mtime, reverse=True) + if not image_deb_package: + log.error("No linux-image-*.deb package found in %s", build_dir) + raise SystemExit(1) + image_deb_package = image_deb_package[0] + log.info("Found built kernel package: %s", image_deb_package) - for old in vmlinuz_dir.glob("vmlinuz-*"): - old.unlink(missing_ok=True) + # copy the deb package to the output directory + target_deb_output = obtain_target_artifact_path(cfg, ensure_parent=True) + log.debug("Copying built kernel package from %s to %s", image_deb_package, target_deb_output) + shutil.copy2(image_deb_package, target_deb_output) - shutil.copy2(kernel_image, vmlinuz_dir / f"vmlinuz-{built_kver}") + # Show dpkg info for the copied package + log.info("Kernel package info:") + run(["dpkg", "-I", str(target_deb_output)]) log.info("Kernel build complete:") - vmlinuz = vmlinuz_dir / f"vmlinuz-{built_kver}" - vmlinuz_size = vmlinuz.stat().st_size / (1024 * 1024) - log.info(" Image: %s (%.1fM)", vmlinuz, vmlinuz_size) - log.info(" Modules: %s/", usr_moddir) - log.info(" Version: %s", built_kver) - log.info(" Output: %s", cfg.kernel_output) + log.info( + " linux-image: %s (%.1fM)", + target_deb_output, + (target_deb_output.stat().st_size / (1024 * 1024)), + ) def build(cfg: Config) -> None: @@ -265,14 +329,20 @@ def build(cfg: Config) -> None: shutil.rmtree(cfg.kernel_output) ensure_dir(cfg.kernel_output) - build_dir = Path("/var/tmp/kernel-build") + log.info("Preparing kernel source...") + # We've a 2-deep because make bindeb-pkg outputs to parent directory of source proper + # Also because one can't share cache between architectures -- full rebuild everytime otherwise + build_dir = Path(KERNEL_BUILD_BASE_PATH) / f"build-{cfg.arch_info.kernel_arch}" + src_dir = download_kernel(cfg, build_dir) # this is one down from build_dir - if cfg.kernel_src and Path(cfg.kernel_src).is_dir(): - log.info("Using provided kernel source at %s", cfg.kernel_src) - src_dir = Path(cfg.kernel_src) - else: - src_dir = download_kernel(cfg.kernel_version, build_dir) + log.debug("Kernel source directory: %s", src_dir) + log.info("Configuring kernel...") configure_kernel(cfg, src_dir) + + log.info("Building kernel...") built_kver = build_kernel(cfg, src_dir) - install_kernel(cfg, src_dir, built_kver) + log.debug("Built kernel version: %s", built_kver) + + log.info("Installing kernel...") + deploy_deb_to_output(cfg, build_dir, built_kver) diff --git a/captain/oci/__init__.py b/captain/oci/__init__.py index ed1ece90..81ae1217 100644 --- a/captain/oci/__init__.py +++ b/captain/oci/__init__.py @@ -27,12 +27,10 @@ * ``skopeo`` extracts individual files for release workflows. """ -from captain.oci._common import compute_version_tag from captain.oci._publish import publish from captain.oci._pull import pull, tag_all, tag_image __all__ = [ - "compute_version_tag", "publish", "pull", "tag_all", diff --git a/captain/oci/_build.py b/captain/oci/_build.py index 4b0a22ba..4dbcce77 100644 --- a/captain/oci/_build.py +++ b/captain/oci/_build.py @@ -4,13 +4,11 @@ import contextlib import logging -import shutil import tarfile from datetime import datetime from pathlib import Path from captain import artifacts, buildah -from captain.util import get_arch_info log = logging.getLogger(__name__) @@ -36,33 +34,22 @@ def _deterministic_tar(file_path: Path, output_dir: Path) -> Path: return tar_path -def _collect_arch_artifacts( - project_dir: Path, - out: Path, - arch: str, - kernel_version: str, -) -> list[Path]: - """Collect and return the artifact files for a single architecture. +def _deterministic_tar_multiple(all_dtb_files: list[Path], dtb_tar_path, out: Path): + with tarfile.open(dtb_tar_path, "w") as tf: + for f in all_dtb_files: + info = tf.gettarinfo(str(f), arcname=str(f.relative_to(out))) + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + info.mode = 0o644 + with open(f, "rb") as fh: + tf.addfile(info, fh) - Returns [vmlinuz, initramfs, iso, checksums] paths in *out*. - """ - # Collect kernel - vmlinuz_dir = project_dir / "mkosi.output" / "kernel" / kernel_version / arch - vmlinuz_files = sorted(vmlinuz_dir.glob("vmlinuz-*")) if vmlinuz_dir.is_dir() else [] - oarch = get_arch_info(arch).output_arch - vmlinuz_dst = out / f"vmlinuz-{kernel_version}-{oarch}" - if vmlinuz_files: - shutil.copy2(vmlinuz_files[0], vmlinuz_dst) - log.info("kernel: %s", vmlinuz_dst) - else: - log.warning("No kernel image found for %s", arch) - - arch_files = [ - out / f"vmlinuz-{kernel_version}-{oarch}", - out / f"initramfs-{kernel_version}-{oarch}", - out / f"captainos-{kernel_version}-{oarch}.iso", - ] - checksums_path = out / f"sha256sums-{kernel_version}-{oarch}.txt" + +def _checksum_files(arch_files: list[Path], flavor_id: str, oarch: str, out: Path) -> list[Path]: + checksums_path = out / f"sha256sums-{flavor_id}-{oarch}.txt" artifacts.collect_checksums(arch_files, checksums_path) push_files = [*arch_files, checksums_path] @@ -76,12 +63,9 @@ def _collect_arch_artifacts( def _build_platform_image( layer_tars: list[Path], platform: str, - sha: str, - repository: str, *, created: str, - tag: str, - artifact_name: str, + oci_metadata: dict[str, str], base: str = "scratch", ) -> str: """Build an OCI image locally for *platform*. @@ -100,16 +84,6 @@ def _build_platform_image( """ os_name, arch = platform.split("/") epoch = int(datetime.fromisoformat(created.replace("Z", "+00:00")).timestamp()) - oci_metadata = { - "org.opencontainers.image.created": created, - "org.opencontainers.image.source": f"https://github.com/{repository}", - "org.opencontainers.image.revision": sha, - "org.opencontainers.image.version": tag, - "org.opencontainers.image.title": artifact_name, - "org.opencontainers.image.description": "CaptainOS build artifacts", - "org.opencontainers.image.vendor": "Tinkerbell", - "org.opencontainers.image.licenses": "Apache-2.0", - } # Build one layer per tar: from base → add file → commit → repeat. # Track intermediate image IDs so they can be cleaned up afterwards; @@ -133,6 +107,7 @@ def _build_platform_image( if prev != base: intermediates.append(prev) + log.debug("Cleaning up intermediate images: %s", intermediates) for img in intermediates: with contextlib.suppress(Exception): buildah.rmi(img) diff --git a/captain/oci/_common.py b/captain/oci/_common.py index 9bb5afc2..6c68ecf6 100644 --- a/captain/oci/_common.py +++ b/captain/oci/_common.py @@ -2,51 +2,13 @@ from __future__ import annotations -import subprocess -from pathlib import Path +import logging _ARCHES = ("amd64", "arm64") +log = logging.getLogger(__name__) + def _image_ref(registry: str, repository: str, artifact_name: str, tag: str) -> str: """Build a fully-qualified OCI image reference.""" return f"{registry}/{repository}/{artifact_name}:{tag}" - - -def compute_version_tag( - project_dir: Path, - sha: str, - *, - exclude: str | None = None, -) -> str: - """Compute the version tag from git describe + short SHA. - - Mirrors the CI logic:: - - VERSION=$(git describe --tags --first-parent --abbrev=0 \\ - --match 'v[0-9]*' 2>/dev/null || echo "v0.0.0") - TAG="${VERSION}-${SHA::7}" - """ - cmd = [ - "git", - "describe", - "--tags", - "--first-parent", - "--abbrev=0", - "--match", - "v[0-9]*", - ] - if exclude: - cmd += [f"--exclude={exclude}"] - try: - result = subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - cwd=project_dir, - ) - version = result.stdout.strip() - except subprocess.CalledProcessError: - version = "v0.0.0" - return f"{version}-{sha[:7]}" diff --git a/captain/oci/_publish.py b/captain/oci/_publish.py index 0bf79417..fa0343ed 100644 --- a/captain/oci/_publish.py +++ b/captain/oci/_publish.py @@ -9,10 +9,17 @@ from uuid import uuid4 from captain import buildah, skopeo +from captain.artifacts import OutputArchArtifactType from captain.config import Config -from captain.util import ensure_dir - -from ._build import _build_platform_image, _collect_arch_artifacts, _deterministic_tar +from captain.flavor import BaseFlavor +from captain.util import ensure_dir, get_arch_info + +from ._build import ( + _build_platform_image, + _checksum_files, + _deterministic_tar, + _deterministic_tar_multiple, +) from ._common import _ARCHES, _image_ref log = logging.getLogger(__name__) @@ -21,6 +28,7 @@ def _create_push_cleanup( image_ids: list[str], dest_ref: str, + oci_metadata: dict[str, str], ) -> None: """Create a manifest list from *image_ids*, push it to *dest_ref*, and clean up. @@ -31,14 +39,16 @@ def _create_push_cleanup( temp_name = f"captain-local-{uuid4().hex[:12]}" manifest_id: str | None = None try: - manifest_id = buildah.manifest_create(temp_name) + manifest_id = buildah.manifest_create(temp_name, oci_metadata) for image_id in image_ids: buildah.manifest_add(manifest_id, image_id) buildah.manifest_push(manifest_id, dest_ref) finally: if manifest_id is not None: + log.debug("Cleaning up manifest: %s", manifest_id) with contextlib.suppress(Exception): buildah.rmi(manifest_id) + log.debug("Cleaning up intermediate images: %s", image_ids) for image_id in image_ids: with contextlib.suppress(Exception): buildah.rmi(image_id) @@ -46,12 +56,10 @@ def _create_push_cleanup( def _publish_single_arch( *, + flavor: BaseFlavor, layer_tars: list[Path], ref: str, - tag: str, - sha: str, repository: str, - artifact_name: str, created: str, ) -> None: """Build a per-arch multi-arch index and push it. @@ -64,25 +72,36 @@ def _publish_single_arch( image_id = _build_platform_image( layer_tars, f"linux/{platform_arch}", - sha, - repository, created=created, - tag=tag, - artifact_name=artifact_name, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="inner-arch", + arch=platform_arch, + repository=repository, + ), ) image_ids.append(image_id) - _create_push_cleanup(image_ids, ref) + _create_push_cleanup( + image_ids=image_ids, + dest_ref=ref, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="outer-arch", + arch=None, + repository=repository, + ), + ) def _publish_combined( *, + flavor: BaseFlavor, arch_layer_tars: dict[str, list[Path]], registry: str, repository: str, artifact_name: str, tag: str, - sha: str, created: str, force: bool = False, ) -> bool: @@ -118,12 +137,10 @@ def _publish_combined( per_arch_ref, ) _publish_single_arch( + flavor=flavor, layer_tars=arch_layer_tars[arch], ref=per_arch_ref, - tag=per_arch_tag, - sha=sha, repository=repository, - artifact_name=artifact_name, created=created, ) @@ -136,28 +153,40 @@ def _publish_combined( image_id = _build_platform_image( arch_layer_tars[other], f"linux/{arch}", - sha, - repository, created=created, - tag=tag, - artifact_name=artifact_name, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="inner-combined", + arch=arch, + repository=repository, + ), base=f"docker://{per_arch_ref}", ) image_ids.append(image_id) - _create_push_cleanup(image_ids, combined_ref) + _create_push_cleanup( + image_ids=image_ids, + dest_ref=combined_ref, + oci_metadata=create_oci_metadata( + flavor=flavor, + image_type="outer-combined", + arch=None, + repository=repository, + ), + ) + return True def publish( cfg: Config, + flavor: BaseFlavor, *, target: str, registry: str, repository: str, artifact_name: str, tag: str, - sha: str, force: bool = False, ) -> None: """Collect artifacts and publish a multi-arch OCI index. @@ -184,44 +213,91 @@ def publish( return out = ensure_dir(cfg.output_dir) - created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Set created to the epoch so it is always the same + created = datetime(1970, 1, 1, tzinfo=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # Collect artifacts for every requested architecture. arch_files: dict[str, list[Path]] = {} + arch_directories: dict[str, list[Path]] = {} for arch in arches: - arch_files[arch] = _collect_arch_artifacts( - cfg.project_dir, - out, - arch, - cfg.kernel_version, + output_arch = get_arch_info(arch).output_arch + arch_artifacts = flavor.list_arch_artifacts(output_arch) + + # First, pick all the type=file artifacts for this arch. + file_arch_artifacts = [a for a in arch_artifacts if a.type == OutputArchArtifactType.FILE] + if not file_arch_artifacts: + log.warning("No file artifacts found for %s/%s", flavor.id, output_arch) + continue + + file_arch_artifacts_in_out = [] + for a in file_arch_artifacts: + path_in_out = out / a.name + if not path_in_out.is_file(): + log.error("Expected artifact file not found in output: %s", path_in_out) + raise SystemExit(1) + file_arch_artifacts_in_out.append(path_in_out) + + # Calculate the checksums & add to arch_files + arch_files[arch] = _checksum_files( + file_arch_artifacts_in_out, cfg.flavor_id, output_arch, out ) + # Also add directories; those are not checksummed + dir_arch_artifacts_in_out = [] + dir_arch_artifacts = [ + a for a in arch_artifacts if a.type == OutputArchArtifactType.DIRECTORY + ] + if dir_arch_artifacts: + for a in dir_arch_artifacts: + dir_in_out = out / a.name + if not dir_in_out.is_dir(): + log.error("Expected artifact directory not found in output: %s", dir_in_out) + raise SystemExit(1) + dir_arch_artifacts_in_out.append(dir_in_out) + arch_directories[arch] = dir_arch_artifacts_in_out + # Create deterministic layer tars (shared across manifest pushes). arch_layer_tars: dict[str, list[Path]] = {} + + # Add the files first, each in a deterministic tar for arch, files in arch_files.items(): - arch_layer_tars[arch] = [_deterministic_tar(f, out) for f in files] + log.info("Creating layer tars (single-file) for %s... files: %s", arch, files) + arch_layer_tars[arch] = [] + for f in files: + log.info("Creating layer tar (single-file) for %s... file: %s", arch, f) + arch_layer_tars[arch].append(_deterministic_tar(f, out)) + + # Add all directories (with every file within); single tar for better compression + for arch, dirs in arch_directories.items(): + for directory in dirs: + log.info("Creating layer tar (directory) for %s... directory: %s", arch, directory) + all_dir_files: list[Path] = sorted(directory.glob("**/*")) + all_dir_files = [f for f in all_dir_files if f.is_file()] # only files, not dirs + dir_tar_path = out / f".layer-dir-{cfg.flavor_id}-{arch}-{directory.name}.tar" + log.debug("Adding %d files to layer tar for arch %s", len(all_dir_files), arch) + _deterministic_tar_multiple(all_dir_files, dir_tar_path, out) + arch_layer_tars[arch].append(dir_tar_path) pushed = True try: if target == "combined": pushed = _publish_combined( + flavor=flavor, arch_layer_tars=arch_layer_tars, registry=registry, repository=repository, artifact_name=artifact_name, tag=tag, - sha=sha, created=created, force=force, ) else: _publish_single_arch( + flavor=flavor, layer_tars=arch_layer_tars[target], ref=final_ref, - tag=full_tag, - sha=sha, repository=repository, - artifact_name=artifact_name, created=created, ) finally: @@ -236,6 +312,8 @@ def publish( artifact_names: list[str] = [] for arch in arches: artifact_names.extend(f.name for f in arch_files.get(arch, [])) + artifact_names.extend(d.name for d in arch_directories.get(arch, [])) + platforms = [f"linux/{a}" for a in _ARCHES] log.info("") log.info("Publish complete") @@ -246,3 +324,50 @@ def publish( log.info(" Artifacts:") for name in artifact_names: log.info(" - %s", name) + + +def create_oci_metadata( + *, + flavor: BaseFlavor, + image_type: str, + arch: str | None = None, + repository: str, +) -> dict[str, str]: + title_parts = ["CaptainOS"] + title_parts += [f"{flavor.id}"] + title_parts += [f"{image_type}"] + if arch is not None: + title_parts += [f"{arch}"] + + desc_parts = ["Tinkerbell CaptainOS"] + + # Add flavor info + desc_parts += [f"flavor {flavor.id}"] + desc_parts += [f"({flavor.description})"] + + if arch is not None: + desc_parts += [f"{arch}"] + + match image_type: + case "inner-arch": + desc_parts += ["single-platform inner image"] + case "inner-combined": + desc_parts += ["combined inner image"] + case "outer-combined": + desc_parts += ["combined multi-platform image"] + case "outer-arch": + desc_parts += ["single-platform image"] + case _: + log.warning("Unknown OCI metadata type: %s", image_type) + + description = " ".join(desc_parts) + title = " ".join(title_parts) + + oci_metadata = { + "org.opencontainers.image.vendor": "Tinkerbell", + "org.opencontainers.image.licenses": "Apache-2.0", + "org.opencontainers.image.source": f"https://github.com/{repository}", + "org.opencontainers.image.title": title, + "org.opencontainers.image.description": description, + } + return oci_metadata diff --git a/captain/oci/_pull.py b/captain/oci/_pull.py index 13165f6a..4c8cc6d5 100644 --- a/captain/oci/_pull.py +++ b/captain/oci/_pull.py @@ -7,7 +7,7 @@ from captain import skopeo -from ._common import _ARCHES, _image_ref +from ._common import _image_ref log = logging.getLogger(__name__) @@ -29,10 +29,9 @@ def pull( """ tag_suffix = "" if target == "combined" else f"-{target}" ref = _image_ref(registry, repository, artifact_name, f"{tag}{tag_suffix}") - skopeo.export_image(ref, output_dir) + extracted = skopeo.export_image(ref, output_dir) - # Recap - extracted = sorted(f.name for f in Path(output_dir).iterdir() if f.is_file()) + # Recap — list what the pulled image contained, not what is on disk. log.info("") log.info("Pull complete") log.info(" Image: %s", ref) @@ -64,32 +63,45 @@ def tag_all( artifact_name: str, src_tag: str, new_tag: str, - arches: list[str] | None = None, + arches: list[str], ) -> None: """Tag all artifact images (per-arch + combined) with a new version.""" - arches = arches or list(_ARCHES) + all_tags = [] for a in arches: + arch_orig_tag = f"{src_tag}-{a}" + arch_new_tag = f"{new_tag}-{a}" + + if len(arches) == 1: + arch_new_tag = new_tag + + log.info("Tagging for arch %s → %s", arch_orig_tag, arch_new_tag) + tag_image( + registry=registry, + repository=repository, + artifact_name=artifact_name, + src_tag=arch_orig_tag, + new_tag=arch_new_tag, + ) + all_tags.append((arch_orig_tag, arch_new_tag)) + + # if both arm64 and amd64 arches, also tag the combined image. + if set(arches) >= {"amd64", "arm64"}: + # Tag the combined image (no arch suffix). + log.info("Tagging combined image %s → %s", src_tag, new_tag) tag_image( registry=registry, repository=repository, artifact_name=artifact_name, - src_tag=f"{src_tag}-{a}", - new_tag=f"{new_tag}-{a}", + src_tag=src_tag, + new_tag=new_tag, ) - # Tag the combined image (no arch suffix). - tag_image( - registry=registry, - repository=repository, - artifact_name=artifact_name, - src_tag=src_tag, - new_tag=new_tag, - ) + all_tags.append((src_tag, new_tag)) # Recap image = f"{registry}/{repository}/{artifact_name}" log.info("") log.info("Tag complete") log.info(" Image: %s", image) - for a in arches: - log.info(" %s-%s → %s-%s", src_tag, a, new_tag, a) - log.info(" %s → %s", src_tag, new_tag) + # log all_tags with arrows + for src, dest in all_tags: + log.info(" %s → %s", src, dest) diff --git a/captain/qemu.py b/captain/qemu.py index d8af60f7..3a2971a7 100644 --- a/captain/qemu.py +++ b/captain/qemu.py @@ -2,18 +2,20 @@ from __future__ import annotations -import argparse +import atexit import logging +import shutil import sys +import tempfile +from pathlib import Path from captain.config import Config -from captain.util import run +from captain.util import detect_current_machine_arch, run log = logging.getLogger(__name__) -# Tinkerbell kernel cmdline parameters. -# Maps the argparse dest name → kernel cmdline key. -# Insertion order is preserved. +# @TODO: not used, qemu cmd passes everything after -- as raw kernel cmdline. +# @TODO: maybe move those to Click params in _qemu.py and remove this list? _TINK_PARAMS: list[tuple[str, str]] = [ # (namespace_attr, cmdline_key) ("tink_worker_image", "tink_worker_image"), @@ -30,43 +32,16 @@ ] -def _tink_cmdline(args: argparse.Namespace) -> str: - """Build tinkerbell kernel cmdline fragment from parsed *args*.""" - parts: list[str] = [] - for attr, cmdline_key in _TINK_PARAMS: - value = getattr(args, attr, "") or "" - if not value: - continue - # Kernel cmdline is space-delimited; whitespace in values would - # split them into multiple arguments and silently change meaning. - if any(ch.isspace() for ch in value): - log.error( - "--%s must not contain whitespace; cannot safely add it to the kernel cmdline.", - attr.replace("_", "-"), - ) - sys.exit(1) - parts.append(f"{cmdline_key}={value}") - - # Static networking via ipam= parameter - ipam = getattr(args, "ipam", "") or "" - if ipam: - if any(ch.isspace() for ch in ipam): - log.error("--ipam must not contain whitespace.") - sys.exit(1) - parts.append(f"ipam={ipam}") - - return " ".join(parts) - - -def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: - """Boot the built image in QEMU for quick testing. - - *args* is the parsed :class:`argparse.Namespace` produced by - :mod:`configargparse`. When provided, Tinkerbell kernel cmdline - parameters are drawn from it instead of the environment. - """ - kernel = cfg.output_dir / f"vmlinuz-{cfg.kernel_version}-{cfg.arch_info.output_arch}" - initrd = cfg.output_dir / f"initramfs-{cfg.kernel_version}-{cfg.arch_info.output_arch}" +def run_qemu(cfg: Config, args: list[str] | None = None) -> None: + """Boot the built image in QEMU for quick testing.""" + kernel = cfg.output_dir / f"vmlinuz-{cfg.flavor_id}-{cfg.arch_info.output_arch}" + initrd = cfg.output_dir / f"initramfs-{cfg.flavor_id}-{cfg.arch_info.output_arch}" + + log.debug("Looking for kernel at %s", kernel) + log.debug("Looking for initramfs at %s", initrd) + + log.debug("Current machine arch: %s", detect_current_machine_arch()) + log.debug("Current machine platform: %s", sys.platform) missing: list[str] = [] if not kernel.is_file(): @@ -77,27 +52,45 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: log.error("Build artifacts not found:") for m in missing: log.error(" %s", m) - log.error("Run './build.py --kernel-version %s' first.", cfg.kernel_version) sys.exit(1) - tink = _tink_cmdline(args) if args is not None else "" - if args is not None and not any( - getattr(args, v, None) for v in ("tink_worker_image", "tink_docker_registry") - ): - log.warning( - "Neither --tink-worker-image nor --tink-docker-registry is set. " - "tink-agent services will not start." - ) - - log.info("Booting CaptainOS in QEMU (Ctrl-A X to exit)...") - - qemu_cmd = cfg.arch_info.qemu_binary - append = f"console=ttyS0 audit=0 {tink} {cfg.qemu_append}".strip() - - log.info("Kernel cmdline: %s", append) + tink = " ".join(args) if args is not None else "" + log.info("Booting CaptainOS in QEMU ('Ctrl-A x' to exit)...") + + cmdline_compos: list[str] = [ + "systemd.journald.forward_to_console=1", + # "systemd.log_level=debug", + # "systemd.log_target=console", + f"{tink}", + ] + + per_arch_extra_cmds: list[str] = [] + match cfg.arch: + # direct kernel boot for x86 + case "amd64": + qemu_amd64_params(cfg, cmdline_compos, per_arch_extra_cmds) + + # indirect boot via OVMF for arm64 + case "arm64": + qemu_arm64_params(cfg, cmdline_compos, per_arch_extra_cmds) + + # If matching the host architecture, enable hardware acceleration via KVM or HVF + if cfg.arch == detect_current_machine_arch(): + # if on Darwin and arm64 (Apple Silicon), enable hvf accel. + if sys.platform == "darwin": + log.info("Native run on Apple, enabling HVF acceleration") + per_arch_extra_cmds += ["-accel", "hvf"] + + if sys.platform == "linux" and Path("/dev/kvm").is_char_device(): + log.info("Native run on Linux with KVM available, enabling KVM acceleration") + per_arch_extra_cmds += ["-accel", "kvm"] + + append = " ".join(cmdline_compos).strip() + log.info("Kernel cmdline: '%s'", append) run( [ - qemu_cmd, + cfg.arch_info.qemu_binary, + *per_arch_extra_cmds, "-kernel", str(kernel), "-initrd", @@ -114,3 +107,109 @@ def run_qemu(cfg: Config, args: argparse.Namespace | None = None) -> None: "-no-reboot", ], ) + + +def qemu_amd64_params(cfg: Config, cmdline_compos: list[str], per_arch_extra_cmds: list[str]): + cmdline_compos.insert(0, "console=ttyS0") + per_arch_extra_cmds += ["-machine", "q35"] + per_arch_extra_cmds += ["-cpu", "max"] + find_ovmf_firmware( + cfg, + per_arch_extra_cmds, + [ + "edk2-x86_64-code.fd", # Darwin/homebrew + "OVMF_CODE_4M.fd", # Debian's 'ovmf' package - recent + "OVMF_CODE.fd", # Debian's 'ovmf' package - older + ], + [ + "edk2-i386-vars.fd", # Darwin/homebrew + "OVMF_VARS_4M.fd", # Debian's 'ovmf' package - recent + "OVMF_VARS.fd", # Debian's 'ovmf' package - older + ], + ) + + +def qemu_arm64_params(cfg: Config, cmdline_compos: list[str], per_arch_extra_cmds: list[str]): + cmdline_compos.insert(0, "console=ttyAMA0") + per_arch_extra_cmds += ["-machine", "virt"] + per_arch_extra_cmds += ["-cpu", "host"] + find_ovmf_firmware( + cfg, + per_arch_extra_cmds, + [ + "edk2-aarch64-code.fd", # Darwin/homebrew + "AAVMF_CODE.no-secboot.fd", # Debian's qemu-efi-aarch64 - recent + "AAVMF_CODE.fd", # Debian's qemu-efi-aarch64 - older + ], + [ + "edk2-arm-vars.fd", # Darwin/homebrew + "AAVMF_VARS.fd", # Debian's qemu-efi-aarch64 + ], + ) + + +def find_ovmf_firmware( + cfg: Config, per_arch_extra_cmds: list[str], fw_paths: list[str], vars_paths: list[str] +): + base_dirs: list[Path] = [] + match sys.platform: + case "darwin": + qemu_brew_prefix = run(["brew", "--prefix", "qemu"], capture=True).stdout.strip() + base_dirs.append(Path(qemu_brew_prefix) / "share" / "qemu") + case "linux": + base_dirs += [Path("/usr/share/OVMF"), Path("/usr/share/AAVMF")] + + # keep only the base_dirs that actually exist and are directories + valid_base_dirs = [d for d in base_dirs if d.is_dir()] + if not valid_base_dirs: + log.error( + "No valid base directories found for OVMF firmware on platform %s: %s", + sys.platform, + base_dirs, + ) + log.error( + "Install the appropriate OVMF package for arch %s your distribution and try again.", + cfg.arch, + ) + sys.exit(1) + + # Now just combinatorics: for each base dir, check if any of the expected firmware paths exist. + combined_full_fw_paths = [base / f for base in valid_base_dirs for f in fw_paths] + # find the first that exists and is a file; if none, edk2_path is None + edk2_path = next((p for p in combined_full_fw_paths if p.is_file()), None) + + if edk2_path is None: + log.error( + "OVMF code binary for arch %s not found in any of the expected paths: %s", + cfg.arch, + combined_full_fw_paths, + ) + sys.exit(1) + + # Again, but for the vars binary, which is also required. + combined_full_vars_paths = [base / f for base in valid_base_dirs for f in vars_paths] + vars_path_ro = next((p for p in combined_full_vars_paths if p.is_file()), None) + if vars_path_ro is None: + log.error( + "OVMF vars binary for arch %s not found in any of the expected paths: %s", + cfg.arch, + combined_full_vars_paths, + ) + sys.exit(1) + + # create a temporary file and copy the vars_path_ro there; clean it up on exit + vars_path = Path(tempfile.mkstemp(suffix=f"captain.qemu.efivars.{vars_path_ro.name}.fd")[1]) + log.info("Copying OVMF vars binary from %s to temporary file %s", vars_path_ro, vars_path) + shutil.copy2(vars_path_ro, vars_path) + atexit.register( + lambda: ( + log.info("Cleaning up temporary OVMF vars file at %s", vars_path), + vars_path.unlink(missing_ok=True), + ) + ) + + log.info("Using OVMF firmware code binary at %s", edk2_path) + per_arch_extra_cmds += ["-drive", f"if=pflash,format=raw,readonly=on,file={edk2_path!s}"] + + log.info("Using OVMF firmware vars binary at %s from %s", vars_path, vars_path_ro) + per_arch_extra_cmds += ["-drive", f"if=pflash,format=raw,file={vars_path!s}"] diff --git a/captain/skopeo.py b/captain/skopeo.py index a3b5712d..925e257f 100644 --- a/captain/skopeo.py +++ b/captain/skopeo.py @@ -9,6 +9,7 @@ import json import logging +import os import tarfile from pathlib import Path @@ -53,7 +54,20 @@ def copy(src: str, dest: str) -> None: destination differ only in the tag component. """ log.info("skopeo copy %s → %s", src, dest) - run(["skopeo", "copy", "--all", f"docker://{src}", f"docker://{dest}"]) + run( + [ + "skopeo", + "copy", + *( + ["--src-tls-verify=false", "--dest-tls-verify=false"] + if os.environ.get("REGISTRY_INSECURE") == "1" + else [] + ), + "--all", + f"docker://{src}", + f"docker://{dest}", + ] + ) def copy_to_dir( @@ -70,7 +84,11 @@ def copy_to_dir( Returns *output_dir*. """ output_dir.mkdir(parents=True, exist_ok=True) - cmd: list[str] = ["skopeo", "copy"] + cmd: list[str] = [ + "skopeo", + "copy", + *(["--src-tls-verify=false"] if os.environ.get("REGISTRY_INSECURE") == "1" else []), + ] if platform: parts = platform.split("/") if len(parts) == 2: @@ -86,17 +104,23 @@ def export_image( output_dir: Path, *, platform: str | None = None, -) -> None: +) -> list[str]: """Download and extract all layers from *image_ref* into *output_dir*. Uses ``skopeo copy`` to download the image to a temporary directory, parses the manifest to find layer blobs, and extracts each layer tar with path-traversal protection. + + Returns the sorted list of top-level entries (files and directories) + that the pulled image's layers contained. This reflects the image + contents, not whatever happens to be on disk in *output_dir*. """ import tempfile output_dir.mkdir(parents=True, exist_ok=True) + extracted: set[str] = set() + with tempfile.TemporaryDirectory(prefix="skopeo-export-") as tmp: tmp_dir = Path(tmp) copy_to_dir(image_ref, tmp_dir, platform=platform) @@ -119,4 +143,11 @@ def export_image( log.info("Extracting layer %s…", digest_str[:20]) with tarfile.open(blob_file, "r:*") as tf: - safe_extractall(tf, output_dir) + members = tf.getmembers() + for m in members: + parts = Path(m.name).parts + if parts and parts[0] not in (".", ""): + extracted.add(parts[0]) + safe_extractall(tf, output_dir, members=members) + + return sorted(extracted) diff --git a/captain/tools.py b/captain/tools.py index d4f6facf..ac2a182e 100644 --- a/captain/tools.py +++ b/captain/tools.py @@ -158,7 +158,7 @@ def download_tool(tool: ToolSpec, arch: str, output_base: Path, force: bool) -> def download_all(cfg: Config) -> None: - """Download all tools into mkosi.output/tools/{arch}/.""" + """Download all tools into mkosi.input/tools/{arch}/.""" arch = cfg.arch_info.dl_arch output_base = cfg.tools_output diff --git a/captain/util.py b/captain/util.py index 9876813b..26432a60 100644 --- a/captain/util.py +++ b/captain/util.py @@ -4,12 +4,20 @@ import logging import os +import platform import subprocess import sys import tarfile from dataclasses import dataclass from pathlib import Path +from rich.panel import Panel +from rich.rule import Rule +from rich.syntax import Syntax +from rich.table import Table + +import captain + log = logging.getLogger(__name__) @@ -26,7 +34,6 @@ class ArchInfo: dl_arch: str # architecture name in download URLs mkosi_arch: str # mkosi --architecture value qemu_binary: str # QEMU system emulator binary - strip_prefix: str # prefix for strip command def get_arch_info(arch: str) -> ArchInfo: @@ -37,13 +44,12 @@ def get_arch_info(arch: str) -> ArchInfo: arch="amd64", output_arch="x86_64", kernel_arch="x86_64", - cross_compile="", + cross_compile="x86_64-linux-gnu-", image_target="bzImage", kernel_image_path="arch/x86/boot/bzImage", dl_arch="amd64", mkosi_arch="x86-64", qemu_binary="qemu-system-x86_64", - strip_prefix="", ) case "arm64" | "aarch64": return ArchInfo( @@ -56,13 +62,22 @@ def get_arch_info(arch: str) -> ArchInfo: dl_arch="arm64", mkosi_arch="arm64", qemu_binary="qemu-system-aarch64", - strip_prefix="aarch64-linux-gnu-", ) case _: log.error("Unsupported architecture: %s", arch) sys.exit(1) +def detect_current_machine_arch() -> str: + machine = platform.machine().lower() + if machine in ("aarch64", "arm64"): + return "arm64" + elif machine in ("x86_64", "amd64"): + return "amd64" + else: + raise RuntimeError(f"Unsupported architecture: {machine}") + + def run( cmd: list[str], *, @@ -75,7 +90,36 @@ def run( run_env: dict[str, str] | None = None if env is not None: run_env = {**os.environ, **env} - return subprocess.run( + + # If not capturing, and debugging, emit a Rich separator line, for visual clarity. + if log.isEnabledFor(logging.DEBUG): + # join cmd into a single string; elements with spaces or special chars should be quoted + special = ('"', "'", "\\", "$", ",", "=", "/") + cmd_joined = " ".join( + f'"{c}"' if " " in c or any(s in c for s in special) else c for c in cmd + ) + syntax = Syntax( + cmd_joined, "bash", theme="monokai", word_wrap=True, background_color="default" + ) + panel = Panel( + syntax, + title="Executing shell command", + width=captain.console.width, + ) + captain.console.print(panel) + if env: + # add a Rich Table with the env vars, two columns + table = Table(show_header=True, header_style="bold magenta") + table.add_column("Env Var", style="cyan", no_wrap=True) + table.add_column("Value", style="white") + for k, v in env.items(): + table.add_row(k, v) + captain.console.print(table) + + if not capture: + captain.console.print(Rule(f"⮕ Starting subprocess: {cmd} ⮕", style="green")) + + proc = subprocess.run( cmd, check=check, capture_output=capture, @@ -84,6 +128,14 @@ def run( cwd=cwd, ) + if capture: + return proc + + if log.isEnabledFor(logging.DEBUG): + captain.console.print(Rule(f"⮕ Finished subprocess: {cmd} ⮕", style="green")) + + return proc + def ensure_dir(path: Path) -> Path: """Create a directory (and parents) if it doesn't exist, return the path.""" @@ -180,3 +232,9 @@ def check_dependencies(arch: str) -> list[str]: Returns a list of missing command names (empty if all found). """ return check_kernel_dependencies(arch) + check_mkosi_dependencies() + + +def symlink_relative(initramfs_dst: Path, initramfs_src: Path): + relative = Path(os.path.relpath(initramfs_src, initramfs_dst.parent)) + log.debug("Symlinking %s to %s (relative path: %s)", initramfs_dst, initramfs_src, relative) + initramfs_dst.symlink_to(relative) diff --git a/custom/files/etc/custom.example b/custom/files/etc/custom.example new file mode 100644 index 00000000..5ef9fff6 --- /dev/null +++ b/custom/files/etc/custom.example @@ -0,0 +1 @@ +This is an example; this file will end up in /etc/custom.example on the built CaptainOS filesystem. diff --git a/custom/mkosi-finalize/example-finalize.sh b/custom/mkosi-finalize/example-finalize.sh new file mode 100644 index 00000000..ea1c09c3 --- /dev/null +++ b/custom/mkosi-finalize/example-finalize.sh @@ -0,0 +1,2 @@ +log warn "Example custom finalize script (custom/mkosi-finalize/example-finalize.sh) running in mkosi finalize phase. This runs after the image is built and before it is packaged." +# Keep in mind that the target filesystem is at /buildroot at this stage, and commands ran here are all powerful ref the host system. diff --git a/custom/mkosi-postinst/example-postinst.sh b/custom/mkosi-postinst/example-postinst.sh new file mode 100644 index 00000000..fdcc0ef5 --- /dev/null +++ b/custom/mkosi-postinst/example-postinst.sh @@ -0,0 +1,58 @@ +log warn "Example custom post-install script (custom/mkosi-postinst/example-postinst.sh) running in mkosi post-install phase. This runs after packages are installed." +# Runs *outside* the image chroot after packages are installed. +# The target filesystem is at /buildroot; use 'mkosi-chroot' to run commands inside the chroot environment. + +declare -i ENABLE_EXAMPLE_CONTAINERD_MIRRORS=0 # flip to 1 to enable +declare -i ENABLE_EXAMPLE_CA_CERTS=0 # flip to 1 to enable + +### Example: configure containerd registry mirrors for a few common registries. +# Each target registry is a directory under /etc/containerd/certs.d with a hosts.toml file. +# You could also do this with individual files and directories in custom/files/ dir. + +if [[ ${ENABLE_EXAMPLE_CONTAINERD_MIRRORS} -gt 0 ]]; then + + declare -A registry_mirrors=( + ["docker.io"]="http://harbor.app.192.168.66.171.nip.io/v2/docker.io" + ["quay.io"]="http://harbor.app.192.168.66.171.nip.io/v2/quay.io" + ["ghcr.io"]="http://harbor.app.192.168.66.171.nip.io/v2/ghcr.io" + ["registry.k8s.io"]="http://harbor.app.192.168.66.171.nip.io/v2/registry.k8s.io" + ) + + log info "Configuring containerd registry mirrors..." + mkdir -pv "${BUILDROOT}/etc/containerd/certs.d" + + for registry in "${!registry_mirrors[@]}"; do + mirror="${registry_mirrors[$registry]}" + log info "Configuring mirror for registry '$registry' pointing to target '$mirror'" + mkdir -pv "${BUILDROOT}/etc/containerd/certs.d/${registry}" + + cat <<- ONE_HOSTS_TOML > "${BUILDROOT}/etc/containerd/certs.d/${registry}/hosts.toml" +server = "${mirror}" +[host."${mirror}"] +capabilities = ["pull", "resolve"] +override_path = true +ONE_HOSTS_TOML + done + + log info "Containerd registry mirrors configuration complete." +fi + +### Example: add a custom root CA certificate to the image's trust store. +# You could also do this by dropping .crt files in custom/files/usr/local/share/ca-certificates +if [[ ${ENABLE_EXAMPLE_CA_CERTS} -gt 0 ]]; then + + log info "Adding custom root CA certificate to image trust store..." + + curl https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x1.pem \ + -o "${BUILDROOT}/usr/local/share/ca-certificates/letsencrypt-stg-root-x1.crt" + + curl https://letsencrypt.org/certs/staging/letsencrypt-stg-root-x2.pem \ + -o "${BUILDROOT}/usr/local/share/ca-certificates/letsencrypt-stg-root-x2.crt" + + # update the CA certificate bundle to include the new certs. + # this is not needed when using .crt files in the custom/files/ directory, since the regular + # (non-custom) postinst already does this. + mkosi-chroot update-ca-certificates --fresh --verbose + + log info "Custom root CA certificate added and CA bundle updated." +fi diff --git a/custom/packages.txt b/custom/packages.txt new file mode 100644 index 00000000..da97e4cd --- /dev/null +++ b/custom/packages.txt @@ -0,0 +1,2 @@ +# Add (Debian) package names, one per line, to be included in CaptainOS. +cowsay diff --git a/docs/design-decisions/oci-tooling-buildah-skopeo.md b/docs/design-decisions/oci-tooling-buildah-skopeo.md index 9cb984d0..1d1b724e 100644 --- a/docs/design-decisions/oci-tooling-buildah-skopeo.md +++ b/docs/design-decisions/oci-tooling-buildah-skopeo.md @@ -12,7 +12,8 @@ We iterated through three OCI client tools before arriving at the current approa ## Tool #1: ORAS -The initial implementation (`b34c119`..`90e6f75`) used [ORAS](https://oras.land/) directly in CI workflows to push artifact files to GHCR: +The initial implementation (`b34c119`..`90e6f75`) used [ORAS](https://oras.land/) directly in CI workflows to push +artifact files to GHCR: ```bash oras push \ @@ -21,61 +22,89 @@ oras push \ vmlinuz-x86_64 initramfs-x86_64 captainos-x86_64.iso ... ``` -**Problem:** `oras push` creates **OCI artifacts** (with an artifact manifest), not **OCI images**. Containerd cannot pull OCI artifacts because the layer count and `rootfs.diff_ids` in the config don't match — the image simply isn't a valid container image. This meant **Kubernetes image volumes didn't work**, which was a hard requirement. +**Problem:** `oras push` creates **OCI artifacts** (with an artifact manifest), not **OCI images**. Containerd cannot +pull OCI artifacts because the layer count and `rootfs.diff_ids` in the config don't match — the image simply isn't a +valid container image. This meant **Kubernetes image volumes didn't work**, which was a hard requirement. -The fix wasn't a flag or config change — ORAS is designed for arbitrary artifact storage, not for producing container images that a runtime can unpack. +The fix wasn't a flag or config change — ORAS is designed for arbitrary artifact storage, not for producing container +images that a runtime can unpack. ## Tool #2: Crane -We replaced ORAS with [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) (`58f25cf`), wrapping it in a Python module (`captain/crane.py`). Crane produces valid OCI images with correct `rootfs.diff_ids`, so containerd could pull them and Kubernetes image volumes worked. +We replaced ORAS with [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) (`58f25cf`), wrapping +it in a Python module (`captain/crane.py`). Crane produces valid OCI images with correct `rootfs.diff_ids`, so +containerd could pull them and Kubernetes image volumes worked. -The crane-based approach used `crane append` to add layers, `crane mutate` to set platform metadata and annotations, `crane index append` for multi-arch indexes, and `crane export` to extract files during release. +The crane-based approach used `crane append` to add layers, `crane mutate` to set platform metadata and annotations, +`crane index append` for multi-arch indexes, and `crane export` to extract files during release. -**Problem:** `crane append` is a **remote-first** operation — each call pushes a manifest to the registry. When building an image with four artifact layers, each `crane append` overwrites the tag with a new manifest, leaving the previous manifests as **untagged ("orphaned") images** in the registry. For every publish cycle we created several untagged manifests per architecture. +**Problem:** `crane append` is a **remote-first** operation — each call pushes a manifest to the registry. When building +an image with four artifact layers, each `crane append` overwrites the tag with a new manifest, leaving the previous +manifests as **untagged ("orphaned") images** in the registry. For every publish cycle we created several untagged +manifests per architecture. We tried multiple mitigations: -- **WIP tags** (`6cca281`): Push layers to a temporary `-wip` tag, then retag to the final ref. But GHCR requires `packages:delete` permission to remove tags, so WIP tag cleanup silently failed, leaving orphaned `-wip` tags instead. -- **Single-layer bundling** (`2cb2206`): Bundle all artifact files into one tar so `crane append` only runs once per platform manifest. This reduced but didn't eliminate the problem — `crane mutate` (for setting platform, annotations, labels) and `crane set_created` each rewrite the manifest, still producing untagged intermediates. -- **Digest refs** (`f1c8e8a`): Capture digests after each platform push and pass digest refs to `crane index append` instead of tags. This avoided some tag churn but didn't solve the fundamental issue. +- **WIP tags** (`6cca281`): Push layers to a temporary `-wip` tag, then retag to the final ref. But GHCR requires + `packages:delete` permission to remove tags, so WIP tag cleanup silently failed, leaving orphaned `-wip` tags instead. +- **Single-layer bundling** (`2cb2206`): Bundle all artifact files into one tar so `crane append` only runs once per + platform manifest. This reduced but didn't eliminate the problem — `crane mutate` (for setting platform, annotations, + labels) and `crane set_created` each rewrite the manifest, still producing untagged intermediates. +- **Digest refs** (`f1c8e8a`): Capture digests after each platform push and pass digest refs to `crane index append` + instead of tags. This avoided some tag churn but didn't solve the fundamental issue. -The root cause is architectural: crane operates directly on the registry for every mutation. There is no local staging area — every `append`, `mutate`, or `edit config` call creates a new manifest on the remote, and the old one becomes untagged. Over time this accumulated significant registry garbage. +The root cause is architectural: crane operates directly on the registry for every mutation. There is no local staging +area — every `append`, `mutate`, or `edit config` call creates a new manifest on the remote, and the old one becomes +untagged. Over time this accumulated significant registry garbage. ## Tool #3: Buildah + Skopeo (current) The current approach splits responsibilities: -- **Buildah** handles image *construction*: creating containers from scratch, adding layers, setting metadata (OS, arch, annotations, labels), committing images, and managing manifest lists. All operations happen **locally** — nothing touches the registry until the final `buildah manifest push --all`. -- **Skopeo** handles image *read operations*: inspecting digests, checking if images exist, copying/retagging between refs, and downloading images for artifact extraction. +- **Buildah** handles image *construction*: creating containers from scratch, adding layers, setting metadata (OS, arch, + annotations, labels), committing images, and managing manifest lists. All operations happen **locally** — nothing + touches the registry until the final `buildah manifest push --all`. +- **Skopeo** handles image *read operations*: inspecting digests, checking if images exist, copying/retagging between + refs, and downloading images for artifact extraction. + +Current CLI entry points are grouped under: + +```bash +uv run captain release --help +uv run captain release publish --help +uv run captain release pull --help +uv run captain release tag --help +``` ### Why this solves the problems -1. **No untagged manifests.** Buildah builds the complete image locally (all layers, all metadata, correct timestamps) and pushes the finished manifest in one shot. There are no intermediate remote manifests to orphan. +1. **No untagged manifests.** Buildah builds the complete image locally (all layers, all metadata, correct timestamps) + and pushes the finished manifest in one shot. There are no intermediate remote manifests to orphan. -2. **Containerd compatibility.** Buildah produces standard OCI images with valid `rootfs.diff_ids` — Kubernetes image volumes work correctly. +2. **Containerd compatibility.** Buildah produces standard OCI images with valid `rootfs.diff_ids` — Kubernetes image + volumes work correctly. -3. **Multi-arch indexes.** `buildah manifest create/add/push` manages OCI indexes natively, pushing the index and all referenced platform manifests in a single `push --all` operation. +3. **Multi-arch indexes.** `buildah manifest create/add/push` manages OCI indexes natively, pushing the index and all + referenced platform manifests in a single `push --all` operation. -4. **No external binary downloads.** Both buildah and skopeo are available as standard distro packages (`apt-get install buildah skopeo`), eliminating the need to download and verify release tarballs from GitHub as we did with crane. +4. **No external binary downloads.** Both buildah and skopeo are available as standard distro packages ( + `apt-get install buildah skopeo`), eliminating the need to download and verify release tarballs from GitHub as we did + with crane. 5. **Rootless operation.** Both tools work without a container runtime or root privileges when using `fuse-overlayfs`. ### Trade-offs -- Buildah's local storage model uses more disk than crane's append-directly-to-registry approach, though this is negligible for CI runners with ephemeral storage. -- Two tools instead of one, but the separation of concerns (write vs. read) maps cleanly to the codebase: `captain/buildah.py` for construction, `captain/skopeo.py` for inspection/extraction. - -## Timeline - -| Commit | Change | -|--------|--------| -| `b34c119` | Initial release workflow with ORAS | -| `4d40c3d` | Add multi-arch OCI index via `oras manifest index create` | -| `65a15a7` | Fix `oras push` flag (`--artifact-platform`) | -| `58f25cf` | Replace ORAS with crane; add Python `crane.py` module | -| `2de90bf` | Fix `crane mutate` (`--set-platform` vs `--platform`) | -| `2cb2206` | Bundle artifacts into single tar to reduce untagged images | -| `f1c8e8a` | Use digest refs for `crane index append` | -| `40e5690` | Per-artifact layers for dedup with multi-arch indexes | -| `dcc3fb2` | Add OCI metadata, created timestamps, simplify layout | -| current | Replace crane with buildah + skopeo | +- Buildah's local storage model uses more disk than crane's append-directly-to-registry approach, though this is + negligible for CI runners with ephemeral storage. +- Two tools instead of one, but the separation of concerns (write vs. read) maps cleanly to the codebase: + `captain/buildah.py` for construction, `captain/skopeo.py` for inspection/extraction. + +### Operational notes + +- Release commands are exposed as a click command group (`captain release ...`) with shared options (`--registry`, + `--repository`, `--target`, `--src-tag`, and `--release-mode`). +- Insecure registries can be supported via `REGISTRY_INSECURE=1` (translated to `--tls-verify=false` in wrapper + commands). +- Per-arch and combined images still use deterministic layers to preserve blob deduplication behavior. + diff --git a/docs/kernel-build.md b/docs/kernel-build.md index 0130ef39..46aa975d 100644 --- a/docs/kernel-build.md +++ b/docs/kernel-build.md @@ -1,178 +1,27 @@ # Kernel Build Process -CaptainOS builds a custom Linux kernel from upstream source with -project-specific defconfigs. The build is orchestrated by the `captain.kernel` -module and driven through the CLI (`./build.py kernel`). +CaptainOS now has two kernel paths, selected by flavor and command: -## Quick Start +1. **Flavor-provided distro kernels** (for example `trixie-full` and several Armbian-based arm64 flavors). +2. **Captain-built kernels** via `captain kernel` (notably `trixie-slim` workflows). -```bash -# Build the kernel (defaults to Docker mode, amd64, kernel 6.18.16) -./build.py kernel - -# Build for arm64 -./build.py kernel --arch=arm64 - -# Use a local kernel source tree instead of downloading -./build.py kernel --kernel-src=/path/to/linux - -# Force a rebuild even if outputs already exist -./build.py kernel --force-kernel - -# Build natively (no Docker) -./build.py kernel --kernel-mode=native -``` - -Every flag also has an environment variable form (e.g. `ARCH`, `KERNEL_VERSION`, -`KERNEL_MODE`, `KERNEL_SRC`, `FORCE_KERNEL`). - -## Execution Modes - -The `--kernel-mode` flag controls how the kernel is built: - -| Mode | Description | -|----------|------------------------------------------------------------------------| -| `docker` | (default) Builds inside the `captainos-builder` Docker container. | -| `native` | Builds directly on the host; requires kernel build tools to be installed. | -| `skip` | Skips the kernel stage entirely. | - -In Docker mode, the builder container is based on Debian Trixie and includes -all kernel build dependencies (gcc, make, flex, bison, bc, libelf, libssl, -dwarves/pahole, etc.) plus cross-compilation toolchains for arm64. Inside -the container, the mode is forced to `native` so Docker is never invoked -recursively. - -## Build Pipeline - -The `kernel.build()` function runs four stages sequentially: - -### 1. Download - -`kernel.download_kernel()` fetches the upstream tarball from -`cdn.kernel.org`: - -``` -https://cdn.kernel.org/pub/linux/kernel/v{major}.x/linux-{version}.tar.xz -``` - -The tarball is extracted into `/var/tmp/kernel-build/linux-{version}/`. -If the extracted source directory already exists, it is reused. - -When `--kernel-src` is set, this step is skipped and the local source tree -is used as-is. - -### 2. Configure - -`kernel.configure_kernel()` applies the project defconfig for the target -architecture: - -1. Copies `kernel.configs/{major}.{minor}.y.{arch}` into the source tree as `.config`. -2. Runs `make olddefconfig` to resolve any new symbols against defaults. -3. Saves the fully resolved config to `kernel.configs/.config.resolved.{branch}.{arch}` for - debugging. - -If no defconfig file exists for the target kernel version and arch, the build -exits with an error listing the available kernel branches. - -For `x86_64` builds, the `COMMAND_LINE_SIZE` is patched from 2048 to 4096 -in `arch/x86/include/asm/setup.h` because Tinkerbell passes large kernel -command lines. - -Cross-compilation is set up automatically via the `ARCH` and -`CROSS_COMPILE` environment variables (e.g. `CROSS_COMPILE=aarch64-linux-gnu-` -for arm64). - -### 3. Compile - -`kernel.build_kernel()` runs the parallel make: +## Commands ```bash -make -j$(nproc) {image_target} modules +uv run captain kernel --help ``` -The image target is architecture-dependent: - -| Architecture | `ARCH` | Image target | Output path | -|-------------|-----------|-------------|-------------------------------| -| amd64 | `x86_64` | `bzImage` | `arch/x86/boot/bzImage` | -| arm64 | `arm64` | `Image` | `arch/arm64/boot/Image` | - -After compilation, `make -s kernelrelease` is invoked to determine the -exact built kernel version string (e.g. `6.18.16-captainos`). - -### 4. Install - -`kernel.install_kernel()` places the built artifacts into the output tree: - -1. **Module installation** — `make INSTALL_MOD_PATH=... modules_install` - installs modules to `mkosi.output/kernel/{version}/{arch}/modules/`. -2. **Strip** — Debug symbols are stripped from every `.ko` file with - `strip --strip-unneeded`. -3. **Compress** — Modules are compressed with `zstd --rm -q -19` producing - `.ko.zst` files. The defconfig enables `CONFIG_MODULE_COMPRESS_ZSTD` - and `CONFIG_MODULE_DECOMPRESS` so the kernel loads compressed modules at - runtime. -4. **Clean up** — The `build` and `source` symlinks are removed from the - modules directory. -5. **Merged-usr layout** — Modules are relocated from `lib/modules/` to - `usr/lib/modules/` to follow the merged-usr filesystem convention. -6. **depmod** — `depmod -a` regenerates module dependency metadata for - the compressed `.ko.zst` files. -7. **Kernel image** — The kernel image (`vmlinuz-{version}`) is copied to - `mkosi.output/kernel/{kernel_version}/{arch}/`. It is kept separate from - the extra-tree so it is **not** included in the initramfs CPIO — iPXE loads - the kernel independently. - -## Output Layout - -After a successful build, the kernel stage produces: - -``` -mkosi.output/ -├── tools/{arch}/ # tools only (containerd, runc, etc.) -│ ├── usr/local/bin/ -│ └── opt/cni/bin/ -└── kernel/{kernel_version}/{arch}/ - ├── vmlinuz-{version} # kernel image (bzImage or Image) - └── modules/ # passed as --extra-tree to mkosi - └── usr/lib/modules/{version}/ - ├── kernel/... # compressed .ko.zst module files - ├── modules.dep - ├── modules.dep.bin - └── ... -``` - -The tools and modules subtrees are both passed to mkosi via -separate `--extra-tree=` flags and merged into the initramfs. The vmlinuz -image is collected into `out/` by `artifacts.collect_kernel()` as -`out/vmlinuz-{kernel_version}-{arch}`. - -## Idempotency - -The CLI checks for existing outputs before starting: - -- If both `mkosi.output/kernel/{kernel_version}/{arch}/modules/usr/lib/modules/` and - `mkosi.output/kernel/{kernel_version}/{arch}/vmlinuz-*` exist, the build is skipped. -- Use `--force-kernel` (or `FORCE_KERNEL=1`) to force a rebuild. -- If modules exist but the vmlinuz is missing, the kernel is rebuilt - automatically. - -## Defconfigs +Important options: -Architecture-specific defconfigs live in the `kernel.configs/` directory, -named by stable branch: `{major}.{minor}.y.{arch}`. This allows -multiple kernel versions to coexist — each stable branch (e.g. 6.18.y, -6.19.y) has its own config per architecture. +- `--kernel-mode {docker,native,skip}` +- `--force-kernel` +- `--kernel-version` +- `--config` (interactive `menuconfig` flow; implies rebuild) -- `kernel.configs/6.18.y.amd64` — x86_64 config adapted for kernel 6.18. -- `kernel.configs/6.18.y.arm64` — arm64 config adapted for kernel 6.18. -- `kernel.configs/6.19.y.amd64` — x86_64 config adapted for kernel 6.19. -- `kernel.configs/6.19.y.arm64` — arm64 config adapted for kernel 6.19. +## Build outputs and handoff -The default kernel version is defined as `DEFAULT_KERNEL_VERSION` in -`captain/config.py` and can be overridden via `--kernel-version` or -the `KERNEL_VERSION` environment variable. +- Kernel build artifacts are staged under `mkosi.input/kernel//`. +- Flavor generation + mkosi consume the staged kernel/modules. +- Final published kernel artifact is copied to `out/vmlinuz--`. -Both configs include support for bare-metal provisioning, container -runtimes (cgroups v2, namespaces, overlayfs), and broad hardware/network -driver coverage. The local version suffix is set to `-captainos`. +For the end-to-end pipeline using kernels and tools together, see `README.md` (`captain build`). diff --git a/kernel.configs/6.18.y.amd64 b/kernel.configs/6.18.y.amd64 index c6d243cc..68c5b6fb 100644 --- a/kernel.configs/6.18.y.amd64 +++ b/kernel.configs/6.18.y.amd64 @@ -1,33 +1,3 @@ -# CaptainOS kernel defconfig for x86_64 -# Comprehensive config for bare-metal provisioning with container/network support. -# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.amd64 .config && make olddefconfig - -# --- Architecture --- -CONFIG_64BIT=y -CONFIG_X86_64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -# CONFIG_X86_EXTENDED_PLATFORM is not set -CONFIG_X86_INTEL_LPSS=y -CONFIG_HYPERVISOR_GUEST=y -CONFIG_PARAVIRT_SPINLOCKS=y -CONFIG_XEN=y -CONFIG_XEN_PVH=y -CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y -# CONFIG_X86_MCE is not set -CONFIG_X86_MSR=y -CONFIG_X86_CPUID=y -# CONFIG_X86_5LEVEL is not set -CONFIG_HZ_1000=y -CONFIG_PHYSICAL_ALIGN=0x1000000 -CONFIG_LEGACY_VSYSCALL_NONE=y -# CONFIG_MODIFY_LDT_SYSCALL is not set -CONFIG_IA32_EMULATION=y - -# --- General --- CONFIG_LOCALVERSION="-captainos" # CONFIG_LOCALVERSION_AUTO is not set CONFIG_DEFAULT_HOSTNAME="captainos" @@ -36,75 +6,61 @@ CONFIG_POSIX_MQUEUE=y CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y CONFIG_PREEMPT_VOLUNTARY=y CONFIG_BSD_PROCESS_ACCT=y CONFIG_BSD_PROCESS_ACCT_V3=y CONFIG_IKCONFIG=y CONFIG_IKCONFIG_PROC=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_EXPERT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_KPROBES=y -CONFIG_JUMP_LABEL=y -CONFIG_BINFMT_MISC=y - -# --- Cgroups --- -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y CONFIG_CGROUP_PIDS=y CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CPUSETS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y CONFIG_CGROUP_PERF=y CONFIG_CGROUP_BPF=y CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CPUSETS=y - -# --- Namespaces --- CONFIG_NAMESPACES=y CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y - -# --- Init / Initramfs --- +CONFIG_CHECKPOINT_RESTORE=y +CONFIG_SCHED_AUTOGROUP=y CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" # CONFIG_RD_BZIP2 is not set # CONFIG_RD_LZMA is not set -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y # CONFIG_RD_LZO is not set # CONFIG_RD_LZ4 is not set -CONFIG_RD_ZSTD=y - -# --- Memory management --- -CONFIG_SLAB_FREELIST_RANDOM=y -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_ZONE_DEVICE=y -CONFIG_HUGETLBFS=y - -# --- Power / Suspend --- +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_EXPERT=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_SMP=y +# CONFIG_X86_EXTENDED_PLATFORM is not set +CONFIG_X86_INTEL_LPSS=y +CONFIG_HYPERVISOR_GUEST=y +CONFIG_PARAVIRT_SPINLOCKS=y +CONFIG_XEN=y +CONFIG_XEN_PVH=y +CONFIG_NR_CPUS=128 +CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y +# CONFIG_X86_MCE is not set +CONFIG_X86_MSR=y +CONFIG_X86_CPUID=y +CONFIG_EFI=y +CONFIG_EFI_STUB=y +CONFIG_EFI_MIXED=y +CONFIG_HZ_1000=y +CONFIG_PHYSICAL_ALIGN=0x1000000 +CONFIG_LEGACY_VSYSCALL_NONE=y +# CONFIG_MODIFY_LDT_SYSCALL is not set # CONFIG_SUSPEND is not set - -# --- ACPI --- -CONFIG_ACPI=y # CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set CONFIG_ACPI_DOCK=y CONFIG_ACPI_IPMI=y @@ -113,8 +69,6 @@ CONFIG_ACPI_SBS=y CONFIG_ACPI_NFIT=y CONFIG_ACPI_APEI=y CONFIG_ACPI_APEI_GHES=y - -# --- CPU frequency --- CONFIG_CPU_FREQ_STAT=y CONFIG_CPU_FREQ_GOV_POWERSAVE=y CONFIG_CPU_FREQ_GOV_USERSPACE=y @@ -126,17 +80,33 @@ CONFIG_X86_POWERNOW_K8=y CONFIG_X86_P4_CLOCKMOD=y CONFIG_CPU_IDLE_GOV_LADDER=y CONFIG_INTEL_IDLE=y - -# --- KVM --- +CONFIG_IA32_EMULATION=y CONFIG_KVM=m CONFIG_KVM_INTEL=m CONFIG_KVM_AMD=m - -# --- Networking --- +CONFIG_KPROBES=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_MODULE_COMPRESS=y +CONFIG_MODULE_COMPRESS_ZSTD=y +CONFIG_BLK_DEV_INTEGRITY=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_BLK_CGROUP_IOLATENCY=y +CONFIG_PARTITION_ADVANCED=y +CONFIG_LDM_PARTITION=y +CONFIG_CMDLINE_PARTITION=y +CONFIG_BINFMT_MISC=y +CONFIG_SLAB_FREELIST_RANDOM=y +# CONFIG_COMPAT_BRK is not set +CONFIG_MEMORY_HOTPLUG=y +CONFIG_MEMORY_HOTREMOVE=y +CONFIG_KSM=y +CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 +CONFIG_TRANSPARENT_HUGEPAGE=y +CONFIG_ZONE_DEVICE=y CONFIG_NET=y CONFIG_PACKET=y CONFIG_PACKET_DIAG=y -CONFIG_UNIX=y CONFIG_UNIX_DIAG=y CONFIG_XFRM_USER=m CONFIG_XFRM_SUB_POLICY=y @@ -144,7 +114,6 @@ CONFIG_XFRM_STATISTICS=y CONFIG_NET_KEY=m CONFIG_NET_KEY_MIGRATE=y CONFIG_XDP_SOCKETS=y -CONFIG_INET=y CONFIG_IP_MULTICAST=y CONFIG_IP_ADVANCED_ROUTER=y CONFIG_IP_FIB_TRIE_STATS=y @@ -169,8 +138,6 @@ CONFIG_INET_ESP=m CONFIG_INET_IPCOMP=m CONFIG_INET_UDP_DIAG=y CONFIG_TCP_MD5SIG=y -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y CONFIG_IPV6_ROUTER_PREF=y CONFIG_INET6_AH=m CONFIG_INET6_ESP=m @@ -185,91 +152,7 @@ CONFIG_IPV6_MULTIPLE_TABLES=y CONFIG_IPV6_SUBTREES=y CONFIG_NETLABEL=y CONFIG_NETWORK_SECMARK=y -CONFIG_BRIDGE=y -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_VLAN_8021Q=y -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NETCONSOLE=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_NLMON=y -CONFIG_IP_SCTP=m -CONFIG_L2TP=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_OPENVSWITCH=m -CONFIG_VSOCKETS=m -CONFIG_VIRTIO_VSOCKETS=m -CONFIG_HYPERV_VSOCKETS=m -CONFIG_NETLINK_DIAG=y -CONFIG_MPLS_ROUTING=m -CONFIG_MPLS_IPTUNNEL=m -CONFIG_NET_SWITCHDEV=y -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y - -# --- Traffic control --- -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_ROUTE4=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=y -CONFIG_NET_EMATCH_NBYTE=y -CONFIG_NET_EMATCH_U32=y -CONFIG_NET_EMATCH_META=y -CONFIG_NET_EMATCH_TEXT=y -CONFIG_NET_EMATCH_IPSET=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_IPT=y -CONFIG_NET_ACT_NAT=y -CONFIG_NET_ACT_PEDIT=y -CONFIG_NET_ACT_SIMP=y -CONFIG_NET_ACT_SKBEDIT=y -CONFIG_NET_ACT_CSUM=y -CONFIG_NET_ACT_BPF=y - -# --- Netfilter --- CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y CONFIG_BRIDGE_NETFILTER=y CONFIG_NF_CONNTRACK=y CONFIG_NF_CONNTRACK_ZONES=y @@ -291,7 +174,6 @@ CONFIG_NF_CT_NETLINK=y CONFIG_NF_CT_NETLINK_TIMEOUT=y CONFIG_NF_CT_NETLINK_HELPER=y CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_NAT=y CONFIG_NF_TABLES=y CONFIG_NF_TABLES_INET=y CONFIG_NF_TABLES_NETDEV=y @@ -320,13 +202,11 @@ CONFIG_NETFILTER_XT_TARGET_HMARK=y CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y CONFIG_NETFILTER_XT_TARGET_LOG=y CONFIG_NETFILTER_XT_TARGET_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y CONFIG_NETFILTER_XT_TARGET_NFLOG=y CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y -CONFIG_NETFILTER_XT_TARGET_NOTRACK=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y CONFIG_NETFILTER_XT_TARGET_TEE=y CONFIG_NETFILTER_XT_TARGET_TPROXY=y -CONFIG_NETFILTER_XT_TARGET_TRACE=y CONFIG_NETFILTER_XT_TARGET_TCPMSS=y CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y @@ -414,20 +294,9 @@ CONFIG_IP_NF_MATCH_AH=y CONFIG_IP_NF_MATCH_ECN=y CONFIG_IP_NF_MATCH_RPFILTER=y CONFIG_IP_NF_MATCH_TTL=y -CONFIG_IP_NF_FILTER=y CONFIG_IP_NF_TARGET_REJECT=y CONFIG_IP_NF_TARGET_SYNPROXY=y -CONFIG_IP_NF_NAT=y -CONFIG_IP_NF_TARGET_MASQUERADE=y -CONFIG_IP_NF_TARGET_NETMAP=y -CONFIG_IP_NF_TARGET_REDIRECT=y -CONFIG_IP_NF_MANGLE=y CONFIG_IP_NF_TARGET_ECN=y -CONFIG_IP_NF_TARGET_TTL=y -CONFIG_IP_NF_RAW=y -CONFIG_IP_NF_SECURITY=y -CONFIG_IP_NF_ARPTABLES=y -CONFIG_IP_NF_ARPFILTER=y CONFIG_IP_NF_ARP_MANGLE=y CONFIG_NFT_DUP_IPV6=y CONFIG_IP6_NF_IPTABLES=y @@ -440,22 +309,12 @@ CONFIG_IP6_NF_MATCH_IPV6HEADER=y CONFIG_IP6_NF_MATCH_MH=y CONFIG_IP6_NF_MATCH_RPFILTER=y CONFIG_IP6_NF_MATCH_RT=y -CONFIG_IP6_NF_TARGET_HL=y -CONFIG_IP6_NF_FILTER=y CONFIG_IP6_NF_TARGET_REJECT=y CONFIG_IP6_NF_TARGET_SYNPROXY=y -CONFIG_IP6_NF_MANGLE=y -CONFIG_IP6_NF_RAW=y -CONFIG_IP6_NF_SECURITY=y -CONFIG_IP6_NF_NAT=y -CONFIG_IP6_NF_TARGET_MASQUERADE=y CONFIG_IP6_NF_TARGET_NPT=y CONFIG_NF_TABLES_BRIDGE=y CONFIG_NFT_BRIDGE_REJECT=y CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y CONFIG_BRIDGE_EBT_802_3=y CONFIG_BRIDGE_EBT_AMONG=y CONFIG_BRIDGE_EBT_ARP=y @@ -473,112 +332,215 @@ CONFIG_BRIDGE_EBT_REDIRECT=y CONFIG_BRIDGE_EBT_SNAT=y CONFIG_BRIDGE_EBT_LOG=y CONFIG_BRIDGE_EBT_NFLOG=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_INTEGRITY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_LDM_PARTITION=y -CONFIG_CMDLINE_PARTITION=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_DRBD=m -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_ATA_OVER_ETH=m -CONFIG_BLK_DEV_NVME=y - -# --- SCSI --- -CONFIG_SCSI=y -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=y -CONFIG_CHR_DEV_SG=y -CONFIG_ISCSI_TCP=m -CONFIG_SCSI_HPSA=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_SMARTPQI=m -CONFIG_VMWARE_PVSCSI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- ATA / SATA --- -CONFIG_ATA=y -# CONFIG_ATA_VERBOSE_ERROR is not set -# CONFIG_SATA_PMP is not set -CONFIG_SATA_AHCI=y -CONFIG_ATA_PIIX=y -CONFIG_SATA_MV=y -CONFIG_SATA_NV=y -CONFIG_SATA_PROMISE=y -CONFIG_SATA_SIL=y -CONFIG_SATA_SIS=y -CONFIG_SATA_SVW=y -CONFIG_SATA_ULI=y -CONFIG_SATA_VIA=y -CONFIG_SATA_VITESSE=y -CONFIG_ATA_GENERIC=y - -# --- Device mapper / MD --- -CONFIG_MD=y -CONFIG_BLK_DEV_MD=y -CONFIG_MD_LINEAR=y -CONFIG_MD_RAID0=y -CONFIG_MD_RAID1=y -CONFIG_MD_RAID10=y -CONFIG_MD_RAID456=y -CONFIG_MD_MULTIPATH=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_MULTIPATH=m -CONFIG_DM_MULTIPATH_QL=m -CONFIG_DM_MULTIPATH_ST=m - -# --- Fusion --- -CONFIG_FUSION=y -CONFIG_FUSION_SPI=y - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -# CONFIG_NET_VENDOR_3COM is not set -# CONFIG_NET_VENDOR_ADAPTEC is not set -# CONFIG_NET_VENDOR_AGERE is not set -# CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set -# CONFIG_NET_VENDOR_AMD is not set -# CONFIG_NET_VENDOR_AQUANTIA is not set -# CONFIG_NET_VENDOR_ARC is not set -# CONFIG_NET_VENDOR_ATHEROS is not set -# CONFIG_NET_VENDOR_CADENCE is not set -# CONFIG_NET_VENDOR_CAVIUM is not set -# CONFIG_NET_VENDOR_CHELSIO is not set -# CONFIG_NET_VENDOR_CORTINA is not set -# CONFIG_NET_VENDOR_DEC is not set -# CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set -# CONFIG_NET_VENDOR_EZCHIP is not set -# CONFIG_NET_VENDOR_HUAWEI is not set -# CONFIG_NET_VENDOR_I825XX is not set -# CONFIG_NET_VENDOR_MARVELL is not set -# CONFIG_NET_VENDOR_MICREL is not set -# CONFIG_NET_VENDOR_MICROCHIP is not set -# CONFIG_NET_VENDOR_MYRI is not set +CONFIG_IP_SCTP=m +CONFIG_L2TP=m +CONFIG_BRIDGE=y +CONFIG_BRIDGE_VLAN_FILTERING=y +CONFIG_VLAN_8021Q=y +CONFIG_NET_SCHED=y +CONFIG_NET_SCH_HTB=m +CONFIG_NET_SCH_HFSC=m +CONFIG_NET_SCH_PRIO=m +CONFIG_NET_SCH_MULTIQ=m +CONFIG_NET_SCH_RED=m +CONFIG_NET_SCH_SFB=m +CONFIG_NET_SCH_SFQ=m +CONFIG_NET_SCH_TEQL=m +CONFIG_NET_SCH_TBF=m +CONFIG_NET_SCH_GRED=m +CONFIG_NET_SCH_NETEM=m +CONFIG_NET_SCH_DRR=m +CONFIG_NET_SCH_MQPRIO=m +CONFIG_NET_SCH_CHOKE=m +CONFIG_NET_SCH_QFQ=m +CONFIG_NET_SCH_INGRESS=m +CONFIG_NET_CLS_BASIC=y +CONFIG_NET_CLS_ROUTE4=y +CONFIG_NET_CLS_FW=y +CONFIG_NET_CLS_U32=y +CONFIG_CLS_U32_PERF=y +CONFIG_CLS_U32_MARK=y +CONFIG_NET_CLS_FLOW=y +CONFIG_NET_CLS_CGROUP=y +CONFIG_NET_CLS_BPF=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_NBYTE=y +CONFIG_NET_EMATCH_U32=y +CONFIG_NET_EMATCH_META=y +CONFIG_NET_EMATCH_TEXT=y +CONFIG_NET_EMATCH_IPSET=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_POLICE=y +CONFIG_NET_ACT_GACT=y +CONFIG_GACT_PROB=y +CONFIG_NET_ACT_MIRRED=y +CONFIG_NET_ACT_NAT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_SIMP=y +CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_CSUM=y +CONFIG_NET_ACT_BPF=y +CONFIG_OPENVSWITCH=m +CONFIG_VSOCKETS=m +CONFIG_VIRTIO_VSOCKETS=m +CONFIG_HYPERV_VSOCKETS=m +CONFIG_NETLINK_DIAG=y +CONFIG_MPLS_ROUTING=m +CONFIG_MPLS_IPTUNNEL=m +CONFIG_NET_SWITCHDEV=y +CONFIG_CGROUP_NET_PRIO=y +# CONFIG_WIRELESS is not set +CONFIG_NET_9P=y +CONFIG_NET_9P_VIRTIO=y +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_HOTPLUG_PCI_PCIE=y +CONFIG_PCI_STUB=y +CONFIG_PCI_IOV=y +# CONFIG_VGA_ARB is not set +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +CONFIG_HOTPLUG_PCI_SHPC=y +CONFIG_PCI_HYPERV_INTERFACE=m +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_DMI_SYSFS=y +CONFIG_SYSFB_SIMPLEFB=y +CONFIG_RESET_ATTACK_MITIGATION=y +# CONFIG_PNP_DEBUG_MESSAGES is not set +CONFIG_BLK_DEV_LOOP=y +CONFIG_BLK_DEV_DRBD=m +CONFIG_BLK_DEV_NBD=y +CONFIG_BLK_DEV_RAM=y +CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_ATA_OVER_ETH=m +CONFIG_VIRTIO_BLK=y +CONFIG_BLK_DEV_NVME=y +CONFIG_HP_ILO=m +# CONFIG_SCSI_PROC_FS is not set +CONFIG_BLK_DEV_SD=y +CONFIG_BLK_DEV_SR=y +CONFIG_CHR_DEV_SG=y +CONFIG_ISCSI_TCP=m +CONFIG_SCSI_HPSA=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m +CONFIG_SCSI_SMARTPQI=m +CONFIG_VMWARE_PVSCSI=y +CONFIG_XEN_SCSI_FRONTEND=y +CONFIG_SCSI_VIRTIO=y +CONFIG_ATA=y +# CONFIG_ATA_VERBOSE_ERROR is not set +# CONFIG_SATA_PMP is not set +CONFIG_SATA_AHCI=y +CONFIG_ATA_PIIX=y +CONFIG_SATA_MV=y +CONFIG_SATA_NV=y +CONFIG_SATA_PROMISE=y +CONFIG_SATA_SIL=y +CONFIG_SATA_SIS=y +CONFIG_SATA_SVW=y +CONFIG_SATA_ULI=y +CONFIG_SATA_VIA=y +CONFIG_SATA_VITESSE=y +CONFIG_ATA_GENERIC=y +CONFIG_MD=y +CONFIG_BLK_DEV_MD=y +CONFIG_MD_LINEAR=y +CONFIG_MD_RAID0=y +CONFIG_MD_RAID1=y +CONFIG_MD_RAID10=y +CONFIG_MD_RAID456=y +CONFIG_BLK_DEV_DM=y +CONFIG_DM_CRYPT=y +CONFIG_DM_SNAPSHOT=y +CONFIG_DM_THIN_PROVISIONING=y +CONFIG_DM_MULTIPATH=m +CONFIG_DM_MULTIPATH_QL=m +CONFIG_DM_MULTIPATH_ST=m +CONFIG_FUSION=y +CONFIG_FUSION_SPI=y +CONFIG_NETDEVICES=y +CONFIG_BONDING=m +CONFIG_DUMMY=m +CONFIG_NET_TEAM=m +CONFIG_NET_TEAM_MODE_BROADCAST=m +CONFIG_NET_TEAM_MODE_ROUNDROBIN=m +CONFIG_NET_TEAM_MODE_RANDOM=m +CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m +CONFIG_NET_TEAM_MODE_LOADBALANCE=m +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_NETCONSOLE=y +CONFIG_TUN=y +CONFIG_VETH=y +CONFIG_VIRTIO_NET=y +CONFIG_NLMON=y +# CONFIG_NET_VENDOR_3COM is not set +# CONFIG_NET_VENDOR_ADAPTEC is not set +# CONFIG_NET_VENDOR_AGERE is not set +# CONFIG_NET_VENDOR_ALACRITECH is not set +# CONFIG_NET_VENDOR_ALTEON is not set +CONFIG_ENA_ETHERNET=m +# CONFIG_NET_VENDOR_AMD is not set +# CONFIG_NET_VENDOR_AQUANTIA is not set +# CONFIG_NET_VENDOR_ARC is not set +# CONFIG_NET_VENDOR_ATHEROS is not set +CONFIG_CNIC=m +CONFIG_TIGON3=y +CONFIG_BNX2X=y +CONFIG_BNXT=y +# CONFIG_NET_VENDOR_CADENCE is not set +# CONFIG_NET_VENDOR_CAVIUM is not set +# CONFIG_NET_VENDOR_CHELSIO is not set +# CONFIG_NET_VENDOR_CORTINA is not set +# CONFIG_NET_VENDOR_DEC is not set +# CONFIG_NET_VENDOR_DLINK is not set +CONFIG_BE2NET=y +# CONFIG_NET_VENDOR_EZCHIP is not set +CONFIG_GVE=m +# CONFIG_NET_VENDOR_HUAWEI is not set +# CONFIG_NET_VENDOR_I825XX is not set +CONFIG_E1000=y +CONFIG_E1000E=y +CONFIG_IGB=y +CONFIG_IGBVF=y +CONFIG_IXGBE=y +CONFIG_IXGBEVF=y +CONFIG_I40E=y +CONFIG_I40EVF=y +CONFIG_ICE=y +CONFIG_IGC=y +# CONFIG_NET_VENDOR_MARVELL is not set +CONFIG_MLX4_EN=y +CONFIG_MLX5_CORE=m +CONFIG_MLX5_CORE_EN=y +# CONFIG_NET_VENDOR_MICREL is not set +# CONFIG_NET_VENDOR_MICROCHIP is not set +# CONFIG_NET_VENDOR_MYRI is not set # CONFIG_NET_VENDOR_NI is not set # CONFIG_NET_VENDOR_NATSEMI is not set # CONFIG_NET_VENDOR_NETERION is not set +CONFIG_NFP=m # CONFIG_NET_VENDOR_NVIDIA is not set # CONFIG_NET_VENDOR_OKI is not set # CONFIG_NET_VENDOR_PACKET_ENGINES is not set +CONFIG_IONIC=y # CONFIG_NET_VENDOR_QLOGIC is not set # CONFIG_NET_VENDOR_BROCADE is not set # CONFIG_NET_VENDOR_QUALCOMM is not set # CONFIG_NET_VENDOR_RDC is not set +CONFIG_8139CP=y +CONFIG_8139TOO=y +CONFIG_R8169=y # CONFIG_NET_VENDOR_RENESAS is not set # CONFIG_NET_VENDOR_ROCKER is not set # CONFIG_NET_VENDOR_SAMSUNG is not set @@ -594,35 +556,17 @@ CONFIG_NETDEVICES=y # CONFIG_NET_VENDOR_TI is not set # CONFIG_NET_VENDOR_VIA is not set # CONFIG_NET_VENDOR_WIZNET is not set -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IGBVF=y -CONFIG_IXGBE=y -CONFIG_IXGBEVF=y -CONFIG_I40E=y -CONFIG_I40EVF=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_CNIC=m -CONFIG_TIGON3=y -CONFIG_BNX2X=y -CONFIG_R8169=y -CONFIG_8139CP=y -CONFIG_8139TOO=y -CONFIG_VMXNET3=y -CONFIG_HYPERV_NET=y -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_NFP=m -CONFIG_IONIC=y -CONFIG_VIRTIO_NET=y - -# --- USB network --- +CONFIG_PPP=m +CONFIG_PPP_BSDCOMP=m +CONFIG_PPP_DEFLATE=m +CONFIG_PPP_FILTER=y +CONFIG_PPP_MPPE=m +CONFIG_PPP_MULTILINK=y +CONFIG_PPPOE=m +CONFIG_PPTP=m +CONFIG_PPPOL2TP=m +CONFIG_PPP_ASYNC=m +CONFIG_PPP_SYNC_TTY=m CONFIG_USB_CATC=m CONFIG_USB_KAWETH=m CONFIG_USB_PEGASUS=m @@ -637,37 +581,72 @@ CONFIG_USB_NET_CDC_NCM=m CONFIG_USB_NET_NET1080=m CONFIG_USB_NET_CDC_SUBSET=m CONFIG_USB_NET_ZAURUS=m - -# --- WAN --- +# CONFIG_WLAN is not set CONFIG_WAN=y CONFIG_HDLC=m CONFIG_HDLC_CISCO=m - -# --- PPP --- -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m - -# --- USB --- -CONFIG_USB_SUPPORT=y +CONFIG_VMXNET3=y +CONFIG_HYPERV_NET=y +CONFIG_INPUT_FF_MEMLESS=y +CONFIG_INPUT_SPARSEKMAP=y +CONFIG_INPUT_MOUSEDEV=y +CONFIG_INPUT_MOUSEDEV_PSAUX=y +CONFIG_INPUT_EVDEV=y +# CONFIG_INPUT_MOUSE is not set +CONFIG_INPUT_MISC=y +CONFIG_INPUT_PCSPKR=y +CONFIG_INPUT_ATLAS_BTNS=y +CONFIG_INPUT_UINPUT=y +CONFIG_SERIO_PCIPS2=y +CONFIG_SERIO_RAW=y +# CONFIG_LEGACY_PTYS is not set +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_8250_NR_UARTS=32 +# CONFIG_SERIAL_8250_MID is not set +CONFIG_SERIAL_DEV_BUS=y +CONFIG_TTY_PRINTK=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_IPMI_HANDLER=y +CONFIG_IPMI_DEVICE_INTERFACE=y +CONFIG_IPMI_SI=y +CONFIG_IPMI_POWEROFF=y +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y +CONFIG_HW_RANDOM_VIRTIO=y +CONFIG_NVRAM=y +CONFIG_HPET=y +CONFIG_HANGCHECK_TIMER=y +CONFIG_TCG_TIS_I2C_ATMEL=m +CONFIG_TCG_TIS_I2C_INFINEON=m +CONFIG_TCG_TIS_I2C_NUVOTON=m +CONFIG_TCG_NSC=m +CONFIG_TCG_ATMEL=m +CONFIG_TCG_INFINEON=m +CONFIG_TCG_XEN=m +CONFIG_TCG_VTPM_PROXY=m +CONFIG_TCG_TIS_ST33ZP24_I2C=m +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MUX=y +# CONFIG_THERMAL_HWMON is not set +CONFIG_LPC_ICH=y +CONFIG_LPC_SCH=y +CONFIG_MFD_INTEL_LPSS_ACPI=y +CONFIG_MFD_INTEL_LPSS_PCI=y +CONFIG_MFD_SM501=y +CONFIG_MFD_VX855=y +CONFIG_DRM=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_DRM_I915=y +CONFIG_FB=y +CONFIG_FB_EFI=y CONFIG_USB=y CONFIG_USB_XHCI_HCD=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_OHCI_HCD=y CONFIG_USB_UHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y CONFIG_USB_STORAGE=y - -# --- USB serial (BMC/console access) --- +CONFIG_USB_UAS=y CONFIG_USB_SERIAL=y CONFIG_USB_SERIAL_CONSOLE=y CONFIG_USB_SERIAL_GENERIC=y @@ -677,33 +656,39 @@ CONFIG_USB_SERIAL_FTDI_SIO=y CONFIG_USB_SERIAL_PL2303=y CONFIG_USB_SERIAL_TI=y CONFIG_USB_SERIAL_OPTION=y - -# --- Input --- -CONFIG_INPUT_FF_MEMLESS=y -CONFIG_INPUT_SPARSEKMAP=y -CONFIG_INPUT_MOUSEDEV=y -CONFIG_INPUT_MOUSEDEV_PSAUX=y -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_MOUSE is not set -CONFIG_INPUT_MISC=y -CONFIG_INPUT_PCSPKR=y -CONFIG_INPUT_ATLAS_BTNS=y -CONFIG_INPUT_UINPUT=y -CONFIG_SERIO_PCIPS2=y -CONFIG_SERIO_RAW=y -# CONFIG_LEGACY_PTYS is not set -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set - -# --- MMC --- CONFIG_MMC=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PCI=y # CONFIG_MMC_RICOH_MMC is not set CONFIG_MMC_SDHCI_ACPI=y CONFIG_MMC_SDHCI_PLTFM=y - -# --- Filesystems --- +CONFIG_RTC_CLASS=y +CONFIG_DMADEVICES=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_VHOST_NET=m +CONFIG_VHOST_VSOCK=m +CONFIG_HYPERV=y +CONFIG_HYPERV_UTILS=y +CONFIG_HYPERV_BALLOON=y +# CONFIG_XEN_BACKEND is not set +CONFIG_XEN_GNTDEV=y +CONFIG_XEN_GRANT_DEV_ALLOC=y +CONFIG_XEN_PVCALLS_FRONTEND=y +CONFIG_XEN_ACPI_PROCESSOR=y +# CONFIG_XEN_SYMS is not set +CONFIG_INTEL_IPS=y +CONFIG_AMD_IOMMU=y +CONFIG_INTEL_IOMMU=y +CONFIG_IRQ_REMAP=y +CONFIG_PM_DEVFREQ=y +CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y +CONFIG_RESET_CONTROLLER=y +CONFIG_DEV_DAX=y +CONFIG_DEV_DAX_PMEM=m CONFIG_EXT4_FS=y CONFIG_EXT4_FS_POSIX_ACL=y CONFIG_EXT4_FS_SECURITY=y @@ -717,34 +702,29 @@ CONFIG_FS_ENCRYPTION=y CONFIG_FANOTIFY=y CONFIG_QUOTA=y CONFIG_QUOTA_NETLINK_INTERFACE=y -CONFIG_VFAT_FS=y +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_OVERLAY_FS=y +CONFIG_FSCACHE_STATS=y +CONFIG_CACHEFILES=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_ZISOFS=y +CONFIG_UDF_FS=y CONFIG_MSDOS_FS=y +CONFIG_VFAT_FS=y CONFIG_FAT_DEFAULT_IOCHARSET="utf8" CONFIG_NTFS_FS=m -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_TMPFS_XATTR=y -CONFIG_PROC_FS=y CONFIG_PROC_KCORE=y -CONFIG_SYSFS=y +CONFIG_TMPFS_POSIX_ACL=y +CONFIG_HUGETLBFS=y CONFIG_EFIVAR_FS=y -CONFIG_OVERLAY_FS=y CONFIG_SQUASHFS=y CONFIG_SQUASHFS_XATTR=y CONFIG_SQUASHFS_LZ4=y CONFIG_SQUASHFS_LZO=y CONFIG_SQUASHFS_XZ=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=y -CONFIG_FSCACHE=y -CONFIG_FSCACHE_STATS=y -CONFIG_CACHEFILES=y CONFIG_NFS_FS=m -# CONFIG_NFS_V2 is not set CONFIG_NFS_V4=m CONFIG_NFS_V4_1=y CONFIG_NFS_V4_2=y @@ -763,97 +743,17 @@ CONFIG_9P_FS=y CONFIG_9P_FSCACHE=y CONFIG_9P_FS_POSIX_ACL=y CONFIG_9P_FS_SECURITY=y - -# --- NLS --- CONFIG_NLS_CODEPAGE_437=y CONFIG_NLS_ASCII=y CONFIG_NLS_ISO8859_1=y CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Console / Serial --- -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=32 -# CONFIG_SERIAL_8250_MID is not set -CONFIG_SERIAL_DEV_BUS=y -CONFIG_TTY_PRINTK=y -CONFIG_HW_CONSOLE=y -CONFIG_VGA_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- IPMI --- -CONFIG_IPMI_HANDLER=y -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=y -CONFIG_IPMI_SI=y -CONFIG_IPMI_POWEROFF=y - -# --- Hardware RNG --- -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y - -# --- I2C --- -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y - -# --- TPM --- -CONFIG_TCG_TIS_I2C_ATMEL=m -CONFIG_TCG_TIS_I2C_INFINEON=m -CONFIG_TCG_TIS_I2C_NUVOTON=m -CONFIG_TCG_NSC=m -CONFIG_TCG_ATMEL=m -CONFIG_TCG_INFINEON=m -CONFIG_TCG_XEN=m -CONFIG_TCG_VTPM_PROXY=m -CONFIG_TCG_TIS_ST33ZP24_I2C=m - -# --- Platform / MFD --- -CONFIG_NVRAM=y -CONFIG_HPET=y -CONFIG_HANGCHECK_TIMER=y -CONFIG_LPC_ICH=y -CONFIG_LPC_SCH=y -CONFIG_MFD_INTEL_LPSS_ACPI=y -CONFIG_MFD_INTEL_LPSS_PCI=y -CONFIG_MFD_SM501=y -CONFIG_MFD_VX855=y -# CONFIG_THERMAL_HWMON is not set - -# --- RTC --- -CONFIG_RTC_CLASS=y - -# --- DMA --- -CONFIG_DMADEVICES=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_SECURITY_NETWORK=y -CONFIG_SECURITY_NETWORK_XFRM=y -CONFIG_SECURITY_PATH=y -CONFIG_SECURITY_DMESG_RESTRICT=y -CONFIG_KEYS=y CONFIG_PERSISTENT_KEYRINGS=y CONFIG_TRUSTED_KEYS=y CONFIG_KEY_DH_OPERATIONS=y -CONFIG_HARDENED_USERCOPY=y -CONFIG_FORTIFY_SOURCE=y +CONFIG_SECURITY_DMESG_RESTRICT=y +CONFIG_SECURITY=y +CONFIG_SECURITY_NETWORK=y +CONFIG_SECURITY_NETWORK_XFRM=y CONFIG_STATIC_USERMODEHELPER=y CONFIG_SECURITY_YAMA=y CONFIG_INTEGRITY_SIGNATURE=y @@ -864,14 +764,10 @@ CONFIG_IMA_READ_POLICY=y CONFIG_IMA_APPRAISE=y CONFIG_EVM=y CONFIG_LSM="yama,loadpin,safesetid,integrity" - -# --- Crypto --- -CONFIG_CRYPTO=y +CONFIG_FORTIFY_SOURCE=y +CONFIG_HARDENED_USERCOPY=y +CONFIG_BUG_ON_DATA_CORRUPTION=y CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y CONFIG_CRYPTO_ANUBIS=y CONFIG_CRYPTO_BLOWFISH=y CONFIG_CRYPTO_CAMELLIA=y @@ -882,7 +778,6 @@ CONFIG_CRYPTO_SEED=y CONFIG_CRYPTO_TEA=y CONFIG_CRYPTO_TWOFISH=y CONFIG_CRYPTO_ARC4=y -CONFIG_CRYPTO_KEYWRAP=y CONFIG_CRYPTO_LRW=y CONFIG_CRYPTO_PCBC=y CONFIG_CRYPTO_CHACHA20POLY1305=y @@ -890,7 +785,6 @@ CONFIG_CRYPTO_SEQIV=y CONFIG_CRYPTO_ECHAINIV=y CONFIG_CRYPTO_MICHAEL_MIC=y CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_VMAC=y CONFIG_CRYPTO_WP512=y CONFIG_CRYPTO_XCBC=y CONFIG_CRYPTO_CRC32=y @@ -903,8 +797,6 @@ CONFIG_CRYPTO_USER_API_HASH=y CONFIG_CRYPTO_USER_API_SKCIPHER=y CONFIG_CRYPTO_USER_API_RNG=y CONFIG_CRYPTO_USER_API_AEAD=y - -# --- x86_64 hardware crypto acceleration --- CONFIG_CRYPTO_AES_NI_INTEL=y CONFIG_CRYPTO_BLOWFISH_X86_64=y CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y @@ -914,119 +806,20 @@ CONFIG_CRYPTO_DES3_EDE_X86_64=y CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y -CONFIG_CRYPTO_CHACHA20_X86_64=y -CONFIG_CRYPTO_POLY1305_X86_64=y -CONFIG_CRYPTO_SHA1_SSSE3=y -CONFIG_CRYPTO_SHA256_SSSE3=y -CONFIG_CRYPTO_SHA512_SSSE3=y CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y -CONFIG_CRYPTO_CRC32C_INTEL=y -CONFIG_CRYPTO_CRC32_PCLMUL=y CONFIG_CRYPTO_DEV_PADLOCK=y CONFIG_CRYPTO_DEV_PADLOCK_AES=y CONFIG_CRYPTO_DEV_PADLOCK_SHA=y CONFIG_CRYPTO_DEV_VIRTIO=m CONFIG_PKCS7_MESSAGE_PARSER=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y - -# --- PCI --- -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI_PCIE=y -CONFIG_PCI_STUB=y -CONFIG_PCI_IOV=y -# CONFIG_VGA_ARB is not set -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_HOTPLUG_PCI_SHPC=y -CONFIG_PCI_HYPERV_INTERFACE=m - -# --- IOMMU --- -CONFIG_AMD_IOMMU=y -CONFIG_INTEL_IOMMU=y -CONFIG_IRQ_REMAP=y - -# --- Xen --- -# CONFIG_XEN_BACKEND is not set -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_XEN_PVCALLS_FRONTEND=y -CONFIG_XEN_ACPI_PROCESSOR=y -# CONFIG_XEN_SYMS is not set - -# --- Intel platform --- -CONFIG_INTEL_IPS=y - -# --- DEVFREQ --- -CONFIG_PM_DEVFREQ=y -CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y - -# --- Reset --- -CONFIG_RESET_CONTROLLER=y - -# --- DAX --- -CONFIG_DEV_DAX=y -CONFIG_DEV_DAX_PMEM=m - -# --- Misc --- -CONFIG_FW_LOADER=y -CONFIG_PRINTK=y CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_EARLY_PRINTK=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_RESET_ATTACK_MITIGATION=y -# CONFIG_PNP_DEBUG_MESSAGES is not set -CONFIG_HYPERV=y -CONFIG_HYPERV_UTILS=y -CONFIG_HYPERV_BALLOON=y - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- EFI boot support --- -CONFIG_EFI=y -CONFIG_EFI_STUB=y -CONFIG_EFI_MIXED=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_I915=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y - -# --- Debugging / Diagnostics --- CONFIG_FRAME_WARN=1024 -CONFIG_BUG_ON_DATA_CORRUPTION=y +CONFIG_MAGIC_SYSRQ=y +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_TIMEOUT=10 CONFIG_HARDLOCKUP_DETECTOR=y CONFIG_WQ_WATCHDOG=y CONFIG_DEBUG_NOTIFIERS=y CONFIG_RCU_CPU_STALL_TIMEOUT=60 # CONFIG_RCU_TRACE is not set CONFIG_IO_STRICT_DEVMEM=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set diff --git a/kernel.configs/6.18.y.arm64 b/kernel.configs/6.18.y.arm64 index 795c91e6..d5291bc5 100644 --- a/kernel.configs/6.18.y.arm64 +++ b/kernel.configs/6.18.y.arm64 @@ -1,25 +1,3 @@ -# CaptainOS kernel defconfig for arm64 (aarch64) -# Minimal config for bare-metal provisioning with container/network support. -# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig - -# --- Architecture --- -CONFIG_ARM64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -CONFIG_NUMA=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_SCHED_MC=y -CONFIG_SCHED_SMT=y -CONFIG_HZ_1000=y -CONFIG_XEN=y -CONFIG_COMPAT=y -CONFIG_RANDOMIZE_BASE=y -CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y - -# --- General --- CONFIG_LOCALVERSION="-captainos" CONFIG_DEFAULT_HOSTNAME="captainos" CONFIG_SYSVIPC=y @@ -27,52 +5,76 @@ CONFIG_POSIX_MQUEUE=y CONFIG_AUDIT=y CONFIG_NO_HZ_IDLE=y CONFIG_HIGH_RES_TIMERS=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_JIT=y +CONFIG_BPF_JIT_ALWAYS_ON=y CONFIG_PREEMPT_VOLUNTARY=y CONFIG_BSD_PROCESS_ACCT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y +CONFIG_MEMCG=y +CONFIG_BLK_CGROUP=y +CONFIG_CGROUP_SCHED=y +CONFIG_CFS_BANDWIDTH=y +CONFIG_RT_GROUP_SCHED=y CONFIG_CGROUP_PIDS=y +CONFIG_CGROUP_RDMA=y +CONFIG_CGROUP_FREEZER=y +CONFIG_CGROUP_HUGETLB=y +CONFIG_CPUSETS=y CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y CONFIG_CGROUP_BPF=y CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_RDMA=y -CONFIG_CPUSETS=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_NAMESPACES=y CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y CONFIG_CHECKPOINT_RESTORE=y -# Memory management -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_KSM=y +CONFIG_BLK_DEV_INITRD=y +CONFIG_CC_OPTIMIZE_FOR_SIZE=y +CONFIG_KEXEC=y +CONFIG_KEXEC_FILE=y +CONFIG_ARCH_SUNXI=y +CONFIG_ARCH_EXYNOS=y +CONFIG_ARCH_HISI=y +CONFIG_ARCH_MESON=y +CONFIG_ARCH_MVEBU=y +CONFIG_ARCH_QCOM=y +CONFIG_ARCH_ROCKCHIP=y +CONFIG_ARCH_SEATTLE=y +CONFIG_ARCH_SYNQUACER=y +CONFIG_ARCH_TEGRA=y +CONFIG_ARCH_THUNDER=y +CONFIG_ARCH_THUNDER2=y +CONFIG_ARCH_UNIPHIER=y +CONFIG_ARCH_VEXPRESS=y +CONFIG_ARCH_XGENE=y +CONFIG_ARM64_VA_BITS_48=y +CONFIG_NR_CPUS=128 +CONFIG_NUMA=y +CONFIG_HZ_1000=y +CONFIG_XEN=y +CONFIG_COMPAT=y +CONFIG_RANDOMIZE_BASE=y +CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y +CONFIG_ACPI=y +CONFIG_ACPI_DOCK=y +CONFIG_ACPI_IPMI=m +CONFIG_ACPI_APEI=y +CONFIG_ACPI_APEI_GHES=y +CONFIG_VIRTUALIZATION=y +CONFIG_KVM=y +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_MODULE_COMPRESS=y +CONFIG_MODULE_COMPRESS_ZSTD=y +CONFIG_BLK_DEV_THROTTLING=y +CONFIG_BLK_CGROUP_IOLATENCY=y CONFIG_MEMORY_HOTPLUG=y CONFIG_MEMORY_HOTREMOVE=y -CONFIG_HUGETLBFS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" - -# --- Networking --- +CONFIG_KSM=y +CONFIG_TRANSPARENT_HUGEPAGE=y CONFIG_NET=y CONFIG_PACKET=y CONFIG_UNIX=y +CONFIG_XDP_SOCKETS=y CONFIG_INET=y CONFIG_IP_MULTICAST=y CONFIG_IP_ADVANCED_ROUTER=y @@ -85,21 +87,49 @@ CONFIG_NET_IPIP=m CONFIG_NET_IPGRE_DEMUX=m CONFIG_NET_IPGRE=m CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y +CONFIG_NETFILTER=y +CONFIG_BRIDGE_NETFILTER=y +CONFIG_NF_CONNTRACK=y +CONFIG_NF_TABLES=y +CONFIG_NF_TABLES_INET=y +CONFIG_NFT_CT=y +CONFIG_NFT_MASQ=y +CONFIG_NFT_NAT=y +CONFIG_NFT_COMPAT=y +CONFIG_NETFILTER_XT_SET=y +CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y +CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y +CONFIG_NETFILTER_XT_MATCH_COMMENT=y +CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y +CONFIG_NETFILTER_XT_MATCH_MARK=y +CONFIG_IP_SET=y +CONFIG_IP_SET_BITMAP_IP=y +CONFIG_IP_SET_BITMAP_PORT=y +CONFIG_IP_SET_HASH_IP=y +CONFIG_IP_SET_HASH_IPPORT=y +CONFIG_IP_SET_HASH_IPPORTNET=y +CONFIG_IP_SET_HASH_NET=y +CONFIG_IP_SET_HASH_NETPORT=y +CONFIG_IP_SET_LIST_SET=y +CONFIG_IP_VS=y +CONFIG_IP_VS_IPV6=y +CONFIG_IP_VS_PROTO_TCP=y +CONFIG_IP_VS_PROTO_UDP=y +CONFIG_IP_VS_RR=y +CONFIG_IP_VS_WRR=y +CONFIG_IP_VS_SH=y +CONFIG_IP_VS_NFCT=y +CONFIG_IP_NF_IPTABLES=y +CONFIG_IP6_NF_IPTABLES=y +CONFIG_BRIDGE_NF_EBTABLES=y +CONFIG_BRIDGE_EBT_ARP=y +CONFIG_BRIDGE_EBT_IP=y +CONFIG_BRIDGE_EBT_IP6=y +CONFIG_BRIDGE_EBT_MARK=y +CONFIG_BRIDGE_EBT_VLAN=y +CONFIG_BRIDGE_EBT_MARK_T=y CONFIG_BRIDGE=y CONFIG_VLAN_8021Q=m -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NLMON=y -# Traffic control / QoS (needed by CNI plugins) CONFIG_NET_SCHED=y CONFIG_NET_SCH_HTB=m CONFIG_NET_SCH_PRIO=m @@ -113,165 +143,152 @@ CONFIG_NET_CLS_U32=y CONFIG_NET_CLS_CGROUP=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_U32=y CONFIG_NET_CLS_ACT=y CONFIG_NET_ACT_POLICE=y CONFIG_NET_ACT_GACT=y CONFIG_NET_ACT_MIRRED=y CONFIG_NET_ACT_BPF=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_U32=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NFT_NAT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_CT=y -CONFIG_NFT_COMPAT=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_NAT=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_NAT=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_VLAN=y -# IPVS (container load balancing / kube-proxy) -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_NFCT=y -# IP sets (used by iptables/nftables for efficient matching) -CONFIG_IP_SET=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_NETFILTER_XT_SET=y - -# --- Block devices --- -CONFIG_BLK_DEV=y +CONFIG_CGROUP_NET_PRIO=y +# CONFIG_WIRELESS is not set +CONFIG_PCI=y +CONFIG_PCIEPORTBUS=y +CONFIG_PCI_IOV=y +CONFIG_PCI_PASID=y +CONFIG_HOTPLUG_PCI=y +CONFIG_HOTPLUG_PCI_ACPI=y +CONFIG_PCI_AARDVARK=y +CONFIG_PCIE_ALTERA=y +CONFIG_PCI_HOST_THUNDER_PEM=y +CONFIG_PCI_HOST_THUNDER_ECAM=y +CONFIG_PCI_HOST_GENERIC=y +CONFIG_PCI_TEGRA=y +CONFIG_PCIE_ROCKCHIP_HOST=y +CONFIG_PCI_XGENE=y +CONFIG_PCIE_AL=y +CONFIG_PCI_MESON=y +CONFIG_PCI_HISI=y +CONFIG_PCIE_KIRIN=y +CONFIG_PCIE_HISI_STB=y +CONFIG_PCIE_ARMADA_8K=y +CONFIG_PCIE_QCOM=y +CONFIG_UEVENT_HELPER=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_CONNECTOR=y +CONFIG_ARM_SCMI_PROTOCOL=y +CONFIG_DMI_SYSFS=y +CONFIG_SYSFB_SIMPLEFB=y CONFIG_BLK_DEV_LOOP=y CONFIG_BLK_DEV_NBD=y CONFIG_BLK_DEV_RAM=y CONFIG_BLK_DEV_RAM_SIZE=65536 +CONFIG_VIRTIO_BLK=y CONFIG_BLK_DEV_NVME=y -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y - -# --- SCSI --- -CONFIG_SCSI=y +CONFIG_HP_ILO=m CONFIG_BLK_DEV_SD=y CONFIG_CHR_DEV_SG=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m CONFIG_SCSI_HPSA=y CONFIG_SCSI_HISI_SAS=y CONFIG_SCSI_HISI_SAS_PCI=y +CONFIG_MEGARAID_SAS=y +CONFIG_SCSI_MPT3SAS=y +CONFIG_SCSI_MPI3MR=m CONFIG_XEN_SCSI_FRONTEND=y CONFIG_SCSI_VIRTIO=y - -# --- Device mapper --- +CONFIG_ATA=y +CONFIG_SATA_AHCI=y +CONFIG_SATA_AHCI_PLATFORM=y CONFIG_MD=y CONFIG_BLK_DEV_DM=y CONFIG_DM_CRYPT=y -CONFIG_DM_THIN_PROVISIONING=y CONFIG_DM_SNAPSHOT=y +CONFIG_DM_THIN_PROVISIONING=y CONFIG_DM_MULTIPATH=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m - -# --- Network drivers (built-in for reliable boot) --- CONFIG_NETDEVICES=y +CONFIG_BONDING=m +CONFIG_DUMMY=m +CONFIG_MACVLAN=y +CONFIG_MACVTAP=y +CONFIG_IPVLAN=y +CONFIG_VXLAN=y +CONFIG_GENEVE=m +CONFIG_TUN=y +CONFIG_VETH=y CONFIG_VIRTIO_NET=y +CONFIG_NLMON=y +CONFIG_ENA_ETHERNET=m +CONFIG_AMD_XGBE=y +CONFIG_NET_XGENE=y +CONFIG_BCMGENET=y +CONFIG_TIGON3=y +CONFIG_BNXT=y +CONFIG_MACB=m +CONFIG_THUNDER_NIC_PF=m +CONFIG_THUNDER_NIC_VF=m +CONFIG_GVE=m +CONFIG_HNS3=m CONFIG_E1000E=y CONFIG_IGB=y CONFIG_IXGBE=y CONFIG_I40E=y CONFIG_ICE=y CONFIG_IGC=y +CONFIG_MVNETA=y +CONFIG_MVPP2=y CONFIG_MLX4_EN=y CONFIG_MLX5_CORE=y CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_TIGON3=y CONFIG_R8169=y -CONFIG_THUNDER_NIC_BGX=m -CONFIG_THUNDER_NIC_PF=m -CONFIG_THUNDER_NIC_VF=m -CONFIG_MACB=m -CONFIG_HNS3=m -CONFIG_NET_XGENE=y -CONFIG_MVNETA=y -CONFIG_MVPP2=y CONFIG_STMMAC_ETH=m -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_VMXNET3=y -CONFIG_AMD_XGBE=y -# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) -CONFIG_BCMGENET=y -# PHY subsystem (required by GENET and other MAC drivers) -CONFIG_PHYLIB=y -CONFIG_MDIO_BUS=y +CONFIG_AMD_PHY=y +CONFIG_MESON_GXL_PHY=y CONFIG_BROADCOM_PHY=y -CONFIG_BCM7XXX_PHY=y -CONFIG_MDIO_BCM_UNIMAC=y CONFIG_MARVELL_PHY=y CONFIG_MARVELL_10G_PHY=y CONFIG_MICREL_PHY=y CONFIG_ROCKCHIP_PHY=y -CONFIG_REALTEK_PHY=y -CONFIG_MESON_GXL_PHY=y -CONFIG_AMD_PHY=y - -# --- USB --- -CONFIG_USB_SUPPORT=y +# CONFIG_WLAN is not set +CONFIG_VMXNET3=y +CONFIG_INPUT_EVDEV=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_SERIAL_AMBA_PL011=y +CONFIG_SERIAL_AMBA_PL011_CONSOLE=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_IPMI_HANDLER=m +CONFIG_IPMI_DEVICE_INTERFACE=m +CONFIG_IPMI_SI=m +CONFIG_HW_RANDOM=y +CONFIG_HW_RANDOM_TIMERIOMEM=y +CONFIG_HW_RANDOM_VIRTIO=y +CONFIG_I2C_CHARDEV=y +CONFIG_I2C_MV64XXX=y +CONFIG_I2C_RK3X=y +CONFIG_I2C_TEGRA=y +CONFIG_SPI=y +CONFIG_SPI_ORION=y +CONFIG_SPI_PL022=y +CONFIG_SPI_ROCKCHIP=y +CONFIG_WATCHDOG=y +CONFIG_DRM=y +CONFIG_DRM_SIMPLEDRM=y +CONFIG_DRM_VIRTIO_GPU=y +CONFIG_FB=y +CONFIG_FB_EFI=y CONFIG_USB=y CONFIG_USB_XHCI_HCD=y CONFIG_USB_EHCI_HCD=y CONFIG_USB_OHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y CONFIG_USB_STORAGE=y +CONFIG_USB_UAS=y CONFIG_USB_DWC3=y -CONFIG_USB_DWC3_HOST=y CONFIG_USB_DWC2=y CONFIG_USB_ISP1760=y - -# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- CONFIG_MMC=y CONFIG_MMC_BLOCK_MINORS=32 +CONFIG_MMC_ARMMMCI=y CONFIG_MMC_SDHCI=y CONFIG_MMC_SDHCI_PCI=y CONFIG_MMC_SDHCI_ACPI=y @@ -280,27 +297,48 @@ CONFIG_MMC_SDHCI_OF_ARASAN=y CONFIG_MMC_SDHCI_OF_DWCMSHC=y CONFIG_MMC_SDHCI_CADENCE=y CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SDHCI_XENON=y CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_ARMMMCI=y +CONFIG_MMC_MESON_GX=y +CONFIG_MMC_SDHCI_MSM=y +CONFIG_MMC_SPI=y CONFIG_MMC_DW=y -CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_DW_K3=y -CONFIG_MMC_MESON_GX=y +CONFIG_MMC_DW_ROCKCHIP=y CONFIG_MMC_SUNXI=m -CONFIG_MMC_SPI=y CONFIG_MMC_HSQ=y - -# --- Filesystems --- +CONFIG_MMC_SDHCI_XENON=y +CONFIG_RTC_CLASS=y +CONFIG_RTC_DRV_EFI=y +CONFIG_DMADEVICES=y +CONFIG_PL330_DMA=y +CONFIG_VFIO=y +CONFIG_VFIO_PCI=y +CONFIG_VIRTIO_PCI=y +CONFIG_VIRTIO_BALLOON=y +CONFIG_VIRTIO_INPUT=y +CONFIG_VIRTIO_MMIO=y +CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y +CONFIG_VHOST_NET=m +CONFIG_ARM_SMMU=y +CONFIG_ARM_SMMU_V3=y +CONFIG_TEE=y +CONFIG_OPTEE=y CONFIG_EXT4_FS=y CONFIG_XFS_FS=m +CONFIG_FUSE_FS=y +CONFIG_CUSE=y +CONFIG_OVERLAY_FS=y +CONFIG_ISO9660_FS=y +CONFIG_JOLIET=y +CONFIG_UDF_FS=y +CONFIG_MSDOS_FS=y CONFIG_VFAT_FS=y +CONFIG_FAT_DEFAULT_IOCHARSET="utf8" +CONFIG_NTFS_FS=m CONFIG_TMPFS=y CONFIG_TMPFS_POSIX_ACL=y -CONFIG_PROC_FS=y -CONFIG_SYSFS=y -CONFIG_OVERLAY_FS=y +CONFIG_HUGETLBFS=y +CONFIG_EFIVAR_FS=y CONFIG_SQUASHFS=y CONFIG_SQUASHFS_XATTR=y CONFIG_SQUASHFS_LZ4=y @@ -310,206 +348,17 @@ CONFIG_NFS_FS=m CONFIG_NFS_V4=m CONFIG_NFS_V4_1=y CONFIG_NFS_V4_2=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_EFIVAR_FS=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_UDF_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS_XATTR=y CONFIG_NLS_CODEPAGE_437=y CONFIG_NLS_ASCII=y CONFIG_NLS_ISO8859_1=y CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y - -# --- Console / Serial --- -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_HW_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y CONFIG_SECURITY=y -CONFIG_KEYS=y - -# --- Crypto --- -CONFIG_CRYPTO=y CONFIG_CRYPTO_AES=y +CONFIG_CRYPTO_XTS=y CONFIG_CRYPTO_SHA256=y CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_ARM64_CRYPTO=y CONFIG_CRYPTO_AES_ARM64_CE=y -CONFIG_CRYPTO_SHA2_ARM64_CE=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_XDP_SOCKETS=y - -# --- ARM64 SoC/platform support --- -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_UNIPHIER=y -# ACPI / UEFI -CONFIG_ACPI=y -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_ROCKCHIP_HOST=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=y -# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) -CONFIG_PCIE_BRCMSTB=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCI_TEGRA=y -CONFIG_PCI_XGENE=y -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_AL=y -CONFIG_PCI_MESON=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI_ACPI=y -# IOMMU -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_FW_LOADER=y -# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_ARM_SCMI_PROTOCOL=y -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_OF=y -CONFIG_USE_OF=y -# RTC -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_EFI=y -# DMA -CONFIG_DMADEVICES=y -CONFIG_PL330_DMA=y -CONFIG_PRINTK=y CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y CONFIG_MAGIC_SYSRQ=y -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -CONFIG_RD_ZSTD=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_EFI=y -CONFIG_EFI_STUB=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_VIRTIO_GPU=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y -CONFIG_SYSFB_SIMPLEFB=y -# Virtualization -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- I2C (SBC board management, EEPROM, sensors) --- -CONFIG_I2C=y -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_DESIGNWARE_PLATFORM=y -# SPI -CONFIG_SPI=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ORION=y -# GPIO -CONFIG_GPIOLIB=y -# Hardware watchdog -CONFIG_WATCHDOG=y -CONFIG_SOFTLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -# Hardware RNG -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y -# TPM -CONFIG_TCG_TIS=y -CONFIG_TCG_FTPM_TEE=y -CONFIG_TEE=y -CONFIG_OPTEE=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set +CONFIG_PANIC_ON_OOPS=y +CONFIG_PANIC_TIMEOUT=10 diff --git a/kernel.configs/6.19.y.amd64 b/kernel.configs/6.19.y.amd64 deleted file mode 100644 index c6d243cc..00000000 --- a/kernel.configs/6.19.y.amd64 +++ /dev/null @@ -1,1032 +0,0 @@ -# CaptainOS kernel defconfig for x86_64 -# Comprehensive config for bare-metal provisioning with container/network support. -# Derived from HookOS generic-6.6.y-x86_64 proven config, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.amd64 .config && make olddefconfig - -# --- Architecture --- -CONFIG_64BIT=y -CONFIG_X86_64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -# CONFIG_X86_EXTENDED_PLATFORM is not set -CONFIG_X86_INTEL_LPSS=y -CONFIG_HYPERVISOR_GUEST=y -CONFIG_PARAVIRT_SPINLOCKS=y -CONFIG_XEN=y -CONFIG_XEN_PVH=y -CONFIG_X86_REROUTE_FOR_BROKEN_BOOT_IRQS=y -# CONFIG_X86_MCE is not set -CONFIG_X86_MSR=y -CONFIG_X86_CPUID=y -# CONFIG_X86_5LEVEL is not set -CONFIG_HZ_1000=y -CONFIG_PHYSICAL_ALIGN=0x1000000 -CONFIG_LEGACY_VSYSCALL_NONE=y -# CONFIG_MODIFY_LDT_SYSCALL is not set -CONFIG_IA32_EMULATION=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -# CONFIG_LOCALVERSION_AUTO is not set -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_BSD_PROCESS_ACCT_V3=y -CONFIG_IKCONFIG=y -CONFIG_IKCONFIG_PROC=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_EXPERT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_SCHED_AUTOGROUP=y -CONFIG_CHECKPOINT_RESTORE=y -CONFIG_KPROBES=y -CONFIG_JUMP_LABEL=y -CONFIG_BINFMT_MISC=y - -# --- Cgroups --- -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_RDMA=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CPUSETS=y - -# --- Namespaces --- -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" -# CONFIG_RD_BZIP2 is not set -# CONFIG_RD_LZMA is not set -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -# CONFIG_RD_LZO is not set -# CONFIG_RD_LZ4 is not set -CONFIG_RD_ZSTD=y - -# --- Memory management --- -CONFIG_SLAB_FREELIST_RANDOM=y -# CONFIG_COMPAT_BRK is not set -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_KSM=y -CONFIG_DEFAULT_MMAP_MIN_ADDR=65536 -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_ZONE_DEVICE=y -CONFIG_HUGETLBFS=y - -# --- Power / Suspend --- -# CONFIG_SUSPEND is not set - -# --- ACPI --- -CONFIG_ACPI=y -# CONFIG_ACPI_REV_OVERRIDE_POSSIBLE is not set -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_PROCESSOR_AGGREGATOR=y -CONFIG_ACPI_SBS=y -CONFIG_ACPI_NFIT=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y - -# --- CPU frequency --- -CONFIG_CPU_FREQ_STAT=y -CONFIG_CPU_FREQ_GOV_POWERSAVE=y -CONFIG_CPU_FREQ_GOV_USERSPACE=y -CONFIG_CPU_FREQ_GOV_ONDEMAND=y -CONFIG_CPU_FREQ_GOV_CONSERVATIVE=y -CONFIG_X86_PCC_CPUFREQ=y -CONFIG_X86_ACPI_CPUFREQ=y -CONFIG_X86_POWERNOW_K8=y -CONFIG_X86_P4_CLOCKMOD=y -CONFIG_CPU_IDLE_GOV_LADDER=y -CONFIG_INTEL_IDLE=y - -# --- KVM --- -CONFIG_KVM=m -CONFIG_KVM_INTEL=m -CONFIG_KVM_AMD=m - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_PACKET_DIAG=y -CONFIG_UNIX=y -CONFIG_UNIX_DIAG=y -CONFIG_XFRM_USER=m -CONFIG_XFRM_SUB_POLICY=y -CONFIG_XFRM_STATISTICS=y -CONFIG_NET_KEY=m -CONFIG_NET_KEY_MIGRATE=y -CONFIG_XDP_SOCKETS=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_FIB_TRIE_STATS=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_ROUTE_VERBOSE=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=y -CONFIG_NET_IPGRE_DEMUX=y -CONFIG_NET_IPGRE=m -CONFIG_NET_IPGRE_BROADCAST=y -CONFIG_IP_MROUTE=y -CONFIG_IP_MROUTE_MULTIPLE_TABLES=y -CONFIG_IP_PIMSM_V1=y -CONFIG_IP_PIMSM_V2=y -CONFIG_NET_IPVTI=m -CONFIG_NET_FOU_IP_TUNNELS=y -CONFIG_INET_AH=m -CONFIG_INET_ESP=m -CONFIG_INET_IPCOMP=m -CONFIG_INET_UDP_DIAG=y -CONFIG_TCP_MD5SIG=y -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_IPV6_ROUTER_PREF=y -CONFIG_INET6_AH=m -CONFIG_INET6_ESP=m -CONFIG_INET6_IPCOMP=m -CONFIG_IPV6_MIP6=m -CONFIG_IPV6_ILA=m -CONFIG_IPV6_VTI=m -CONFIG_IPV6_SIT=m -CONFIG_IPV6_SIT_6RD=y -CONFIG_IPV6_GRE=m -CONFIG_IPV6_MULTIPLE_TABLES=y -CONFIG_IPV6_SUBTREES=y -CONFIG_NETLABEL=y -CONFIG_NETWORK_SECMARK=y -CONFIG_BRIDGE=y -CONFIG_BRIDGE_VLAN_FILTERING=y -CONFIG_VLAN_8021Q=y -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NETCONSOLE=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_NLMON=y -CONFIG_IP_SCTP=m -CONFIG_L2TP=m -CONFIG_NET_TEAM=m -CONFIG_NET_TEAM_MODE_BROADCAST=m -CONFIG_NET_TEAM_MODE_ROUNDROBIN=m -CONFIG_NET_TEAM_MODE_RANDOM=m -CONFIG_NET_TEAM_MODE_ACTIVEBACKUP=m -CONFIG_NET_TEAM_MODE_LOADBALANCE=m -CONFIG_OPENVSWITCH=m -CONFIG_VSOCKETS=m -CONFIG_VIRTIO_VSOCKETS=m -CONFIG_HYPERV_VSOCKETS=m -CONFIG_NETLINK_DIAG=y -CONFIG_MPLS_ROUTING=m -CONFIG_MPLS_IPTUNNEL=m -CONFIG_NET_SWITCHDEV=y -CONFIG_NET_9P=y -CONFIG_NET_9P_VIRTIO=y - -# --- Traffic control --- -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_HFSC=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_MULTIQ=m -CONFIG_NET_SCH_RED=m -CONFIG_NET_SCH_SFB=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TEQL=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_GRED=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_DRR=m -CONFIG_NET_SCH_MQPRIO=m -CONFIG_NET_SCH_CHOKE=m -CONFIG_NET_SCH_QFQ=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_ROUTE4=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_CLS_U32_PERF=y -CONFIG_CLS_U32_MARK=y -CONFIG_NET_CLS_FLOW=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_CMP=y -CONFIG_NET_EMATCH_NBYTE=y -CONFIG_NET_EMATCH_U32=y -CONFIG_NET_EMATCH_META=y -CONFIG_NET_EMATCH_TEXT=y -CONFIG_NET_EMATCH_IPSET=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_GACT_PROB=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_IPT=y -CONFIG_NET_ACT_NAT=y -CONFIG_NET_ACT_PEDIT=y -CONFIG_NET_ACT_SIMP=y -CONFIG_NET_ACT_SKBEDIT=y -CONFIG_NET_ACT_CSUM=y -CONFIG_NET_ACT_BPF=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_CONNTRACK_ZONES=y -CONFIG_NF_CONNTRACK_PROCFS=y -CONFIG_NF_CONNTRACK_EVENTS=y -CONFIG_NF_CONNTRACK_TIMEOUT=y -CONFIG_NF_CONNTRACK_TIMESTAMP=y -CONFIG_NF_CONNTRACK_AMANDA=y -CONFIG_NF_CONNTRACK_FTP=y -CONFIG_NF_CONNTRACK_H323=y -CONFIG_NF_CONNTRACK_IRC=y -CONFIG_NF_CONNTRACK_NETBIOS_NS=y -CONFIG_NF_CONNTRACK_SNMP=y -CONFIG_NF_CONNTRACK_PPTP=y -CONFIG_NF_CONNTRACK_SANE=y -CONFIG_NF_CONNTRACK_SIP=y -CONFIG_NF_CONNTRACK_TFTP=y -CONFIG_NF_CT_NETLINK=y -CONFIG_NF_CT_NETLINK_TIMEOUT=y -CONFIG_NF_CT_NETLINK_HELPER=y -CONFIG_NETFILTER_NETLINK_GLUE_CT=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_NETDEV=y -CONFIG_NFT_CT=y -CONFIG_NFT_CONNLIMIT=y -CONFIG_NFT_LOG=y -CONFIG_NFT_LIMIT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_REDIR=y -CONFIG_NFT_NAT=y -CONFIG_NFT_TUNNEL=y -CONFIG_NFT_QUEUE=y -CONFIG_NFT_REJECT=y -CONFIG_NFT_COMPAT=y -CONFIG_NFT_HASH=y -CONFIG_NFT_OSF=y -CONFIG_NFT_TPROXY=y -CONFIG_NFT_DUP_NETDEV=y -CONFIG_NFT_FWD_NETDEV=y -CONFIG_NETFILTER_XT_SET=y -CONFIG_NETFILTER_XT_TARGET_CHECKSUM=y -CONFIG_NETFILTER_XT_TARGET_CLASSIFY=y -CONFIG_NETFILTER_XT_TARGET_CONNMARK=y -CONFIG_NETFILTER_XT_TARGET_DSCP=y -CONFIG_NETFILTER_XT_TARGET_HMARK=y -CONFIG_NETFILTER_XT_TARGET_IDLETIMER=y -CONFIG_NETFILTER_XT_TARGET_LOG=y -CONFIG_NETFILTER_XT_TARGET_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_NETFILTER_XT_TARGET_NFLOG=y -CONFIG_NETFILTER_XT_TARGET_NFQUEUE=y -CONFIG_NETFILTER_XT_TARGET_NOTRACK=y -CONFIG_NETFILTER_XT_TARGET_TEE=y -CONFIG_NETFILTER_XT_TARGET_TPROXY=y -CONFIG_NETFILTER_XT_TARGET_TRACE=y -CONFIG_NETFILTER_XT_TARGET_TCPMSS=y -CONFIG_NETFILTER_XT_TARGET_TCPOPTSTRIP=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_BPF=y -CONFIG_NETFILTER_XT_MATCH_CGROUP=y -CONFIG_NETFILTER_XT_MATCH_CLUSTER=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_CONNBYTES=y -CONFIG_NETFILTER_XT_MATCH_CONNLABEL=y -CONFIG_NETFILTER_XT_MATCH_CONNLIMIT=y -CONFIG_NETFILTER_XT_MATCH_CONNMARK=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_CPU=y -CONFIG_NETFILTER_XT_MATCH_DCCP=y -CONFIG_NETFILTER_XT_MATCH_DEVGROUP=y -CONFIG_NETFILTER_XT_MATCH_DSCP=y -CONFIG_NETFILTER_XT_MATCH_ESP=y -CONFIG_NETFILTER_XT_MATCH_HASHLIMIT=y -CONFIG_NETFILTER_XT_MATCH_HELPER=y -CONFIG_NETFILTER_XT_MATCH_IPCOMP=y -CONFIG_NETFILTER_XT_MATCH_IPRANGE=y -CONFIG_NETFILTER_XT_MATCH_IPVS=y -CONFIG_NETFILTER_XT_MATCH_L2TP=y -CONFIG_NETFILTER_XT_MATCH_LENGTH=y -CONFIG_NETFILTER_XT_MATCH_LIMIT=y -CONFIG_NETFILTER_XT_MATCH_MAC=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y -CONFIG_NETFILTER_XT_MATCH_NFACCT=y -CONFIG_NETFILTER_XT_MATCH_OSF=y -CONFIG_NETFILTER_XT_MATCH_OWNER=y -CONFIG_NETFILTER_XT_MATCH_POLICY=y -CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y -CONFIG_NETFILTER_XT_MATCH_PKTTYPE=y -CONFIG_NETFILTER_XT_MATCH_QUOTA=y -CONFIG_NETFILTER_XT_MATCH_RATEEST=y -CONFIG_NETFILTER_XT_MATCH_REALM=y -CONFIG_NETFILTER_XT_MATCH_RECENT=y -CONFIG_NETFILTER_XT_MATCH_SCTP=y -CONFIG_NETFILTER_XT_MATCH_SOCKET=y -CONFIG_NETFILTER_XT_MATCH_STATE=y -CONFIG_NETFILTER_XT_MATCH_STATISTIC=y -CONFIG_NETFILTER_XT_MATCH_STRING=y -CONFIG_NETFILTER_XT_MATCH_TCPMSS=y -CONFIG_NETFILTER_XT_MATCH_TIME=y -CONFIG_NETFILTER_XT_MATCH_U32=y -CONFIG_IP_SET=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_IPMAC=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTIP=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_HASH_NETIFACE=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_PROTO_ESP=y -CONFIG_IP_VS_PROTO_AH=y -CONFIG_IP_VS_PROTO_SCTP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_LC=y -CONFIG_IP_VS_WLC=y -CONFIG_IP_VS_FO=y -CONFIG_IP_VS_OVF=y -CONFIG_IP_VS_LBLC=y -CONFIG_IP_VS_LBLCR=y -CONFIG_IP_VS_DH=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_MH=y -CONFIG_IP_VS_SED=y -CONFIG_IP_VS_NQ=y -CONFIG_IP_VS_FTP=y -CONFIG_NFT_DUP_IPV4=y -CONFIG_NF_TABLES_ARP=y -CONFIG_NF_LOG_ARP=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_MATCH_AH=y -CONFIG_IP_NF_MATCH_ECN=y -CONFIG_IP_NF_MATCH_RPFILTER=y -CONFIG_IP_NF_MATCH_TTL=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_TARGET_REJECT=y -CONFIG_IP_NF_TARGET_SYNPROXY=y -CONFIG_IP_NF_NAT=y -CONFIG_IP_NF_TARGET_MASQUERADE=y -CONFIG_IP_NF_TARGET_NETMAP=y -CONFIG_IP_NF_TARGET_REDIRECT=y -CONFIG_IP_NF_MANGLE=y -CONFIG_IP_NF_TARGET_ECN=y -CONFIG_IP_NF_TARGET_TTL=y -CONFIG_IP_NF_RAW=y -CONFIG_IP_NF_SECURITY=y -CONFIG_IP_NF_ARPTABLES=y -CONFIG_IP_NF_ARPFILTER=y -CONFIG_IP_NF_ARP_MANGLE=y -CONFIG_NFT_DUP_IPV6=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_MATCH_AH=y -CONFIG_IP6_NF_MATCH_EUI64=y -CONFIG_IP6_NF_MATCH_FRAG=y -CONFIG_IP6_NF_MATCH_OPTS=y -CONFIG_IP6_NF_MATCH_HL=y -CONFIG_IP6_NF_MATCH_IPV6HEADER=y -CONFIG_IP6_NF_MATCH_MH=y -CONFIG_IP6_NF_MATCH_RPFILTER=y -CONFIG_IP6_NF_MATCH_RT=y -CONFIG_IP6_NF_TARGET_HL=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_TARGET_REJECT=y -CONFIG_IP6_NF_TARGET_SYNPROXY=y -CONFIG_IP6_NF_MANGLE=y -CONFIG_IP6_NF_RAW=y -CONFIG_IP6_NF_SECURITY=y -CONFIG_IP6_NF_NAT=y -CONFIG_IP6_NF_TARGET_MASQUERADE=y -CONFIG_IP6_NF_TARGET_NPT=y -CONFIG_NF_TABLES_BRIDGE=y -CONFIG_NFT_BRIDGE_REJECT=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_802_3=y -CONFIG_BRIDGE_EBT_AMONG=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_LIMIT=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_PKTTYPE=y -CONFIG_BRIDGE_EBT_STP=y -CONFIG_BRIDGE_EBT_VLAN=y -CONFIG_BRIDGE_EBT_ARPREPLY=y -CONFIG_BRIDGE_EBT_DNAT=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_REDIRECT=y -CONFIG_BRIDGE_EBT_SNAT=y -CONFIG_BRIDGE_EBT_LOG=y -CONFIG_BRIDGE_EBT_NFLOG=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_INTEGRITY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_PARTITION_ADVANCED=y -CONFIG_LDM_PARTITION=y -CONFIG_CMDLINE_PARTITION=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_DRBD=m -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_ATA_OVER_ETH=m -CONFIG_BLK_DEV_NVME=y - -# --- SCSI --- -CONFIG_SCSI=y -# CONFIG_SCSI_PROC_FS is not set -CONFIG_BLK_DEV_SD=y -CONFIG_BLK_DEV_SR=y -CONFIG_CHR_DEV_SG=y -CONFIG_ISCSI_TCP=m -CONFIG_SCSI_HPSA=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_SMARTPQI=m -CONFIG_VMWARE_PVSCSI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- ATA / SATA --- -CONFIG_ATA=y -# CONFIG_ATA_VERBOSE_ERROR is not set -# CONFIG_SATA_PMP is not set -CONFIG_SATA_AHCI=y -CONFIG_ATA_PIIX=y -CONFIG_SATA_MV=y -CONFIG_SATA_NV=y -CONFIG_SATA_PROMISE=y -CONFIG_SATA_SIL=y -CONFIG_SATA_SIS=y -CONFIG_SATA_SVW=y -CONFIG_SATA_ULI=y -CONFIG_SATA_VIA=y -CONFIG_SATA_VITESSE=y -CONFIG_ATA_GENERIC=y - -# --- Device mapper / MD --- -CONFIG_MD=y -CONFIG_BLK_DEV_MD=y -CONFIG_MD_LINEAR=y -CONFIG_MD_RAID0=y -CONFIG_MD_RAID1=y -CONFIG_MD_RAID10=y -CONFIG_MD_RAID456=y -CONFIG_MD_MULTIPATH=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_MULTIPATH=m -CONFIG_DM_MULTIPATH_QL=m -CONFIG_DM_MULTIPATH_ST=m - -# --- Fusion --- -CONFIG_FUSION=y -CONFIG_FUSION_SPI=y - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -# CONFIG_NET_VENDOR_3COM is not set -# CONFIG_NET_VENDOR_ADAPTEC is not set -# CONFIG_NET_VENDOR_AGERE is not set -# CONFIG_NET_VENDOR_ALACRITECH is not set -# CONFIG_NET_VENDOR_ALTEON is not set -# CONFIG_NET_VENDOR_AMD is not set -# CONFIG_NET_VENDOR_AQUANTIA is not set -# CONFIG_NET_VENDOR_ARC is not set -# CONFIG_NET_VENDOR_ATHEROS is not set -# CONFIG_NET_VENDOR_CADENCE is not set -# CONFIG_NET_VENDOR_CAVIUM is not set -# CONFIG_NET_VENDOR_CHELSIO is not set -# CONFIG_NET_VENDOR_CORTINA is not set -# CONFIG_NET_VENDOR_DEC is not set -# CONFIG_NET_VENDOR_DLINK is not set -# CONFIG_NET_VENDOR_EMULEX is not set -# CONFIG_NET_VENDOR_EZCHIP is not set -# CONFIG_NET_VENDOR_HUAWEI is not set -# CONFIG_NET_VENDOR_I825XX is not set -# CONFIG_NET_VENDOR_MARVELL is not set -# CONFIG_NET_VENDOR_MICREL is not set -# CONFIG_NET_VENDOR_MICROCHIP is not set -# CONFIG_NET_VENDOR_MYRI is not set -# CONFIG_NET_VENDOR_NI is not set -# CONFIG_NET_VENDOR_NATSEMI is not set -# CONFIG_NET_VENDOR_NETERION is not set -# CONFIG_NET_VENDOR_NVIDIA is not set -# CONFIG_NET_VENDOR_OKI is not set -# CONFIG_NET_VENDOR_PACKET_ENGINES is not set -# CONFIG_NET_VENDOR_QLOGIC is not set -# CONFIG_NET_VENDOR_BROCADE is not set -# CONFIG_NET_VENDOR_QUALCOMM is not set -# CONFIG_NET_VENDOR_RDC is not set -# CONFIG_NET_VENDOR_RENESAS is not set -# CONFIG_NET_VENDOR_ROCKER is not set -# CONFIG_NET_VENDOR_SAMSUNG is not set -# CONFIG_NET_VENDOR_SEEQ is not set -# CONFIG_NET_VENDOR_SILAN is not set -# CONFIG_NET_VENDOR_SIS is not set -# CONFIG_NET_VENDOR_SMSC is not set -# CONFIG_NET_VENDOR_SOCIONEXT is not set -# CONFIG_NET_VENDOR_STMICRO is not set -# CONFIG_NET_VENDOR_SUN is not set -# CONFIG_NET_VENDOR_SYNOPSYS is not set -# CONFIG_NET_VENDOR_TEHUTI is not set -# CONFIG_NET_VENDOR_TI is not set -# CONFIG_NET_VENDOR_VIA is not set -# CONFIG_NET_VENDOR_WIZNET is not set -CONFIG_E1000=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IGBVF=y -CONFIG_IXGBE=y -CONFIG_IXGBEVF=y -CONFIG_I40E=y -CONFIG_I40EVF=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_CNIC=m -CONFIG_TIGON3=y -CONFIG_BNX2X=y -CONFIG_R8169=y -CONFIG_8139CP=y -CONFIG_8139TOO=y -CONFIG_VMXNET3=y -CONFIG_HYPERV_NET=y -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_NFP=m -CONFIG_IONIC=y -CONFIG_VIRTIO_NET=y - -# --- USB network --- -CONFIG_USB_CATC=m -CONFIG_USB_KAWETH=m -CONFIG_USB_PEGASUS=m -CONFIG_USB_RTL8150=y -CONFIG_USB_RTL8152=y -CONFIG_USB_LAN78XX=m -CONFIG_USB_USBNET=y -CONFIG_USB_NET_AX8817X=m -CONFIG_USB_NET_AX88179_178A=m -CONFIG_USB_NET_CDC_EEM=m -CONFIG_USB_NET_CDC_NCM=m -CONFIG_USB_NET_NET1080=m -CONFIG_USB_NET_CDC_SUBSET=m -CONFIG_USB_NET_ZAURUS=m - -# --- WAN --- -CONFIG_WAN=y -CONFIG_HDLC=m -CONFIG_HDLC_CISCO=m - -# --- PPP --- -CONFIG_PPP=m -CONFIG_PPP_BSDCOMP=m -CONFIG_PPP_DEFLATE=m -CONFIG_PPP_FILTER=y -CONFIG_PPP_MPPE=m -CONFIG_PPP_MULTILINK=y -CONFIG_PPPOE=m -CONFIG_PPTP=m -CONFIG_PPPOL2TP=m -CONFIG_PPP_ASYNC=m -CONFIG_PPP_SYNC_TTY=m - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_UHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y - -# --- USB serial (BMC/console access) --- -CONFIG_USB_SERIAL=y -CONFIG_USB_SERIAL_CONSOLE=y -CONFIG_USB_SERIAL_GENERIC=y -CONFIG_USB_SERIAL_CH341=y -CONFIG_USB_SERIAL_CP210X=y -CONFIG_USB_SERIAL_FTDI_SIO=y -CONFIG_USB_SERIAL_PL2303=y -CONFIG_USB_SERIAL_TI=y -CONFIG_USB_SERIAL_OPTION=y - -# --- Input --- -CONFIG_INPUT_FF_MEMLESS=y -CONFIG_INPUT_SPARSEKMAP=y -CONFIG_INPUT_MOUSEDEV=y -CONFIG_INPUT_MOUSEDEV_PSAUX=y -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_MOUSE is not set -CONFIG_INPUT_MISC=y -CONFIG_INPUT_PCSPKR=y -CONFIG_INPUT_ATLAS_BTNS=y -CONFIG_INPUT_UINPUT=y -CONFIG_SERIO_PCIPS2=y -CONFIG_SERIO_RAW=y -# CONFIG_LEGACY_PTYS is not set -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set - -# --- MMC --- -CONFIG_MMC=y -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -# CONFIG_MMC_RICOH_MMC is not set -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_EXT4_FS_POSIX_ACL=y -CONFIG_EXT4_FS_SECURITY=y -CONFIG_XFS_FS=y -CONFIG_XFS_QUOTA=y -CONFIG_XFS_POSIX_ACL=y -CONFIG_BTRFS_FS=m -CONFIG_BTRFS_FS_POSIX_ACL=y -CONFIG_FS_DAX=y -CONFIG_FS_ENCRYPTION=y -CONFIG_FANOTIFY=y -CONFIG_QUOTA=y -CONFIG_QUOTA_NETLINK_INTERFACE=y -CONFIG_VFAT_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_TMPFS_XATTR=y -CONFIG_PROC_FS=y -CONFIG_PROC_KCORE=y -CONFIG_SYSFS=y -CONFIG_EFIVAR_FS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_ZISOFS=y -CONFIG_UDF_FS=y -CONFIG_FSCACHE=y -CONFIG_FSCACHE_STATS=y -CONFIG_CACHEFILES=y -CONFIG_NFS_FS=m -# CONFIG_NFS_V2 is not set -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_NFS_FSCACHE=y -CONFIG_NFSD=m -CONFIG_NFSD_V4=y -CONFIG_CEPH_FS=m -CONFIG_CEPH_FSCACHE=y -CONFIG_CEPH_FS_POSIX_ACL=y -CONFIG_CIFS=y -# CONFIG_CIFS_ALLOW_INSECURE_LEGACY is not set -CONFIG_CIFS_XATTR=y -CONFIG_CIFS_DFS_UPCALL=y -CONFIG_CIFS_FSCACHE=y -CONFIG_9P_FS=y -CONFIG_9P_FSCACHE=y -CONFIG_9P_FS_POSIX_ACL=y -CONFIG_9P_FS_SECURITY=y - -# --- NLS --- -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Console / Serial --- -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_SERIAL_8250_NR_UARTS=32 -# CONFIG_SERIAL_8250_MID is not set -CONFIG_SERIAL_DEV_BUS=y -CONFIG_TTY_PRINTK=y -CONFIG_HW_CONSOLE=y -CONFIG_VGA_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- IPMI --- -CONFIG_IPMI_HANDLER=y -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=y -CONFIG_IPMI_SI=y -CONFIG_IPMI_POWEROFF=y - -# --- Hardware RNG --- -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y - -# --- I2C --- -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MUX=y - -# --- TPM --- -CONFIG_TCG_TIS_I2C_ATMEL=m -CONFIG_TCG_TIS_I2C_INFINEON=m -CONFIG_TCG_TIS_I2C_NUVOTON=m -CONFIG_TCG_NSC=m -CONFIG_TCG_ATMEL=m -CONFIG_TCG_INFINEON=m -CONFIG_TCG_XEN=m -CONFIG_TCG_VTPM_PROXY=m -CONFIG_TCG_TIS_ST33ZP24_I2C=m - -# --- Platform / MFD --- -CONFIG_NVRAM=y -CONFIG_HPET=y -CONFIG_HANGCHECK_TIMER=y -CONFIG_LPC_ICH=y -CONFIG_LPC_SCH=y -CONFIG_MFD_INTEL_LPSS_ACPI=y -CONFIG_MFD_INTEL_LPSS_PCI=y -CONFIG_MFD_SM501=y -CONFIG_MFD_VX855=y -# CONFIG_THERMAL_HWMON is not set - -# --- RTC --- -CONFIG_RTC_CLASS=y - -# --- DMA --- -CONFIG_DMADEVICES=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_SECURITY_NETWORK=y -CONFIG_SECURITY_NETWORK_XFRM=y -CONFIG_SECURITY_PATH=y -CONFIG_SECURITY_DMESG_RESTRICT=y -CONFIG_KEYS=y -CONFIG_PERSISTENT_KEYRINGS=y -CONFIG_TRUSTED_KEYS=y -CONFIG_KEY_DH_OPERATIONS=y -CONFIG_HARDENED_USERCOPY=y -CONFIG_FORTIFY_SOURCE=y -CONFIG_STATIC_USERMODEHELPER=y -CONFIG_SECURITY_YAMA=y -CONFIG_INTEGRITY_SIGNATURE=y -CONFIG_INTEGRITY_ASYMMETRIC_KEYS=y -CONFIG_IMA=y -CONFIG_IMA_DEFAULT_HASH_SHA256=y -CONFIG_IMA_READ_POLICY=y -CONFIG_IMA_APPRAISE=y -CONFIG_EVM=y -CONFIG_LSM="yama,loadpin,safesetid,integrity" - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_USER=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_CRYPTO_ANUBIS=y -CONFIG_CRYPTO_BLOWFISH=y -CONFIG_CRYPTO_CAMELLIA=y -CONFIG_CRYPTO_DES=y -CONFIG_CRYPTO_FCRYPT=y -CONFIG_CRYPTO_KHAZAD=y -CONFIG_CRYPTO_SEED=y -CONFIG_CRYPTO_TEA=y -CONFIG_CRYPTO_TWOFISH=y -CONFIG_CRYPTO_ARC4=y -CONFIG_CRYPTO_KEYWRAP=y -CONFIG_CRYPTO_LRW=y -CONFIG_CRYPTO_PCBC=y -CONFIG_CRYPTO_CHACHA20POLY1305=y -CONFIG_CRYPTO_SEQIV=y -CONFIG_CRYPTO_ECHAINIV=y -CONFIG_CRYPTO_MICHAEL_MIC=y -CONFIG_CRYPTO_RMD160=y -CONFIG_CRYPTO_VMAC=y -CONFIG_CRYPTO_WP512=y -CONFIG_CRYPTO_XCBC=y -CONFIG_CRYPTO_CRC32=y -CONFIG_CRYPTO_LZO=y -CONFIG_CRYPTO_842=y -CONFIG_CRYPTO_LZ4=y -CONFIG_CRYPTO_LZ4HC=y -CONFIG_CRYPTO_ANSI_CPRNG=y -CONFIG_CRYPTO_USER_API_HASH=y -CONFIG_CRYPTO_USER_API_SKCIPHER=y -CONFIG_CRYPTO_USER_API_RNG=y -CONFIG_CRYPTO_USER_API_AEAD=y - -# --- x86_64 hardware crypto acceleration --- -CONFIG_CRYPTO_AES_NI_INTEL=y -CONFIG_CRYPTO_BLOWFISH_X86_64=y -CONFIG_CRYPTO_CAMELLIA_AESNI_AVX2_X86_64=y -CONFIG_CRYPTO_CAST5_AVX_X86_64=y -CONFIG_CRYPTO_CAST6_AVX_X86_64=y -CONFIG_CRYPTO_DES3_EDE_X86_64=y -CONFIG_CRYPTO_SERPENT_SSE2_X86_64=y -CONFIG_CRYPTO_SERPENT_AVX2_X86_64=y -CONFIG_CRYPTO_TWOFISH_AVX_X86_64=y -CONFIG_CRYPTO_CHACHA20_X86_64=y -CONFIG_CRYPTO_POLY1305_X86_64=y -CONFIG_CRYPTO_SHA1_SSSE3=y -CONFIG_CRYPTO_SHA256_SSSE3=y -CONFIG_CRYPTO_SHA512_SSSE3=y -CONFIG_CRYPTO_GHASH_CLMUL_NI_INTEL=y -CONFIG_CRYPTO_CRC32C_INTEL=y -CONFIG_CRYPTO_CRC32_PCLMUL=y -CONFIG_CRYPTO_DEV_PADLOCK=y -CONFIG_CRYPTO_DEV_PADLOCK_AES=y -CONFIG_CRYPTO_DEV_PADLOCK_SHA=y -CONFIG_CRYPTO_DEV_VIRTIO=m -CONFIG_PKCS7_MESSAGE_PARSER=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y - -# --- PCI --- -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI_PCIE=y -CONFIG_PCI_STUB=y -CONFIG_PCI_IOV=y -# CONFIG_VGA_ARB is not set -CONFIG_HOTPLUG_PCI=y -CONFIG_HOTPLUG_PCI_ACPI=y -CONFIG_HOTPLUG_PCI_SHPC=y -CONFIG_PCI_HYPERV_INTERFACE=m - -# --- IOMMU --- -CONFIG_AMD_IOMMU=y -CONFIG_INTEL_IOMMU=y -CONFIG_IRQ_REMAP=y - -# --- Xen --- -# CONFIG_XEN_BACKEND is not set -CONFIG_XEN_GNTDEV=y -CONFIG_XEN_GRANT_DEV_ALLOC=y -CONFIG_XEN_PVCALLS_FRONTEND=y -CONFIG_XEN_ACPI_PROCESSOR=y -# CONFIG_XEN_SYMS is not set - -# --- Intel platform --- -CONFIG_INTEL_IPS=y - -# --- DEVFREQ --- -CONFIG_PM_DEVFREQ=y -CONFIG_DEVFREQ_GOV_SIMPLE_ONDEMAND=y - -# --- Reset --- -CONFIG_RESET_CONTROLLER=y - -# --- DAX --- -CONFIG_DEV_DAX=y -CONFIG_DEV_DAX_PMEM=m - -# --- Misc --- -CONFIG_FW_LOADER=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_EARLY_PRINTK=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_SYSFB_SIMPLEFB=y -CONFIG_RESET_ATTACK_MITIGATION=y -# CONFIG_PNP_DEBUG_MESSAGES is not set -CONFIG_HYPERV=y -CONFIG_HYPERV_UTILS=y -CONFIG_HYPERV_BALLOON=y - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- EFI boot support --- -CONFIG_EFI=y -CONFIG_EFI_STUB=y -CONFIG_EFI_MIXED=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_I915=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y - -# --- Debugging / Diagnostics --- -CONFIG_FRAME_WARN=1024 -CONFIG_BUG_ON_DATA_CORRUPTION=y -CONFIG_HARDLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -CONFIG_DEBUG_NOTIFIERS=y -CONFIG_RCU_CPU_STALL_TIMEOUT=60 -# CONFIG_RCU_TRACE is not set -CONFIG_IO_STRICT_DEVMEM=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set diff --git a/kernel.configs/6.19.y.arm64 b/kernel.configs/6.19.y.arm64 deleted file mode 100644 index 795c91e6..00000000 --- a/kernel.configs/6.19.y.arm64 +++ /dev/null @@ -1,515 +0,0 @@ -# CaptainOS kernel defconfig for arm64 (aarch64) -# Minimal config for bare-metal provisioning with container/network support. -# Based on 6.12.y config with targeted additions for CaptainOS use case, adapted for kernel 6.18. -# -# Generate a full .config from this: -# cp 6.18.y.arm64 .config && make ARCH=arm64 olddefconfig - -# --- Architecture --- -CONFIG_ARM64=y -CONFIG_SMP=y -CONFIG_NR_CPUS=128 -CONFIG_NUMA=y -CONFIG_ARM64_VA_BITS_48=y -CONFIG_SCHED_MC=y -CONFIG_SCHED_SMT=y -CONFIG_HZ_1000=y -CONFIG_XEN=y -CONFIG_COMPAT=y -CONFIG_RANDOMIZE_BASE=y -CONFIG_ARM64_ACPI_PARKING_PROTOCOL=y - -# --- General --- -CONFIG_LOCALVERSION="-captainos" -CONFIG_DEFAULT_HOSTNAME="captainos" -CONFIG_SYSVIPC=y -CONFIG_POSIX_MQUEUE=y -CONFIG_AUDIT=y -CONFIG_NO_HZ_IDLE=y -CONFIG_HIGH_RES_TIMERS=y -CONFIG_PREEMPT_VOLUNTARY=y -CONFIG_BSD_PROCESS_ACCT=y -CONFIG_KEXEC=y -CONFIG_KEXEC_FILE=y -CONFIG_CC_OPTIMIZE_FOR_SIZE=y -CONFIG_CGROUPS=y -CONFIG_CGROUP_FREEZER=y -CONFIG_CGROUP_PIDS=y -CONFIG_CGROUP_DEVICE=y -CONFIG_CGROUP_CPUACCT=y -CONFIG_CGROUP_PERF=y -CONFIG_CGROUP_BPF=y -CONFIG_CGROUP_MISC=y -CONFIG_CGROUP_HUGETLB=y -CONFIG_CGROUP_RDMA=y -CONFIG_CPUSETS=y -CONFIG_MEMCG=y -CONFIG_BLK_CGROUP=y -CONFIG_BLK_CGROUP_IOLATENCY=y -CONFIG_BLK_DEV_THROTTLING=y -CONFIG_CFS_BANDWIDTH=y -CONFIG_RT_GROUP_SCHED=y -CONFIG_CGROUP_SCHED=y -CONFIG_CGROUP_NET_PRIO=y -CONFIG_NAMESPACES=y -CONFIG_USER_NS=y -CONFIG_PID_NS=y -CONFIG_NET_NS=y -CONFIG_UTS_NS=y -CONFIG_IPC_NS=y -CONFIG_CHECKPOINT_RESTORE=y -# Memory management -CONFIG_TRANSPARENT_HUGEPAGE=y -CONFIG_KSM=y -CONFIG_MEMORY_HOTPLUG=y -CONFIG_MEMORY_HOTREMOVE=y -CONFIG_HUGETLBFS=y - -# --- Init / Initramfs --- -CONFIG_BLK_DEV_INITRD=y -CONFIG_INITRAMFS_SOURCE="" - -# --- Networking --- -CONFIG_NET=y -CONFIG_PACKET=y -CONFIG_UNIX=y -CONFIG_INET=y -CONFIG_IP_MULTICAST=y -CONFIG_IP_ADVANCED_ROUTER=y -CONFIG_IP_MULTIPLE_TABLES=y -CONFIG_IP_ROUTE_MULTIPATH=y -CONFIG_IP_PNP=y -CONFIG_IP_PNP_DHCP=y -CONFIG_IP_PNP_BOOTP=y -CONFIG_NET_IPIP=m -CONFIG_NET_IPGRE_DEMUX=m -CONFIG_NET_IPGRE=m -CONFIG_SYN_COOKIES=y -CONFIG_IPV6=y -CONFIG_BRIDGE=y -CONFIG_VLAN_8021Q=m -CONFIG_BONDING=m -CONFIG_TUN=y -CONFIG_VETH=y -CONFIG_MACVLAN=y -CONFIG_MACVTAP=y -CONFIG_IPVLAN=y -CONFIG_TAP=y -CONFIG_DUMMY=m -CONFIG_VXLAN=y -CONFIG_GENEVE=m -CONFIG_NLMON=y -# Traffic control / QoS (needed by CNI plugins) -CONFIG_NET_SCHED=y -CONFIG_NET_SCH_HTB=m -CONFIG_NET_SCH_PRIO=m -CONFIG_NET_SCH_SFQ=m -CONFIG_NET_SCH_TBF=m -CONFIG_NET_SCH_NETEM=m -CONFIG_NET_SCH_INGRESS=m -CONFIG_NET_CLS_BASIC=y -CONFIG_NET_CLS_FW=y -CONFIG_NET_CLS_U32=y -CONFIG_NET_CLS_CGROUP=y -CONFIG_NET_CLS_BPF=y -CONFIG_NET_CLS_MATCHALL=y -CONFIG_NET_CLS_ACT=y -CONFIG_NET_ACT_POLICE=y -CONFIG_NET_ACT_GACT=y -CONFIG_NET_ACT_MIRRED=y -CONFIG_NET_ACT_BPF=y -CONFIG_NET_EMATCH=y -CONFIG_NET_EMATCH_U32=y - -# --- Netfilter --- -CONFIG_NETFILTER=y -CONFIG_NETFILTER_ADVANCED=y -CONFIG_NF_CONNTRACK=y -CONFIG_NF_NAT=y -CONFIG_NF_TABLES=y -CONFIG_NF_TABLES_INET=y -CONFIG_NF_TABLES_IPV4=y -CONFIG_NF_TABLES_IPV6=y -CONFIG_NFT_NAT=y -CONFIG_NFT_MASQ=y -CONFIG_NFT_CT=y -CONFIG_NFT_COMPAT=y -CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y -CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y -CONFIG_NETFILTER_XT_MATCH_COMMENT=y -CONFIG_NETFILTER_XT_MATCH_MARK=y -CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y -CONFIG_IP_NF_IPTABLES=y -CONFIG_IP_NF_FILTER=y -CONFIG_IP_NF_NAT=y -CONFIG_IP6_NF_IPTABLES=y -CONFIG_IP6_NF_FILTER=y -CONFIG_IP6_NF_NAT=y -CONFIG_BRIDGE_NETFILTER=y -CONFIG_BRIDGE_NF_EBTABLES=y -CONFIG_BRIDGE_EBT_BROUTE=y -CONFIG_BRIDGE_EBT_T_FILTER=y -CONFIG_BRIDGE_EBT_T_NAT=y -CONFIG_BRIDGE_EBT_ARP=y -CONFIG_BRIDGE_EBT_IP=y -CONFIG_BRIDGE_EBT_IP6=y -CONFIG_BRIDGE_EBT_MARK=y -CONFIG_BRIDGE_EBT_MARK_T=y -CONFIG_BRIDGE_EBT_VLAN=y -# IPVS (container load balancing / kube-proxy) -CONFIG_IP_VS=y -CONFIG_IP_VS_IPV6=y -CONFIG_IP_VS_PROTO_TCP=y -CONFIG_IP_VS_PROTO_UDP=y -CONFIG_IP_VS_RR=y -CONFIG_IP_VS_WRR=y -CONFIG_IP_VS_SH=y -CONFIG_IP_VS_NFCT=y -# IP sets (used by iptables/nftables for efficient matching) -CONFIG_IP_SET=y -CONFIG_IP_SET_HASH_IP=y -CONFIG_IP_SET_HASH_IPPORT=y -CONFIG_IP_SET_HASH_IPPORTNET=y -CONFIG_IP_SET_HASH_NET=y -CONFIG_IP_SET_HASH_NETPORT=y -CONFIG_IP_SET_BITMAP_IP=y -CONFIG_IP_SET_BITMAP_PORT=y -CONFIG_IP_SET_LIST_SET=y -CONFIG_NETFILTER_XT_SET=y - -# --- Block devices --- -CONFIG_BLK_DEV=y -CONFIG_BLK_DEV_LOOP=y -CONFIG_BLK_DEV_NBD=y -CONFIG_BLK_DEV_RAM=y -CONFIG_BLK_DEV_RAM_SIZE=65536 -CONFIG_BLK_DEV_NVME=y -CONFIG_ATA=y -CONFIG_SATA_AHCI=y -CONFIG_SATA_AHCI_PLATFORM=y - -# --- SCSI --- -CONFIG_SCSI=y -CONFIG_BLK_DEV_SD=y -CONFIG_CHR_DEV_SG=y -CONFIG_MEGARAID_SAS=y -CONFIG_SCSI_MPT3SAS=y -CONFIG_SCSI_MPI3MR=m -CONFIG_SCSI_HPSA=y -CONFIG_SCSI_HISI_SAS=y -CONFIG_SCSI_HISI_SAS_PCI=y -CONFIG_XEN_SCSI_FRONTEND=y -CONFIG_SCSI_VIRTIO=y - -# --- Device mapper --- -CONFIG_MD=y -CONFIG_BLK_DEV_DM=y -CONFIG_DM_CRYPT=y -CONFIG_DM_THIN_PROVISIONING=y -CONFIG_DM_SNAPSHOT=y -CONFIG_DM_MULTIPATH=m -CONFIG_MD_RAID0=m -CONFIG_MD_RAID1=m - -# --- Network drivers (built-in for reliable boot) --- -CONFIG_NETDEVICES=y -CONFIG_VIRTIO_NET=y -CONFIG_E1000E=y -CONFIG_IGB=y -CONFIG_IXGBE=y -CONFIG_I40E=y -CONFIG_ICE=y -CONFIG_IGC=y -CONFIG_MLX4_EN=y -CONFIG_MLX5_CORE=y -CONFIG_MLX5_CORE_EN=y -CONFIG_BNXT=y -CONFIG_TIGON3=y -CONFIG_R8169=y -CONFIG_THUNDER_NIC_BGX=m -CONFIG_THUNDER_NIC_PF=m -CONFIG_THUNDER_NIC_VF=m -CONFIG_MACB=m -CONFIG_HNS3=m -CONFIG_NET_XGENE=y -CONFIG_MVNETA=y -CONFIG_MVPP2=y -CONFIG_STMMAC_ETH=m -CONFIG_ENA_ETHERNET=m -CONFIG_GVE=m -CONFIG_VMXNET3=y -CONFIG_AMD_XGBE=y -# Broadcom GENET (Raspberry Pi 4B built-in Ethernet) -CONFIG_BCMGENET=y -# PHY subsystem (required by GENET and other MAC drivers) -CONFIG_PHYLIB=y -CONFIG_MDIO_BUS=y -CONFIG_BROADCOM_PHY=y -CONFIG_BCM7XXX_PHY=y -CONFIG_MDIO_BCM_UNIMAC=y -CONFIG_MARVELL_PHY=y -CONFIG_MARVELL_10G_PHY=y -CONFIG_MICREL_PHY=y -CONFIG_ROCKCHIP_PHY=y -CONFIG_REALTEK_PHY=y -CONFIG_MESON_GXL_PHY=y -CONFIG_AMD_PHY=y - -# --- USB --- -CONFIG_USB_SUPPORT=y -CONFIG_USB=y -CONFIG_USB_XHCI_HCD=y -CONFIG_USB_EHCI_HCD=y -CONFIG_USB_OHCI_HCD=y -CONFIG_USB_XHCI_PCI=y -CONFIG_USB_UAS=y -CONFIG_USB_STORAGE=y -CONFIG_USB_DWC3=y -CONFIG_USB_DWC3_HOST=y -CONFIG_USB_DWC2=y -CONFIG_USB_ISP1760=y - -# --- MMC/SD (Raspberry Pi, Rockchip, and other SBCs) --- -CONFIG_MMC=y -CONFIG_MMC_BLOCK_MINORS=32 -CONFIG_MMC_SDHCI=y -CONFIG_MMC_SDHCI_PCI=y -CONFIG_MMC_SDHCI_ACPI=y -CONFIG_MMC_SDHCI_PLTFM=y -CONFIG_MMC_SDHCI_OF_ARASAN=y -CONFIG_MMC_SDHCI_OF_DWCMSHC=y -CONFIG_MMC_SDHCI_CADENCE=y -CONFIG_MMC_SDHCI_TEGRA=y -CONFIG_MMC_SDHCI_MSM=y -CONFIG_MMC_SDHCI_XENON=y -CONFIG_MMC_SDHCI_F_SDH30=y -CONFIG_MMC_ARMMMCI=y -CONFIG_MMC_DW=y -CONFIG_MMC_DW_ROCKCHIP=y -CONFIG_MMC_DW_K3=y -CONFIG_MMC_MESON_GX=y -CONFIG_MMC_SUNXI=m -CONFIG_MMC_SPI=y -CONFIG_MMC_HSQ=y - -# --- Filesystems --- -CONFIG_EXT4_FS=y -CONFIG_XFS_FS=m -CONFIG_VFAT_FS=y -CONFIG_TMPFS=y -CONFIG_TMPFS_POSIX_ACL=y -CONFIG_PROC_FS=y -CONFIG_SYSFS=y -CONFIG_OVERLAY_FS=y -CONFIG_SQUASHFS=y -CONFIG_SQUASHFS_XATTR=y -CONFIG_SQUASHFS_LZ4=y -CONFIG_SQUASHFS_LZO=y -CONFIG_SQUASHFS_XZ=y -CONFIG_NFS_FS=m -CONFIG_NFS_V4=m -CONFIG_NFS_V4_1=y -CONFIG_NFS_V4_2=y -CONFIG_FUSE_FS=y -CONFIG_CUSE=y -CONFIG_EFIVAR_FS=y -CONFIG_ISO9660_FS=y -CONFIG_JOLIET=y -CONFIG_UDF_FS=y -CONFIG_MSDOS_FS=y -CONFIG_FAT_DEFAULT_IOCHARSET="utf8" -CONFIG_NTFS_FS=m -CONFIG_TMPFS_XATTR=y -CONFIG_NLS_CODEPAGE_437=y -CONFIG_NLS_ASCII=y -CONFIG_NLS_ISO8859_1=y -CONFIG_NLS_UTF8=y - -# --- Virtio (built-in for reliable boot) --- -CONFIG_VIRTIO=y -CONFIG_VIRTIO_PCI=y -CONFIG_VIRTIO_BLK=y -CONFIG_VIRTIO_CONSOLE=y -CONFIG_VIRTIO_BALLOON=y -CONFIG_VIRTIO_INPUT=y -CONFIG_VIRTIO_MMIO=y -CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y -CONFIG_HW_RANDOM_VIRTIO=y - -# --- Console / Serial --- -CONFIG_SERIAL_AMBA_PL011=y -CONFIG_SERIAL_AMBA_PL011_CONSOLE=y -CONFIG_SERIAL_8250=y -CONFIG_SERIAL_8250_CONSOLE=y -CONFIG_VT=y -CONFIG_VT_CONSOLE=y -CONFIG_HW_CONSOLE=y -CONFIG_DUMMY_CONSOLE=y - -# --- Security --- -CONFIG_SECCOMP=y -CONFIG_SECCOMP_FILTER=y -CONFIG_SECURITY=y -CONFIG_KEYS=y - -# --- Crypto --- -CONFIG_CRYPTO=y -CONFIG_CRYPTO_AES=y -CONFIG_CRYPTO_SHA256=y -CONFIG_CRYPTO_SHA512=y -CONFIG_CRYPTO_XTS=y -CONFIG_ARM64_CRYPTO=y -CONFIG_CRYPTO_AES_ARM64_CE=y -CONFIG_CRYPTO_SHA2_ARM64_CE=y - -# --- BPF --- -CONFIG_BPF=y -CONFIG_BPF_SYSCALL=y -CONFIG_BPF_JIT=y -CONFIG_BPF_JIT_ALWAYS_ON=y -CONFIG_XDP_SOCKETS=y - -# --- ARM64 SoC/platform support --- -CONFIG_ARCH_BCM2835=y -CONFIG_ARCH_ROCKCHIP=y -CONFIG_ARCH_MESON=y -CONFIG_ARCH_SUNXI=y -CONFIG_ARCH_TEGRA=y -CONFIG_ARCH_MVEBU=y -CONFIG_ARCH_HISI=y -CONFIG_ARCH_QCOM=y -CONFIG_ARCH_EXYNOS=y -CONFIG_ARCH_VEXPRESS=y -CONFIG_ARCH_XGENE=y -CONFIG_ARCH_THUNDER=y -CONFIG_ARCH_THUNDER2=y -CONFIG_ARCH_SEATTLE=y -CONFIG_ARCH_SYNQUACER=y -CONFIG_ARCH_UNIPHIER=y -# ACPI / UEFI -CONFIG_ACPI=y -CONFIG_ACPI_DOCK=y -CONFIG_ACPI_IPMI=y -CONFIG_ACPI_APEI=y -CONFIG_ACPI_APEI_GHES=y -CONFIG_IPMI_HANDLER=m -CONFIG_IPMI_DMI_DECODE=y -CONFIG_IPMI_DEVICE_INTERFACE=m -CONFIG_IPMI_SI=m -CONFIG_PCI=y -CONFIG_PCIEPORTBUS=y -CONFIG_HOTPLUG_PCI=y -CONFIG_PCI_HOST_GENERIC=y -CONFIG_PCIE_ROCKCHIP_HOST=y -CONFIG_PCIE_ARMADA_8K=y -CONFIG_PCIE_TEGRA194_HOST=y -# Broadcom STB PCIe controller (Raspberry Pi 4B — needed for VL805 USB 3.0) -CONFIG_PCIE_BRCMSTB=y -CONFIG_PCIE_ALTERA=y -CONFIG_PCI_AARDVARK=y -CONFIG_PCI_TEGRA=y -CONFIG_PCI_XGENE=y -CONFIG_PCI_HOST_THUNDER_PEM=y -CONFIG_PCI_HOST_THUNDER_ECAM=y -CONFIG_PCIE_QCOM=y -CONFIG_PCIE_AL=y -CONFIG_PCI_MESON=y -CONFIG_PCI_HISI=y -CONFIG_PCIE_KIRIN=y -CONFIG_PCIE_HISI_STB=y -CONFIG_PCI_IOV=y -CONFIG_PCI_PASID=y -CONFIG_HOTPLUG_PCI_ACPI=y -# IOMMU -CONFIG_ARM_SMMU=y -CONFIG_ARM_SMMU_V3=y -CONFIG_FW_LOADER=y -# Raspberry Pi firmware mailbox (required for GENET MAC address, clocks, etc.) -CONFIG_RASPBERRYPI_FIRMWARE=y -CONFIG_RASPBERRYPI_POWER=y -CONFIG_ARM_SCMI_PROTOCOL=y -CONFIG_ARM_SCPI_PROTOCOL=y -CONFIG_OF=y -CONFIG_USE_OF=y -# RTC -CONFIG_RTC_CLASS=y -CONFIG_RTC_DRV_EFI=y -# DMA -CONFIG_DMADEVICES=y -CONFIG_PL330_DMA=y -CONFIG_PRINTK=y -CONFIG_PRINTK_TIME=y -CONFIG_MODULES=y -CONFIG_MODULE_UNLOAD=y -CONFIG_PANIC_TIMEOUT=10 -CONFIG_PANIC_ON_OOPS=y -CONFIG_MAGIC_SYSRQ=y -CONFIG_RD_GZIP=y -CONFIG_RD_XZ=y -CONFIG_RD_ZSTD=y -CONFIG_UEVENT_HELPER=y -CONFIG_DEVTMPFS=y -CONFIG_DEVTMPFS_MOUNT=y -CONFIG_CONNECTOR=y -CONFIG_DMI_SYSFS=y -CONFIG_EFI=y -CONFIG_EFI_STUB=y - -# --- Framebuffer/Console (required for EFI boot video output) --- -CONFIG_DRM=y -CONFIG_DRM_SIMPLEDRM=y -CONFIG_DRM_VIRTIO_GPU=y -CONFIG_FB=y -CONFIG_FB_EFI=y -CONFIG_FRAMEBUFFER_CONSOLE=y -CONFIG_SYSFB_SIMPLEFB=y -# Virtualization -CONFIG_VIRTUALIZATION=y -CONFIG_KVM=y -CONFIG_VFIO=y -CONFIG_VFIO_PCI=y -CONFIG_VHOST_NET=m -CONFIG_VHOST_VSOCK=m - -# --- Module compression --- -CONFIG_MODULE_COMPRESS_ZSTD=y -CONFIG_MODULE_DECOMPRESS=y - -# --- I2C (SBC board management, EEPROM, sensors) --- -CONFIG_I2C=y -CONFIG_I2C_CHARDEV=y -CONFIG_I2C_MV64XXX=y -CONFIG_I2C_RK3X=y -CONFIG_I2C_TEGRA=y -CONFIG_I2C_DESIGNWARE_PLATFORM=y -# SPI -CONFIG_SPI=y -CONFIG_SPI_ROCKCHIP=y -CONFIG_SPI_PL022=y -CONFIG_SPI_ORION=y -# GPIO -CONFIG_GPIOLIB=y -# Hardware watchdog -CONFIG_WATCHDOG=y -CONFIG_SOFTLOCKUP_DETECTOR=y -CONFIG_WQ_WATCHDOG=y -# Hardware RNG -CONFIG_HW_RANDOM=y -CONFIG_HW_RANDOM_TIMERIOMEM=y -# TPM -CONFIG_TCG_TIS=y -CONFIG_TCG_FTPM_TEE=y -CONFIG_TEE=y -CONFIG_OPTEE=y - -# --- Disable unnecessary subsystems --- -# CONFIG_SOUND is not set -# CONFIG_WIRELESS is not set -# CONFIG_WLAN is not set -# CONFIG_BLUETOOTH is not set -# CONFIG_NFC is not set -# CONFIG_INFINIBAND is not set -# CONFIG_MEDIA_SUPPORT is not set -CONFIG_INPUT_EVDEV=y -# CONFIG_INPUT_JOYSTICK is not set -# CONFIG_INPUT_TOUCHSCREEN is not set diff --git a/mkosi.postinst b/mkosi.postinst deleted file mode 100755 index 6ce6f597..00000000 --- a/mkosi.postinst +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# mkosi.postinst — Post-installation script for the CaptainOS initrd. -# Runs inside the image chroot after packages are installed. -# NOTE: The filesystem is mostly read-only here. Use mkosi.extra/ for -# creating directories and dropping config files. -set -euo pipefail - -echo "==> CaptainOS post-install: configuring services..." - -# Ensure CA certificate bundle is generated (dpkg triggers may not -# fire reliably in mkosi's chroot). -update-ca-certificates --fresh 2>/dev/null || true - -# Enable core services -for unit in systemd-networkd systemd-resolved systemd-timesyncd containerd captainos-banner \ - captainos-static-network tink-agent-setup tink-agent rsyslog rsyslog-hostname-reload.path; do - systemctl enable "$unit" 2>/dev/null || true -done - -# Root password is set via mkosi.conf RootPassword= setting - -# Disable unnecessary systemd units to speed up boot -for unit in \ - apt-daily.timer \ - apt-daily-upgrade.timer \ - e2scrub_all.timer \ - e2scrub_reap.service \ - fstrim.timer \ - logrotate.timer \ - man-db.timer \ - remote-fs.target \ - systemd-firstboot.service; do - systemctl disable "$unit" 2>/dev/null || true - systemctl mask "$unit" 2>/dev/null || true -done - -# Set default target to multi-user (no graphical) -systemctl set-default multi-user.target 2>/dev/null || true - -echo "==> CaptainOS post-install complete." diff --git a/pyproject.toml b/pyproject.toml index 17aa984a..f4330ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,23 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] version = "0.0.1" name = "captain" requires-python = ">=3.13" -dependencies = ["configargparse>=1.7", "rich>=14.3.3"] +dependencies = [ + "click>=8.1", + "rich>=15.0", + "jinja2>=3.1", + "trogon>=0.6", +] + +[project.scripts] +captain = "build:main" [project.optional-dependencies] -dev = ["ruff>=0.9", "pyright>=1.1"] +dev = ["ruff>=0.15", "pyright>=1.1"] [tool.ruff] target-version = "py310" @@ -28,5 +40,5 @@ select = [ [tool.pyright] pythonVersion = "3.13" typeCheckingMode = "standard" -exclude = ["mkosi.tools", "mkosi.output", "__pycache__"] +exclude = ["mkosi.tools", "mkosi.output", "mkosi.input", "__pycache__"] include = ["captain", "build.py"] diff --git a/uv.lock b/uv.lock index b436ceb9..e0f3177f 100644 --- a/uv.lock +++ b/uv.lock @@ -5,10 +5,12 @@ requires-python = ">=3.13" [[package]] name = "captain" version = "0.0.1" -source = { virtual = "." } +source = { editable = "." } dependencies = [ - { name = "configargparse" }, + { name = "click" }, + { name = "jinja2" }, { name = "rich" }, + { name = "trogon" }, ] [package.optional-dependencies] @@ -19,32 +21,139 @@ dev = [ [package.metadata] requires-dist = [ - { name = "configargparse", specifier = ">=1.7" }, + { name = "click", specifier = ">=8.1" }, + { name = "jinja2", specifier = ">=3.1" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1" }, - { name = "rich", specifier = ">=14.3.3" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.9" }, + { name = "rich", specifier = ">=15.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15" }, + { name = "trogon", specifier = ">=0.6" }, ] provides-extras = ["dev"] [[package]] -name = "configargparse" -version = "1.7.5" +name = "click" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/0b/30328302903c55218ffc5199646d0e9d28348ff26c02ba77b2ffc58d294a/configargparse-1.7.5.tar.gz", hash = "sha256:e3f9a7bb6be34d66b2e3c4a2f58e3045f8dfae47b0dc039f87bcfaa0f193fb0f", size = 53548, upload-time = "2026-03-11T02:19:38.144Z" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/19/3ba5e1b0bcc7b91aeab6c258afd70e4907d220fed3972febe38feb40db30/configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592", size = 27692, upload-time = "2026-03-11T02:19:36.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -65,6 +174,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "platformdirs" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/cd/4f25b2f95b23f5d2c9c1fe43e49841bff5800562149b2666afc09309aa8f/platformdirs-4.10.1.tar.gz", hash = "sha256:ceab4084426fe6319ce18e86deada8ab1b7487c7aee7040c55e277c9ae793695", size = 31678, upload-time = "2026-07-18T03:53:43.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/73/6fd0bb9ce84138c3857f12e9de63bc901852975a092d545f18087a204aa2/platformdirs-4.10.1-py3-none-any.whl", hash = "sha256:0e4eff26be2d75293977f7cddc153fd9b8eaa7fb0c7b64ffe4076cb443117443", size = 22906, upload-time = "2026-07-18T03:53:42.576Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -76,15 +194,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.408" +version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [[package]] @@ -102,34 +220,73 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, - { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, - { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, - { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, - { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, - { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, - { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, - { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, - { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, - { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, - { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, +version = "0.15.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, +] + +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + +[[package]] +name = "trogon" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/ae/7367acac2194a215b092ba3fccde3b558272702110b8bb9bea164ab4ac42/trogon-0.6.0.tar.gz", hash = "sha256:fd1abfeb7b15d79d6e6cfc9e724aad2a2728812e4713a744d975f133e7ec73a4", size = 22902, upload-time = "2024-10-02T13:38:12.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/30/33035d5796a3b8b9624997fec7545e3febd2268c7b48df38a715a95cb5e4/trogon-0.6.0-py3-none-any.whl", hash = "sha256:fb5b6c25acd7a0eaba8d2cd32a57f1d80c26413cea737dad7a4eebcda56060e0", size = 26077, upload-time = "2024-10-02T13:38:11.09Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ]