From 280874f5642d50d96d905f9846cabe9c58189773 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 27 Jul 2026 11:24:53 +0300 Subject: [PATCH] Build Fedora aarch64 images for the Lenovo Yoga C630 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assembles a ready-to-write disk image via Gitea Actions. Mainline has carried sdm850-lenovo-yoga-c630.dts since 5.5 and Fedora ships it in kernel-core, so unlike aarch64-laptops/build there is no kernel or GRUB to compile — what is left is producing an image that boots on firmware which hands Linux no device tree. The build runs in an aarch64 container under qemu-user and builds filesystems from directory trees with mke2fs -d and mcopy rather than mounting loop devices, so it works on runners that will not hand out /dev/loop-control. A kernel-install hook writes the devicetree line into each BLS entry; without it the first `dnf update kernel` would produce an unbootable system. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS --- .gitea/workflows/build-image.yaml | 122 +++++++ .gitea/workflows/probe-runner.yaml | 44 +++ .gitignore | 12 + README.md | 94 +++++ build/build-image.sh | 116 ++++++ build/stage2.sh | 337 ++++++++++++++++++ config/device.env | 50 +++ config/packages/base.pkgs | 65 ++++ config/packages/minimal.pkgs | 12 + config/packages/workstation.pkgs | 7 + docs/firmware.md | 91 +++++ docs/install.md | 97 +++++ docs/runner-setup.md | 61 ++++ firmware/local/.gitkeep | 0 overlay/etc/default/grub.in | 13 + overlay/etc/dracut.conf.d/10-c630.conf | 12 + .../install.d/95-c630-devicetree.install.in | 47 +++ overlay/etc/motd.d/c630.in | 7 + .../etc/systemd/system/c630-growfs.service | 15 + overlay/usr/local/sbin/c630-firmware | 49 +++ overlay/usr/local/sbin/c630-growfs | 43 +++ 21 files changed, 1294 insertions(+) create mode 100644 .gitea/workflows/build-image.yaml create mode 100644 .gitea/workflows/probe-runner.yaml create mode 100644 .gitignore create mode 100644 README.md create mode 100755 build/build-image.sh create mode 100755 build/stage2.sh create mode 100644 config/device.env create mode 100644 config/packages/base.pkgs create mode 100644 config/packages/minimal.pkgs create mode 100644 config/packages/workstation.pkgs create mode 100644 docs/firmware.md create mode 100644 docs/install.md create mode 100644 docs/runner-setup.md create mode 100644 firmware/local/.gitkeep create mode 100644 overlay/etc/default/grub.in create mode 100644 overlay/etc/dracut.conf.d/10-c630.conf create mode 100755 overlay/etc/kernel/install.d/95-c630-devicetree.install.in create mode 100644 overlay/etc/motd.d/c630.in create mode 100644 overlay/etc/systemd/system/c630-growfs.service create mode 100755 overlay/usr/local/sbin/c630-firmware create mode 100755 overlay/usr/local/sbin/c630-growfs diff --git a/.gitea/workflows/build-image.yaml b/.gitea/workflows/build-image.yaml new file mode 100644 index 0000000..283ad2e --- /dev/null +++ b/.gitea/workflows/build-image.yaml @@ -0,0 +1,122 @@ +name: build image + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + workflow_dispatch: + inputs: + variant: + description: which variant to build + type: choice + default: minimal + options: [minimal, workstation, both] + +jobs: + # Which variants to build. Pushes and PRs build minimal only — it is the fast + # one and it exercises the entire pipeline. Tags build everything. + prepare: + runs-on: metal + outputs: + variants: ${{ steps.pick.outputs.variants }} + steps: + - id: pick + run: | + if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then + variants='["minimal","workstation"]' + else + case "${{ inputs.variant }}" in + both) variants='["minimal","workstation"]' ;; + workstation) variants='["workstation"]' ;; + *) variants='["minimal"]' ;; + esac + fi + echo "variants=$variants" | tee -a "$GITHUB_OUTPUT" + + build: + needs: prepare + runs-on: metal + timeout-minutes: 600 + strategy: + fail-fast: false + matrix: + variant: ${{ fromJSON(needs.prepare.outputs.variants) }} + + steps: + - uses: actions/checkout@v4 + + - name: Report runner + run: | + echo "host arch : $(uname -m)" + echo "kernel : $(uname -r)" + echo "user : $(id -un) (uid $(id -u))" + echo "podman : $(podman --version 2>/dev/null || echo MISSING)" + df -h . + + # Building an aarch64 root filesystem means executing aarch64 rpm + # scriptlets, which needs a binfmt_misc handler in the host kernel. A + # container cannot register one for itself. + - name: Ensure aarch64 emulation + run: | + handler=/proc/sys/fs/binfmt_misc/qemu-aarch64 + if [ ! -e "$handler" ]; then + echo "no aarch64 binfmt handler, attempting to install one" + sudo dnf install -y qemu-user-static-aarch64 + sudo systemctl restart systemd-binfmt + fi + if [ ! -e "$handler" ]; then + echo "::error::aarch64 emulation is unavailable on this runner." \ + "See docs/runner-setup.md for the one-time host setup." + exit 1 + fi + cat "$handler" + + - name: Build + run: | + case "${{ matrix.variant }}" in + workstation) size=16384 ;; + *) size=8192 ;; + esac + ./build/build-image.sh --variant "${{ matrix.variant }}" --size "$size" + + - name: Checksums + run: cat output/*.sha256 + + - uses: actions/upload-artifact@v4 + with: + name: fedora-${{ matrix.variant }}-lenovo-yoga-c630 + path: | + output/*.img.zst + output/*.sha256 + retention-days: 14 + compression-level: 0 # already zstd + + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/') + env: + TOKEN: ${{ github.token }} + SERVER: ${{ github.server_url }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + api="$SERVER/api/v1/repos/$REPO" + auth="Authorization: token $TOKEN" + # python3 rather than jq — jq is not guaranteed on a Fedora Server runner. + field() { python3 -c 'import json,sys; print(json.load(sys.stdin).get(sys.argv[1],""))' "$1"; } + + id=$(curl -sf -H "$auth" "$api/releases/tags/$TAG" | field id || true) + if [ -z "${id:-}" ]; then + id=$(curl -sf -X POST -H "$auth" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}" \ + "$api/releases" | field id) + fi + echo "release id: $id" + + for f in output/*.img.zst output/*.sha256; do + echo "uploading $(basename "$f")" + curl -sf -X POST -H "$auth" \ + -F "attachment=@${f}" \ + "$api/releases/$id/assets?name=$(basename "$f")" >/dev/null + done diff --git a/.gitea/workflows/probe-runner.yaml b/.gitea/workflows/probe-runner.yaml new file mode 100644 index 0000000..54af7cf --- /dev/null +++ b/.gitea/workflows/probe-runner.yaml @@ -0,0 +1,44 @@ +name: probe runner + +# Run this once by hand to find out what the runners can actually do before +# blaming the build for failing. It changes nothing. +on: + workflow_dispatch: + +jobs: + probe: + runs-on: metal + steps: + - name: Identity and privileges + run: | + echo "runner name : ${RUNNER_NAME:-?}" + echo "arch : $(uname -m)" + echo "kernel : $(uname -r)" + echo "os : $(. /etc/os-release && echo "$PRETTY_NAME")" + echo "user : $(id -un) uid=$(id -u)" + echo -n "passwordless sudo : " + sudo -n true 2>/dev/null && echo yes || echo no + + - name: Container tooling + run: | + echo -n "podman : "; podman --version 2>/dev/null || echo MISSING + echo -n "rootless: "; podman info --format '{{.Host.Security.Rootless}}' 2>/dev/null || echo '?' + echo "storage : $(podman info --format '{{.Store.GraphRoot}}' 2>/dev/null || echo '?')" + + - name: Emulation + run: | + if [ -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then + cat /proc/sys/fs/binfmt_misc/qemu-aarch64 + else + echo "no aarch64 binfmt handler registered" + fi + + - name: Can we actually run an aarch64 container? + continue-on-error: true + run: | + podman run --rm --arch arm64 registry.fedoraproject.org/fedora:44 \ + bash -c 'echo "inside: $(uname -m)"; rpm --eval %{_arch}' + + - name: Disk space + run: | + df -h / /var /tmp . 2>/dev/null | sort -u diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf5ad4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Build output +/output/ +*.img +*.img.zst +*.raw +*.sha256 + +# Firmware extracted from a Windows install is signed per-model and is not +# ours to redistribute — this repo is public. Drop blobs in firmware/local/ to +# have the build bake them in, but they stay out of git. +/firmware/local/* +!/firmware/local/.gitkeep diff --git a/README.md b/README.md new file mode 100644 index 0000000..352311d --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Fedora for the Lenovo Yoga C630 13Q50 + +Builds a ready-to-write Fedora aarch64 disk image for the Lenovo Yoga C630 +(model 81JL, Qualcomm SDM850), using Gitea Actions. + +The approach is borrowed from [aarch64-laptops/build][aal], but the heavy +lifting that project had to do in 2019 is now unnecessary: mainline Linux has +carried `sdm850-lenovo-yoga-c630.dts` since 5.5, and Fedora ships it in +`kernel-core`. There is no kernel to patch and no GRUB to compile. What is left +is assembling a disk image that boots on hardware whose firmware hands Linux no +device tree. + +[aal]: https://github.com/aarch64-laptops/build + +## What you get + +`output/fedora-44--lenovo-yoga-c630--.img.zst` — a GPT disk +image with an ESP, a `/boot` partition and an ext4 root. Decompress, write it to +a USB stick or microSD card, and boot. The root filesystem grows to fill the +medium on first boot. + +Two variants: + +| Variant | Size | Contents | +|---------------|-------|----------------------------------------------| +| `minimal` | 8 GiB | Console, sshd, and enough tools to debug the machine | +| `workstation` | 16 GiB| GNOME desktop | + +Default login is `fedora` / `fedora`, and the password must be changed at first +login. Root is locked. + +## Hardware status + +| Works out of the box | Needs firmware from Windows | Not supported | +|---------------------------------|-----------------------------|---------------| +| UFS storage, USB, keyboard, touchpad, touchscreen | Audio | LTE modem | +| WiFi + Bluetooth (ath10k WCN3990) | Sensor hub — lid switch, accelerometer, auto-rotate | | +| Graphics (freedreno, Adreno 630) | Hardware video decode (venus) | | +| Battery and charging | | | + +Qualcomm allows the Adreno and ath10k firmware to be redistributed, so Fedora +ships it and this image includes it. The SDM850 DSP and display blobs are signed +per model and exist only in your machine's Windows partition. Run +`sudo c630-firmware` once after installing — see [docs/firmware.md](docs/firmware.md). + +## Building + +CI does this on every push to `main`. To run it yourself: + +```sh +./build/build-image.sh --variant minimal +``` + +You need `podman` and, on an x86_64 host, aarch64 emulation: + +```sh +sudo dnf install -y qemu-user-static-aarch64 +sudo systemctl restart systemd-binfmt +``` + +`build/build-image.sh` runs on the host and only sets up the container. +`build/stage2.sh` runs inside an aarch64 Fedora container and does everything +else: `dnf --installroot`, the overlay, dracut, and the disk assembly. It builds +filesystems from directory trees with `mke2fs -d` and `mcopy` rather than +mounting loop devices, so it does not need `/dev/loop-control` — which CI +runners generally will not hand out. + +## Repository layout + +``` +config/device.env C630 parameters: DTB path, kernel command line, geometry +config/packages/*.pkgs Package lists — base plus one file per variant +overlay/ Files copied into the root filesystem (*.in are templated) +build/build-image.sh Host driver: emulation checks, podman invocation +build/stage2.sh The actual build, inside an aarch64 container +firmware/local/ Optional drop-in for firmware you extracted yourself (gitignored) +.gitea/workflows/ CI +docs/ Installation, firmware, runner setup +``` + +## Documentation + +- [docs/install.md](docs/install.md) — writing the image and booting the laptop +- [docs/firmware.md](docs/firmware.md) — what needs extracting from Windows and why +- [docs/runner-setup.md](docs/runner-setup.md) — one-time Gitea runner preparation + +## Caveats + +The build has not yet been confirmed to boot on real hardware. The device tree, +kernel command line and firmware layout are taken from Fedora's Snapdragon WoA +documentation and the aarch64-laptops project; the parts specific to the C630's +older SDM850 are reasoned from those rather than tested. Expect to spend a boot +or two adjusting `DEVICE_CMDLINE` in `config/device.env`. Findings belong in +this README. diff --git a/build/build-image.sh b/build/build-image.sh new file mode 100755 index 0000000..edc9942 --- /dev/null +++ b/build/build-image.sh @@ -0,0 +1,116 @@ +#!/usr/bin/bash +# +# Host-side driver. Runs on an x86_64 CI runner (or your workstation) and does +# the real work inside an aarch64 Fedora container under qemu-user emulation. +# +# ./build/build-image.sh --variant minimal +# +# Everything arch-specific happens in build/stage2.sh, which runs inside that +# container. This script's only jobs are checking that emulation is wired up +# and handing the container the right mounts. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_DIR" + +VARIANT=minimal +OUTPUT_DIR="$REPO_DIR/output" +CONTAINER_IMAGE="" +KEEP_ROOTFS=0 + +usage() { + cat <&2; usage >&2; exit 2 ;; + esac +done + +# shellcheck source=../config/device.env +source "$REPO_DIR/config/device.env" + +: "${CONTAINER_IMAGE:=registry.fedoraproject.org/fedora:${FEDORA_RELEASE}}" + +if [ ! -f "$REPO_DIR/config/packages/${VARIANT}.pkgs" ]; then + echo "no such variant: ${VARIANT}" >&2 + echo "available: $(cd "$REPO_DIR/config/packages" && ls *.pkgs | sed 's/\.pkgs$//' | grep -v '^base$' | tr '\n' ' ')" >&2 + exit 2 +fi + +# --- emulation check ---------------------------------------------------- +# +# Building an aarch64 rootfs means running aarch64 rpm scriptlets, which needs +# a binfmt_misc handler registered in the *host* kernel. A container cannot +# provide that for itself. +if [ "$(uname -m)" != "$TARGET_ARCH" ]; then + handler=/proc/sys/fs/binfmt_misc/qemu-aarch64 + if [ ! -e "$handler" ]; then + cat >&2 <&2 + echo " interpreter will not be visible inside the container." >&2 + echo " Install qemu-user-static-aarch64 rather than qemu-user." >&2 + exit 1 + fi +fi + +command -v podman >/dev/null || { echo "error: podman not found" >&2; exit 1; } + +mkdir -p "$OUTPUT_DIR" + +BUILD_REF="$(git -C "$REPO_DIR" rev-parse --short HEAD 2>/dev/null || echo unknown)" +BUILD_DATE="$(date -u +%Y-%m-%d)" + +echo "==> variant=${VARIANT} release=${FEDORA_RELEASE} arch=${TARGET_ARCH} ref=${BUILD_REF}" +echo "==> build container: ${CONTAINER_IMAGE}" + +# --privileged is what lets stage2 bind-mount /proc and /sys into the staged +# rootfs so dracut can run in a chroot. Rootless podman grants only the caps +# the invoking user already has inside their user namespace, so this is not +# the escalation it looks like. +exec podman run --rm \ + --arch arm64 \ + --privileged \ + --security-opt label=disable \ + -v "$REPO_DIR:/src:ro" \ + -v "$OUTPUT_DIR:/out" \ + -e VARIANT="$VARIANT" \ + -e FEDORA_RELEASE="$FEDORA_RELEASE" \ + -e IMAGE_SIZE_MIB="${IMAGE_SIZE_MIB}" \ + -e BUILD_REF="$BUILD_REF" \ + -e BUILD_DATE="$BUILD_DATE" \ + -e KEEP_ROOTFS="$KEEP_ROOTFS" \ + "$CONTAINER_IMAGE" \ + /bin/bash /src/build/stage2.sh diff --git a/build/stage2.sh b/build/stage2.sh new file mode 100755 index 0000000..1d8ed59 --- /dev/null +++ b/build/stage2.sh @@ -0,0 +1,337 @@ +#!/usr/bin/bash +# +# Runs inside an aarch64 Fedora container (see build/build-image.sh). +# +# Stages a Fedora root filesystem with dnf, applies the C630 overlay, then +# assembles a GPT disk image. Deliberately avoids loop devices: filesystems are +# built from directory trees with `mke2fs -d` and `mcopy`, then dd'd into a +# partitioned sparse file. That keeps the whole thing working on CI runners +# where /dev/loop-control is not available to the job. + +set -euo pipefail + +SRC=/src +OUT=/out +WORK="$OUT/.work" +ROOTFS="$WORK/rootfs" + +# shellcheck source=../config/device.env +source "$SRC/config/device.env" + +: "${VARIANT:=minimal}" +: "${BUILD_REF:=unknown}" +: "${BUILD_DATE:=unknown}" +: "${KEEP_ROOTFS:=0}" + +IMAGE_NAME="fedora-${FEDORA_RELEASE}-${VARIANT}-${DEVICE_NAME}-${BUILD_DATE}-${BUILD_REF}" +IMAGE_PATH="$OUT/${IMAGE_NAME}.img" + +log() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; } + +MOUNTED=() +unbind_all() { + local i + for (( i=${#MOUNTED[@]}-1; i>=0; i-- )); do + umount "${MOUNTED[i]}" 2>/dev/null || umount -l "${MOUNTED[i]}" 2>/dev/null || true + done + MOUNTED=() +} +trap unbind_all EXIT + +bind() { mount --bind "$1" "$2" && MOUNTED+=("$2"); } + +# --------------------------------------------------------------------------- +log "Installing build tooling into the container" +# --------------------------------------------------------------------------- +dnf -y install --setopt=install_weak_deps=False \ + e2fsprogs dosfstools mtools gdisk util-linux rsync zstd findutils \ + >/dev/null + +rm -rf "$WORK" +mkdir -p "$ROOTFS" "$WORK/esp" + +# --------------------------------------------------------------------------- +log "Resolving package list (base + ${VARIANT})" +# --------------------------------------------------------------------------- +read_pkgs() { + # Strip comments and blank lines. '@^env' is dnf4 spelling for an + # environment group; dnf5 wants a plain '@env'. + sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e '/^$/d' \ + -e 's/^@\^/@/' "$1" +} + +mapfile -t PACKAGES < <( + read_pkgs "$SRC/config/packages/base.pkgs" + read_pkgs "$SRC/config/packages/${VARIANT}.pkgs" +) +echo "${#PACKAGES[@]} package specs" + +# --------------------------------------------------------------------------- +log "Seeding repository configuration into the install root" +# --------------------------------------------------------------------------- +# dnf reads its repo definitions from inside --installroot. Seed them from the +# container (same release, same arch) so the first transaction has somewhere to +# fetch from and something to verify signatures against. The fedora-repos +# package overwrites these with its own copies during the transaction. +mkdir -p "$ROOTFS/etc/yum.repos.d" "$ROOTFS/etc/pki/rpm-gpg" "$ROOTFS/etc/dnf" +cp -a /etc/yum.repos.d/. "$ROOTFS/etc/yum.repos.d/" +cp -a /etc/pki/rpm-gpg/. "$ROOTFS/etc/pki/rpm-gpg/" +if [ -d /etc/dnf/vars ]; then cp -a /etc/dnf/vars "$ROOTFS/etc/dnf/"; fi + +# --------------------------------------------------------------------------- +log "Installing Fedora ${FEDORA_RELEASE} (${TARGET_ARCH}) — this is the slow part" +# --------------------------------------------------------------------------- +dnf -y \ + --installroot="$ROOTFS" \ + --releasever="$FEDORA_RELEASE" \ + --setopt=keepcache=0 \ + --setopt=install_weak_deps=True \ + install "${PACKAGES[@]}" + +KVER="$(rpm --root "$ROOTFS" -q kernel-core --qf '%{VERSION}-%{RELEASE}.%{ARCH}\n' \ + | sort -V | tail -1)" +[ -n "$KVER" ] || { echo "could not determine installed kernel version" >&2; exit 1; } +echo "kernel: $KVER" + +if [ ! -e "$ROOTFS/boot/dtb-${KVER}/${DEVICE_DTB}" ]; then + echo "error: ${DEVICE_DTB} is not in this kernel's device trees." >&2 + echo " Check DEVICE_DTB in config/device.env against:" >&2 + ls "$ROOTFS/boot/dtb-${KVER}/qcom/" | grep -i yoga >&2 || true + exit 1 +fi + +# --------------------------------------------------------------------------- +log "Applying overlay" +# --------------------------------------------------------------------------- +render() { + sed -e "s|@DEVICE_DTB@|${DEVICE_DTB}|g" \ + -e "s|@DEVICE_CMDLINE@|${DEVICE_CMDLINE}|g" \ + -e "s|@DEVICE_DESC@|${DEVICE_DESC}|g" \ + -e "s|@DEVICE_NAME@|${DEVICE_NAME}|g" \ + -e "s|@FEDORA_RELEASE@|${FEDORA_RELEASE}|g" \ + -e "s|@BUILD_REF@|${BUILD_REF}|g" \ + -e "s|@BUILD_DATE@|${BUILD_DATE}|g" +} + +while IFS= read -r rel; do + src="$SRC/overlay/$rel" + dst="$ROOTFS/$rel" + if [[ "$rel" == *.in ]]; then + dst="${dst%.in}" + mkdir -p "$(dirname "$dst")" + render < "$src" > "$dst" + else + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + fi + chmod --reference="$src" "$dst" +done < <(cd "$SRC/overlay" && find . -type f -printf '%P\n') + +# Locally-supplied firmware, if the operator dropped any in. Contents mirror +# /usr/lib/firmware/updates/ and are gitignored — see docs/firmware.md. +if compgen -G "$SRC/firmware/local/*" >/dev/null; then + log "Baking in firmware from firmware/local/" + mkdir -p "$ROOTFS/usr/lib/firmware/updates" + rsync -a --exclude=.gitkeep "$SRC/firmware/local/" \ + "$ROOTFS/usr/lib/firmware/updates/" + find "$ROOTFS/usr/lib/firmware/updates" -type f -printf ' %P\n' +fi + +# --------------------------------------------------------------------------- +log "Generating identifiers and filesystem tables" +# --------------------------------------------------------------------------- +ROOT_UUID="$(uuidgen)" +BOOT_UUID="$(uuidgen)" +ESP_ID="$(od -An -tx1 -N4 /dev/urandom | tr -d ' \n' | tr 'a-f' 'A-F')" +ESP_UUID="${ESP_ID:0:4}-${ESP_ID:4:4}" + +cat > "$ROOTFS/etc/fstab" < "$ROOTFS/etc/machine-id" + +ln -sf ../run/systemd/resolve/stub-resolv.conf "$ROOTFS/etc/resolv.conf" +echo "$DEVICE_NAME" > "$ROOTFS/etc/hostname" + +# mke2fs -d does not reliably carry SELinux labels across, and the builder has +# no policy loaded anyway. Relabel on first boot. +: > "$ROOTFS/.autorelabel" + +# --------------------------------------------------------------------------- +log "Configuring the target system" +# --------------------------------------------------------------------------- +bind /proc "$ROOTFS/proc" +bind /sys "$ROOTFS/sys" +bind /dev "$ROOTFS/dev" +bind /dev/pts "$ROOTFS/dev/pts" + +chroot "$ROOTFS" useradd -m -G wheel -s /bin/bash "$DEFAULT_USER" +echo "${DEFAULT_USER}:${DEFAULT_PASSWORD}" | chroot "$ROOTFS" chpasswd +chroot "$ROOTFS" chage -d 0 "$DEFAULT_USER" # force a change at first login +chroot "$ROOTFS" passwd -l root + +chroot "$ROOTFS" systemctl enable \ + c630-growfs.service \ + sshd.service \ + NetworkManager.service \ + systemd-resolved.service + +if [ "$VARIANT" = workstation ]; then + chroot "$ROOTFS" systemctl set-default graphical.target +else + chroot "$ROOTFS" systemctl set-default multi-user.target +fi + +# --------------------------------------------------------------------------- +log "Building initramfs for ${KVER}" +# --------------------------------------------------------------------------- +chroot "$ROOTFS" dracut --force --no-hostonly --no-hostonly-cmdline \ + "/boot/initramfs-${KVER}.img" "$KVER" + +# --------------------------------------------------------------------------- +log "Writing bootloader configuration" +# --------------------------------------------------------------------------- +# The bootstrap BLS entry. Subsequent kernels get theirs from kernel-install, +# with the devicetree line supplied by 95-c630-devicetree.install. +mkdir -p "$ROOTFS/boot/loader/entries" +cat > "$ROOTFS/boot/loader/entries/c630-${KVER}.conf" < "$ROOTFS/boot/grub2/grub.cfg" < "$ROOTFS/boot/grub2/grubenv" + +# ESP: GRUB at the removable-media path, because that is the only thing the +# C630's firmware will find on a freshly written USB stick or SD card, and a +# one-line stub telling it where the real configuration lives. +GRUB_EFI="$ROOTFS/boot/efi/EFI/fedora/grubaa64.efi" +[ -f "$GRUB_EFI" ] || { echo "grubaa64.efi missing from the install root" >&2; exit 1; } + +mkdir -p "$WORK/esp/EFI/BOOT" "$WORK/esp/EFI/fedora" +cp "$GRUB_EFI" "$WORK/esp/EFI/BOOT/BOOTAA64.EFI" +cp "$GRUB_EFI" "$WORK/esp/EFI/fedora/grubaa64.efi" +if [ -f "$ROOTFS/boot/efi/EFI/fedora/shimaa64.efi" ]; then + cp "$ROOTFS/boot/efi/EFI/fedora/shimaa64.efi" "$WORK/esp/EFI/fedora/" +fi + +# grubaa64.efi is built with a compiled-in prefix of /EFI/fedora, so this is +# the file it looks for regardless of which path it was launched from. +cat > "$WORK/esp/EFI/fedora/grub.cfg" <&2 + exit 1 + fi +done + +mv "$ROOTFS/boot" "$WORK/boot" +mkdir -p "$ROOTFS/boot" +rm -rf "$WORK/boot/efi" +mkdir -p "$WORK/boot/efi" + +# --------------------------------------------------------------------------- +log "Partitioning ${IMAGE_SIZE_MIB} MiB image" +# --------------------------------------------------------------------------- +rm -f "$IMAGE_PATH" +truncate -s "${IMAGE_SIZE_MIB}M" "$IMAGE_PATH" + +sgdisk --zap-all "$IMAGE_PATH" >/dev/null +sgdisk \ + --new "1:1M:+${ESP_SIZE_MIB}M" --typecode 1:ef00 --change-name 1:ESP \ + --new "2:0:+${BOOT_SIZE_MIB}M" --typecode 2:8300 --change-name 2:boot \ + --new "3:0:0" --typecode 3:8300 --change-name 3:root \ + "$IMAGE_PATH" >/dev/null +sgdisk --print "$IMAGE_PATH" + +part_first() { sgdisk --info="$1" "$IMAGE_PATH" | awk '/First sector/ {print $3}'; } +part_last() { sgdisk --info="$1" "$IMAGE_PATH" | awk '/Last sector/ {print $3}'; } + +ESP_START=$(part_first 1); ESP_SECTORS=$(( $(part_last 1) - ESP_START + 1 )) +BOOT_START=$(part_first 2); BOOT_SECTORS=$(( $(part_last 2) - BOOT_START + 1 )) +ROOT_START=$(part_first 3); ROOT_SECTORS=$(( $(part_last 3) - ROOT_START + 1 )) + +# --------------------------------------------------------------------------- +log "Building filesystems from the staged trees" +# --------------------------------------------------------------------------- +# orphan_file and metadata_csum_seed are recent ext4 features that older GRUB +# builds refuse to read. /boot has to be readable by whatever GRUB the firmware +# ends up running, so keep both filesystems conservative. +EXT4_OPTS="^orphan_file,^metadata_csum_seed" + +mkfs.vfat -F 32 -n ESP -i "$ESP_ID" -C "$WORK/esp.img" $(( ESP_SECTORS / 2 )) >/dev/null +mcopy -i "$WORK/esp.img" -s "$WORK/esp/EFI" :: + +mke2fs -q -t ext4 -b 4096 -O "$EXT4_OPTS" -L boot -U "$BOOT_UUID" \ + -d "$WORK/boot" "$WORK/boot.img" $(( BOOT_SECTORS / 8 )) + +mke2fs -q -t ext4 -b 4096 -O "$EXT4_OPTS" -L fedora -U "$ROOT_UUID" \ + -d "$ROOTFS" "$WORK/root.img" $(( ROOT_SECTORS / 8 )) + +# --------------------------------------------------------------------------- +log "Assembling the disk image" +# --------------------------------------------------------------------------- +dd if="$WORK/esp.img" of="$IMAGE_PATH" bs=512 seek="$ESP_START" conv=notrunc,sparse status=none +dd if="$WORK/boot.img" of="$IMAGE_PATH" bs=512 seek="$BOOT_START" conv=notrunc,sparse status=none +dd if="$WORK/root.img" of="$IMAGE_PATH" bs=512 seek="$ROOT_START" conv=notrunc,sparse status=none + +# --------------------------------------------------------------------------- +log "Compressing" +# --------------------------------------------------------------------------- +zstd -12 -T0 --rm -f -o "${IMAGE_PATH}.zst" "$IMAGE_PATH" +( cd "$OUT" && sha256sum "${IMAGE_NAME}.img.zst" > "${IMAGE_NAME}.img.zst.sha256" ) + +if [ "$KEEP_ROOTFS" != "1" ]; then + rm -rf "$WORK" +fi + +log "Done" +ls -lh "$OUT" diff --git a/config/device.env b/config/device.env new file mode 100644 index 0000000..5a6fbcb --- /dev/null +++ b/config/device.env @@ -0,0 +1,50 @@ +# Device parameters for the Lenovo Yoga C630 13Q50 (81JL) — Qualcomm SDM850. +# +# Sourced by build/stage2.sh. Everything here is overridable from the +# environment, so CI can tweak a value without editing this file. + +# --- identity ----------------------------------------------------------- +: "${DEVICE_NAME:=lenovo-yoga-c630}" +: "${DEVICE_DESC:=Lenovo Yoga C630 13Q50}" + +# Device tree shipped by Fedora's kernel-core, relative to /boot/dtb-$KVER/. +: "${DEVICE_DTB:=qcom/sdm850-lenovo-yoga-c630.dtb}" + +# --- kernel command line ------------------------------------------------ +# +# clk_ignore_unused / pd_ignore_unused +# The SDM850 clock and power-domain trees are only partially described in +# the device tree. Without these the kernel gates clocks and power domains +# that nothing has claimed but that the machine still needs, and the boot +# dies somewhere between the pivot and the display coming up. +# +# efi=noruntime +# The C630's EFI runtime services are not usable from Linux. Since 6.7 the +# qcom_uefisecapp driver provides efivars through SCM instead, so turning +# runtime services off costs nothing and avoids the hangs. +# +# arm64.nopauth +# Harmless on Cortex-A75/A55 (no pointer auth), kept for parity with the +# rest of the Snapdragon WoA laptop fleet. +: "${DEVICE_CMDLINE:=clk_ignore_unused pd_ignore_unused efi=noruntime arm64.nopauth}" + +# Uncomment if USB dies during boot before the Windows DSP firmware has been +# extracted — the ADSP reset puts the USB-C PHY into high-Z briefly. +# DEVICE_CMDLINE="$DEVICE_CMDLINE modprobe.blacklist=qcom_q6v5_pas" + +# --- image geometry (MiB) ----------------------------------------------- +: "${ESP_SIZE_MIB:=512}" +: "${BOOT_SIZE_MIB:=1024}" + +# Total image size. The root partition takes whatever is left, and grows to +# fill the target medium on first boot. +: "${IMAGE_SIZE_MIB:=8192}" + +# --- distro ------------------------------------------------------------- +: "${FEDORA_RELEASE:=44}" +: "${TARGET_ARCH:=aarch64}" + +# --- default account ---------------------------------------------------- +# Password is expired at first login, so it must be changed immediately. +: "${DEFAULT_USER:=fedora}" +: "${DEFAULT_PASSWORD:=fedora}" diff --git a/config/packages/base.pkgs b/config/packages/base.pkgs new file mode 100644 index 0000000..9735605 --- /dev/null +++ b/config/packages/base.pkgs @@ -0,0 +1,65 @@ +# Packages common to every variant. +# Lines are stripped of '#' comments and blanks before being handed to dnf. + +# --- core --------------------------------------------------------------- +@core +fedora-release-common +glibc-langpack-en +tzdata + +# --- kernel + firmware -------------------------------------------------- +kernel +kernel-modules +kernel-modules-extra +linux-firmware + +# qcom-firmware carries the Adreno 630 bits (a630_gmu.bin, a630_sqe.fw, +# sdm845/a630_zap.mbn) that freedreno needs for accelerated graphics. +# atheros-firmware carries ath10k WCN3990 including wlanmdsp.mbn, so WiFi +# works without touching the Windows partition. +qcom-firmware +atheros-firmware + +# Pulls the model-specific SDM850 DSP/display blobs off the Windows +# partition. See docs/firmware.md — those cannot be redistributed. +qcom-firmware-extract + +# --- boot --------------------------------------------------------------- +grub2-efi-aa64 +grub2-efi-aa64-modules +grub2-tools +grub2-tools-minimal +shim-aa64 +efibootmgr +dracut +dracut-config-generic + +# --- filesystem + growth ------------------------------------------------ +e2fsprogs +dosfstools +gdisk +parted +cloud-utils-growpart + +# --- selinux ------------------------------------------------------------ +selinux-policy-targeted +policycoreutils + +# --- networking --------------------------------------------------------- +NetworkManager +NetworkManager-wifi +wpa_supplicant +iw +openssh-server + +# --- power / peripherals ------------------------------------------------ +bluez +alsa-utils +libqmi-utils + +# --- basics ------------------------------------------------------------- +sudo +shadow-utils +vim-minimal +less +bash-completion diff --git a/config/packages/minimal.pkgs b/config/packages/minimal.pkgs new file mode 100644 index 0000000..06a8fdf --- /dev/null +++ b/config/packages/minimal.pkgs @@ -0,0 +1,12 @@ +# Bring-up variant: small enough to iterate on quickly, enough tooling to +# diagnose whatever does not work on the first boot. + +tmux +htop +pciutils +usbutils +lshw +dmidecode +strace +tcpdump +dtc diff --git a/config/packages/workstation.pkgs b/config/packages/workstation.pkgs new file mode 100644 index 0000000..34c5ff0 --- /dev/null +++ b/config/packages/workstation.pkgs @@ -0,0 +1,7 @@ +# Full GNOME desktop. Build this once the minimal image is confirmed to boot. + +@^workstation-product-environment +gnome-initial-setup +mesa-dri-drivers +mesa-vulkan-drivers +iio-sensor-proxy diff --git a/docs/firmware.md b/docs/firmware.md new file mode 100644 index 0000000..a3ec1ab --- /dev/null +++ b/docs/firmware.md @@ -0,0 +1,91 @@ +# Firmware + +The C630's firmware splits into two groups, and the split is about licensing, +not difficulty. + +## What ships in the image + +Qualcomm permits redistribution of these, so Fedora packages them and the build +installs them: + +| Package | Files | Enables | +|---|---|---| +| `qcom-firmware` | `qcom/a630_gmu.bin`, `qcom/a630_sqe.fw`, `qcom/sdm845/a630_zap.mbn` | Adreno 630 — accelerated graphics via freedreno | +| `atheros-firmware` | `ath10k/WCN3990/hw1.0/{firmware-5.bin,board-2.bin,wlanmdsp.mbn}` | WiFi and Bluetooth | + +So graphics and networking work on a freshly written image with no extra steps. + +## What has to come off the Windows partition + +The SDM850's DSP and display firmware is signed against per-model keys. Nobody +can redistribute it, and it exists on your machine only inside its Windows +install: + +| File | Enables | +|---|---| +| `qcadsp850.mbn` | Audio DSP — speakers, microphone | +| `qccdsp850.mbn` | Compute DSP — sensor hub, so lid switch, accelerometer, auto-rotate | +| `qcdxkmsuc850.mbn` | Display/UEFI secure code | +| `qcdsp1v2850.mbn`, `qcdsp2850.mbn` | Modem DSP stages | +| `ipa_fws.elf` | IPA — needed by the modem path | + +They belong under `/lib/firmware/updates/qcom/sdm850/LENOVO/81JL/`. + +## Extracting them + +The image ships Fedora's `qcom-firmware-extract`. Run it once: + +```sh +sudo c630-firmware +sudo reboot +``` + +That wraps `qcom-firmware-extract` and regenerates the initramfs afterwards, +which matters — the ADSP firmware has to be available before the root +filesystem is mounted, and `overlay/etc/dracut.conf.d/10-c630.conf` only takes +effect on a rebuild. + +If it cannot find Windows, mount it yourself and point the tool at it: + +```sh +lsblk -o NAME,SIZE,FSTYPE,LABEL # find the NTFS partition +sudo mkdir -p /mnt/windows +sudo mount /dev/sda4 /mnt/windows +sudo qcom-firmware-extract --windows-dir /mnt/windows +``` + +`qcom-firmware-extract` was written for the Snapdragon 8cx and X Elite laptops. +If it does not recognise the SDM850, copy the files by hand — they are in +`Windows/System32/DriverStore/FileRepository/` under a `qcdx*.inf_arm64_*` +directory: + +```sh +sudo find /mnt/windows/Windows/System32/DriverStore/FileRepository \ + -iname 'qc*850.mbn' -o -iname 'ipa_fws.elf' -o -iname 'qcdxkmsuc850.mbn' + +sudo mkdir -p /lib/firmware/updates/qcom/sdm850/LENOVO/81JL +sudo cp /lib/firmware/updates/qcom/sdm850/LENOVO/81JL/ +sudo dracut --force --regenerate-all +``` + +Check it took: + +```sh +dmesg | grep -iE 'remoteproc|adsp|cdsp' +``` + +## Baking firmware into the image + +If you would rather not repeat the extraction on every reinstall, drop the files +into `firmware/local/` in this repo, mirroring the `/usr/lib/firmware/updates/` +layout: + +``` +firmware/local/qcom/sdm850/LENOVO/81JL/qcadsp850.mbn +firmware/local/qcom/sdm850/LENOVO/81JL/qccdsp850.mbn +... +``` + +`build/stage2.sh` picks them up automatically. They are gitignored, and should +stay that way — this repository is public and the blobs are not yours to +publish. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..a85d2eb --- /dev/null +++ b/docs/install.md @@ -0,0 +1,97 @@ +# Installing on the Yoga C630 + +## Before you start + +Leave Windows in place. Lenovo ships firmware updates only through Windows, and +the per-model Qualcomm blobs Linux needs live in that partition — wiping it +means losing audio and the sensor hub permanently. Update everything through +Lenovo Vantage first. + +Disable Secure Boot. The image is not signed for it: + +- Press **Fn+F2** during startup, go to **Security → Secure Boot → Disabled**, or +- **Settings → Update & Security → Recovery → Advanced startup → UEFI Firmware Settings** + +## Write the image + +```sh +zstd -d fedora-44-minimal-lenovo-yoga-c630-*.img.zst +sudo dd if=fedora-44-minimal-lenovo-yoga-c630-*.img of=/dev/sdX bs=4M status=progress oflag=direct conv=fsync +``` + +Check `/dev/sdX` twice. An 8 GiB stick is enough for `minimal`, 16 GiB for +`workstation`; the root filesystem expands to fill whatever you use. + +## First boot + +Insert the stick and power on holding **Fn+F12** for the boot menu, or enter +setup with **Fn+F2** and put USB ahead of the internal drive. + +GRUB should appear with one entry. It has already been told which device tree to +load — the C630's UEFI does not supply one, which is the whole reason the +`devicetree` line exists in the boot loader entry. + +First boot does two slow things: it relabels the filesystem for SELinux and +grows the root partition. Several minutes on a USB stick is normal. Log in as +`fedora` / `fedora` and set a new password when prompted. + +## Extract the Qualcomm firmware + +Audio, the sensor hub and video decode do not work until you do this: + +```sh +sudo c630-firmware +sudo reboot +``` + +See [firmware.md](firmware.md) if it cannot find the Windows partition. + +## Installing to internal storage + +Once the USB image is behaving, copy it onto the internal UFS. + +Shrink the Windows partition from within Windows (**Disk Management → Shrink +Volume**) rather than from Linux — Windows is fussy about its own filesystem +being moved underneath it. + +Then, booted from USB, write the image to the free space. The simplest approach +that keeps Windows intact is to create the partitions by hand and copy the +filesystems across rather than `dd`-ing the whole image, since `dd` would +overwrite the existing GPT and the Windows ESP: + +1. `sudo gdisk /dev/sda` — add a `/boot` partition (1 GiB, type 8300) and a root + partition (type 8300) in the free space. Keep the existing Windows ESP. +2. `mkfs.ext4 -O ^orphan_file,^metadata_csum_seed /dev/sdaN` for both. +3. Copy the running system across with `rsync -aHAX --exclude=/dev --exclude=/proc + --exclude=/sys --exclude=/run --exclude=/boot`, then `/boot` separately. +4. Copy `EFI/fedora` from the USB stick's ESP onto the Windows ESP, and edit + `EFI/fedora/grub.cfg` so the `--fs-uuid` matches the new `/boot`. +5. Update `/etc/fstab` and the `root=UUID=` in + `/boot/loader/entries/c630-*.conf` to the new UUIDs. + +Finally, point the firmware at GRUB from an Administrator command prompt in +Windows: + +``` +bcdedit /set {bootmgr} path \EFI\fedora\grubaa64.efi +``` + +Windows resets this on some updates. To get back, boot the USB stick and run it +again, or use `efibootmgr` from Linux. + +## If it does not boot + +The kernel command line is the first thing to change. It lives in +`options` in `/boot/loader/entries/c630-*.conf`, and GRUB lets you edit it for +one boot by pressing **e**. + +| Symptom | Try | +|---|---| +| Hangs immediately after GRUB, no kernel output | Wrong or missing device tree — check the `devicetree` line points at a file that exists in `/boot/dtb-*/qcom/` | +| Boots then freezes partway | Drop `efi=noruntime`, or add `pd_ignore_unused` if it is missing | +| USB dies during boot | Add `modprobe.blacklist=qcom_q6v5_pas` | +| Cannot find root filesystem | The initramfs is missing the UFS drivers — see `overlay/etc/dracut.conf.d/10-c630.conf` | +| 90-second stall early in boot | Add `systemd.tpm2_wait=0` | + +Adding `earlycon` and `ignore_loglevel` will tell you a great deal more about +which of these it is. diff --git a/docs/runner-setup.md b/docs/runner-setup.md new file mode 100644 index 0000000..5105add --- /dev/null +++ b/docs/runner-setup.md @@ -0,0 +1,61 @@ +# Gitea runner setup + +The build runs on runners labelled `metal`, executing directly on the host +rather than in a runner-provided container, because it needs to drive podman +itself. + +## One-time host preparation + +On each runner host: + +```sh +sudo dnf install -y podman qemu-user-static-aarch64 +sudo systemctl restart systemd-binfmt +``` + +Verify: + +```sh +cat /proc/sys/fs/binfmt_misc/qemu-aarch64 +``` + +The `flags:` line must contain `F`. That flag makes the kernel open the +interpreter at registration time and keep the reference, so `qemu-aarch64-static` +resolves even inside a container that does not have it. Without it, every +aarch64 binary inside the build container fails with `exec format error`. +`qemu-user-static-aarch64` registers it correctly; plain `qemu-user` does not. + +The workflow attempts this install itself, but that only works if the runner +account has passwordless sudo. Run `probe-runner.yaml` to find out. + +## Checking what a runner can do + +The `probe runner` workflow is manual-dispatch only and reports architecture, +user, sudo, podman, emulation status and free disk. Run it once per runner +before debugging a build failure. + +## Disk space + +The build stages a full root filesystem and three filesystem images alongside +the final disk image, under the job workspace. Budget roughly: + +- `minimal` — about 20 GiB +- `workstation` — about 45 GiB + +## Runtime + +Every aarch64 binary runs under qemu-user emulation, and rpm scriptlets are the +worst case. Expect roughly 45–90 minutes for `minimal` and several hours for +`workstation`. The workflow's `timeout-minutes` is set to 600 accordingly. + +If this becomes tiresome, the fix is a native aarch64 runner. Register one with +an `aarch64` label and change `runs-on: metal` to `runs-on: aarch64` in +`.gitea/workflows/build-image.yaml`; `build/build-image.sh` already skips the +emulation check when the host is already the target architecture. + +## Changing which runners are used + +All nine current runners carry `metal` and `podman`. `runs-on: metal` therefore +matches any of them. To pin the build to a subset, give those runners a distinct +label and use it — Gitea requires a runner to carry every label listed in +`runs-on`. diff --git a/firmware/local/.gitkeep b/firmware/local/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/overlay/etc/default/grub.in b/overlay/etc/default/grub.in new file mode 100644 index 0000000..6161dbf --- /dev/null +++ b/overlay/etc/default/grub.in @@ -0,0 +1,13 @@ +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" +GRUB_DEFAULT=saved +GRUB_DISABLE_SUBMENU=true +GRUB_TERMINAL_OUTPUT="console" +GRUB_CMDLINE_LINUX="@DEVICE_CMDLINE@" +GRUB_DISABLE_RECOVERY=true +GRUB_ENABLE_BLSCFG=true + +# Consumed by grub2-mkconfig on aarch64. The BLS entries carry their own +# `devicetree` line as well (written by 95-c630-devicetree.install), because +# kernel-install does not read this file. +GRUB_DEFAULT_DTB=/dtb/@DEVICE_DTB@ diff --git a/overlay/etc/dracut.conf.d/10-c630.conf b/overlay/etc/dracut.conf.d/10-c630.conf new file mode 100644 index 0000000..980fa4b --- /dev/null +++ b/overlay/etc/dracut.conf.d/10-c630.conf @@ -0,0 +1,12 @@ +# The image is built on a machine that is nothing like a C630, so the initramfs +# must be generic — a host-only initramfs would contain the builder's drivers. +hostonly="no" +hostonly_cmdline="no" + +# UFS is the C630's only internal storage and the rootfs lives on it, so the +# controller and PHY drivers have to be in the initramfs, not modules loaded +# later from a filesystem we cannot yet read. +add_drivers+=" ufshcd-core ufshcd-pltfrm ufs-qcom phy-qcom-qmp-ufs " + +# Needed before the display comes up, and cheap to include. +add_drivers+=" nvmem_qfprom qcom_scm rtc-pm8xxx " diff --git a/overlay/etc/kernel/install.d/95-c630-devicetree.install.in b/overlay/etc/kernel/install.d/95-c630-devicetree.install.in new file mode 100755 index 0000000..45268d2 --- /dev/null +++ b/overlay/etc/kernel/install.d/95-c630-devicetree.install.in @@ -0,0 +1,47 @@ +#!/usr/bin/bash +# +# Teach kernel-install about the C630's device tree. +# +# The C630's UEFI hands Linux no device tree, so GRUB has to load one. Fedora's +# BLS snippets have a `devicetree` key for exactly this, but nothing in the +# stock toolchain populates it — which means a plain `dnf update kernel` would +# otherwise produce an unbootable entry. This hook runs after 20-grub.install +# and patches the entry it just wrote. + +set -eu + +COMMAND="${1:?}" +KERNEL_VERSION="${2:?}" + +DTB_REL="@DEVICE_DTB@" +BOOT_ROOT="${KERNEL_INSTALL_BOOT_ROOT:-/boot}" +ENTRIES_DIR="${BOOT_ROOT}/loader/entries" + +case "$COMMAND" in +add) + dtb_path="/dtb-${KERNEL_VERSION}/${DTB_REL}" + if [ ! -e "${BOOT_ROOT}${dtb_path}" ]; then + echo "95-c630-devicetree: ${BOOT_ROOT}${dtb_path} is missing;" \ + "leaving boot entries alone rather than writing a bad one" >&2 + exit 0 + fi + + shopt -s nullglob + for entry in "${ENTRIES_DIR}"/*-"${KERNEL_VERSION}".conf; do + # Drop any stale devicetree line, then append the current one. Doing it + # in that order makes the hook idempotent across re-installs. + tmp="${entry}.c630.$$" + grep -v '^devicetree[[:space:]]' "$entry" > "$tmp" + printf 'devicetree %s\n' "$dtb_path" >> "$tmp" + mv -f "$tmp" "$entry" + echo "95-c630-devicetree: set devicetree ${dtb_path} in ${entry}" + done + ;; +remove) + # The build writes its own bootstrap entry under a fixed name, which + # kernel-install would not otherwise clean up when the kernel goes away. + rm -f "${ENTRIES_DIR}/c630-${KERNEL_VERSION}.conf" + ;; +esac + +exit 0 diff --git a/overlay/etc/motd.d/c630.in b/overlay/etc/motd.d/c630.in new file mode 100644 index 0000000..d1be7a0 --- /dev/null +++ b/overlay/etc/motd.d/c630.in @@ -0,0 +1,7 @@ + + Fedora @FEDORA_RELEASE@ for the @DEVICE_DESC@ (built @BUILD_DATE@ from @BUILD_REF@) + + Graphics and WiFi work with the firmware Fedora ships. Audio, the sensor + hub and hardware video decode need per-model blobs from the Windows + partition — run `sudo c630-firmware` once to extract them. + diff --git a/overlay/etc/systemd/system/c630-growfs.service b/overlay/etc/systemd/system/c630-growfs.service new file mode 100644 index 0000000..a479483 --- /dev/null +++ b/overlay/etc/systemd/system/c630-growfs.service @@ -0,0 +1,15 @@ +[Unit] +Description=Grow C630 root filesystem to fill the medium +DefaultDependencies=no +After=systemd-remount-fs.service +Before=local-fs.target sysinit.target +ConditionPathExists=!/var/lib/c630/growfs-done + +[Service] +Type=oneshot +ExecStart=/usr/local/sbin/c630-growfs +RemainAfterExit=yes +StandardOutput=journal+console + +[Install] +WantedBy=sysinit.target diff --git a/overlay/usr/local/sbin/c630-firmware b/overlay/usr/local/sbin/c630-firmware new file mode 100755 index 0000000..235f21b --- /dev/null +++ b/overlay/usr/local/sbin/c630-firmware @@ -0,0 +1,49 @@ +#!/usr/bin/bash +# +# Pull the model-specific Qualcomm blobs off this machine's Windows partition. +# +# Fedora ships everything for the C630 that Qualcomm allows to be redistributed +# — Adreno 630 (graphics) and ath10k WCN3990 (WiFi/Bluetooth) both work out of +# the box. What it cannot ship is the per-model signed firmware: the ADSP, the +# compute DSP, and the display/UEFI blob. Those are signed against this +# machine's own keys and live only in its Windows install. +# +# Without them you lose audio, the sensor hub (lid switch, accelerometer, +# auto-rotate) and hardware video decode. Everything else still works. + +set -euo pipefail + +if [ "$(id -u)" -ne 0 ]; then + echo "c630-firmware: needs root — try: sudo c630-firmware" >&2 + exit 1 +fi + +echo "Looking for a Windows installation to extract firmware from..." +echo + +if ! command -v qcom-firmware-extract >/dev/null; then + echo "qcom-firmware-extract is not installed. Install it with:" >&2 + echo " sudo dnf install qcom-firmware-extract" >&2 + exit 1 +fi + +qcom-firmware-extract "$@" + +echo +echo "Regenerating the initramfs so the DSP firmware is available early..." +dracut --force --regenerate-all + +cat <<'EOF' + +Done. Reboot to pick up the new firmware. + +If qcom-firmware-extract could not find the Windows partition, mount it by hand +and point the tool at it: + + sudo mkdir -p /mnt/windows + sudo mount /dev/disk/by-partlabel/Windows /mnt/windows # adjust as needed + sudo qcom-firmware-extract --windows-dir /mnt/windows + +See docs/firmware.md in the c630 repo for the full file list and the manual +fallback if the tool does not recognise this model. +EOF diff --git a/overlay/usr/local/sbin/c630-growfs b/overlay/usr/local/sbin/c630-growfs new file mode 100755 index 0000000..e94a503 --- /dev/null +++ b/overlay/usr/local/sbin/c630-growfs @@ -0,0 +1,43 @@ +#!/usr/bin/bash +# +# Grow the root partition and filesystem to fill the medium. +# +# The image is built at a fixed size so the artifact stays small; whatever it +# gets written to is almost always bigger. Runs once, then disables itself. + +set -euo pipefail + +STAMP=/var/lib/c630/growfs-done + +root_src="$(findmnt -no SOURCE /)" +root_dev="$(basename "$root_src")" + +part_num_file="/sys/class/block/${root_dev}/partition" +if [ ! -r "$part_num_file" ]; then + echo "c630-growfs: / is on ${root_src}, which is not a partition — nothing to grow" + mkdir -p "$(dirname "$STAMP")" && touch "$STAMP" + exit 0 +fi + +part_num="$(cat "$part_num_file")" +disk="/dev/$(lsblk -no pkname "$root_src")" + +# The image was written with dd, so the secondary GPT header is still sitting +# where the end of the image used to be. growpart refuses to touch a disk in +# that state, so move it to the real end of the device first. +sgdisk --move-second-header "$disk" || true + +if growpart "$disk" "$part_num"; then + echo "c630-growfs: grew ${disk}${part_num}" +else + # growpart exits non-zero with NOCHANGE when the partition already fills + # the disk. That is a success for our purposes. + echo "c630-growfs: partition already at full size" +fi + +partprobe "$disk" || true +resize2fs "$root_src" + +mkdir -p "$(dirname "$STAMP")" +touch "$STAMP" +echo "c630-growfs: done"