The first real run took 100 minutes. Roughly 60 of those were spent doing things that either did not need doing or did not need doing under emulation. dracut ran three times. The kernel's %posttrans runs kernel-install, which builds an initramfs, and dracut-config-rescue makes it build a second, rescue one — both before this build has written /etc/dracut.conf.d/10-c630.conf and before /proc is bind-mounted, so both are wrong as well as expensive. stage2 then builds the real one. Setting initrd_generator=none in the install root for the duration of the transaction suppresses both: 50-dracut.install and 51-dracut-rescue.install each bail when KERNEL_INSTALL_INITRD_GENERATOR is not "dracut". Excluding dracut-config-rescue also spares the laptop a rescue initramfs on every future kernel update, which on this hardware is not cheap. That file must not ship. With it in place the machine would boot fine and then fail to come back after its next kernel update — a bug that surfaces weeks later looking nothing like an image problem. It is removed from the working copy, and asserted absent again immediately before the root filesystem is built. Compression was running as an aarch64 binary under qemu-user for no reason; zstd does not care what architecture it runs on. It now runs on the host when the host has zstd, falling back to in-container otherwise so the build never depends on it. Measured ~20 minutes against ~2. Add BASE_RECIPE to the staged-base stamp. Excluding a weak dependency changes what the base contains without changing the package list it hashes, so without this the next build would happily reuse a stale base. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
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, 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.
What you get
output/fedora-44-<variant>-lenovo-yoga-c630-<date>-<ref>.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 | Graphics (Adreno 630 zap shader) | LTE modem |
| WiFi + Bluetooth (ath10k WCN3990) | Audio | |
| Battery and charging | Sensor hub — lid switch, accelerometer, auto-rotate | |
| Display (unaccelerated) | Hardware video decode (venus) |
Fedora ships everything Qualcomm permits to be redistributed, which covers
WiFi and Bluetooth outright. Graphics is the awkward case: the generic Adreno
pieces (a630_gmu.bin, a630_sqe.fw) are packaged, but the C630's device tree
asks for a model-signed zap shader, qcdxkmsuc850.mbn, which exists only in
your machine's Windows partition — Fedora's generic sdm845/a630_zap.mbn is not
what this device requests. Audio, sensors and video decode are the same story.
Run sudo c630-firmware once after installing — see
docs/firmware.md for the full list and a manual fallback.
Building
CI does this on every push to main. To run it yourself:
./build/build-image.sh --variant minimal
You need podman and, on an x86_64 host, aarch64 emulation:
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.
Iterating
Every aarch64 binary runs under emulation, and the dnf transaction is
essentially all of the cost — 500-odd packages' worth of rpm scriptlets for
minimal, four times that for workstation. Everything after it takes
minutes. Since getting this machine to boot will take a few attempts, the build
is arranged so you only pay that once:
- The post-
dnfroot filesystem is staged under<work>/base, keyed on a hash of the package lists, release and variant. Editingconfig/device.env,overlay/, or the bootloader config reuses it. Editingconfig/packages/invalidates it automatically. - The working copy is made with
cp --reflink=auto, so on btrfs or xfs it is a copy-on-write clone rather than a real copy. - Downloaded rpms live in
.cache/dnf, outside the staged tree, so even--freshre-runs the scriptlets without re-downloading.
In practice a kernel-command-line change rebuilds in a few minutes.
./build/build-image.sh --variant minimal # reuses the staged base
./build/build-image.sh --variant minimal --fresh # forces a reinstall
./build/build-image.sh --variant minimal --keep-rootfs # keep the tree to poke at
CI points --work and --cache at /var/tmp/c630-build so both survive
between jobs. That is per-runner, so the first build on a given runner is cold.
The build container defaults to git.lair.cafe/gongfoo/build-fedora-44-aarch64,
which ships the assembly tooling so it does not have to be installed under
emulation on every run. If that image is not reachable the build falls back to
stock Fedora and installs the tooling itself — slower, but it works.
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 — writing the image and booting the laptop
- docs/firmware.md — what needs extracting from Windows and why
- 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.