Files
c630/README.md
rob thijssen f0b2d7846a
Some checks failed
build image / build (push) Has been cancelled
Add install-to-disk.sh, and get the GPU firmware into the initramfs
Validated by running it on the machine, twice, and inspecting the result.

Two properties of this laptop rule out the obvious approach, and the script
exists mainly to encode them. Its internal UFS reports 4096-byte logical
sectors, so the image — built with 512-byte geometry — cannot be dd'd onto it;
the GPT header and every partition offset would land in the wrong place. And
there are no EFI runtime variables, so efibootmgr cannot register a boot entry
and GRUB has to sit at the removable-media path where the firmware looks
unprompted.

Three things the validation runs caught that review would not have:

rsync is not in the image. I had put it in the build container and never in
the package list, so the first run died at the copy. It now falls back to tar
(--xattrs-include='*', or SELinux labels are silently dropped and the result
does not boot), and rsync is in base.pkgs for the progress output.

Copying a live root makes tar exit non-zero — files change underneath it, and
this machine's clock is wrong besides, so every mtime looks like it is in the
future. With pipefail that aborted the install after the root filesystem and
before /boot, leaving a half-installed disk that looked plausible. Warning-level
exits are now tolerated and only a fatal exit 2 stops the run.

systemd-machine-id-setup keeps an existing valid id, and one had just been
copied off the stick, so the installed system was a clone. The file is removed
first now.

Also: msm_dpu probes ~6s in, while the initramfs is still root, and asks for
qcom/a630_sqe.fw before the real filesystem carrying it is reachable. It never
retries. Adding the Adreno firmware to the initramfs is a few tens of
kilobytes. c630-firmware does the same for the DSP blobs once they exist, since
dracut rejects install_items globs that match nothing.

chrony, because the RTC reads 1970 and nothing was correcting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
2026-07-27 19:23:29 +03:00

175 lines
7.7 KiB
Markdown

# 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-<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](docs/firmware.md) for the full list and a manual fallback.
## 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.
### 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-`dnf` root filesystem is staged under `<work>/base`, keyed on a hash
of the package lists, release and variant. Editing `config/device.env`,
`overlay/`, or the bootloader config reuses it. Editing `config/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
`--fresh` re-runs the scriptlets without re-downloading.
In practice a kernel-command-line change rebuilds in a few minutes.
```sh
./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](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
## Boot status
Confirmed on hardware, from a USB stick:
- GRUB loads the kernel and the device tree
- The kernel comes up and probes the SDM850 — UFS controller, display
subsystem, WiFi and IPA all appear as platform devices, so
`DEVICE_CMDLINE` and the DTB are right
- The framebuffer console works (`simple-framebuffer`, 240x67)
- The root filesystem mounts, systemd starts, and it reaches a login prompt
- Networking works over a USB WiFi dongle, and sshd is reachable
- `build/install-to-disk.sh` copies it onto the internal UFS
Not yet confirmed: booting from the internal drive rather than USB.
Onboard WiFi does not appear at all, and audio, sensors, video decode and
accelerated graphics are all absent — every one of them waiting on the
model-signed firmware described in [docs/firmware.md](docs/firmware.md). On
this machine Windows has been wiped, so those blobs are gone; the kernel names
each missing file explicitly in `dmesg`.
The internal drive needs `build/install-to-disk.sh` rather than `dd`: its UFS
uses 4096-byte logical sectors, which the 512-byte image geometry cannot be
written onto directly. See [docs/install.md](docs/install.md).
SELinux is shipped permissive — see below.
### SELinux
The build labels the filesystem offline with `setfiles`, so enforcing should
work. It ships **permissive** anyway, because the failure mode is unusually
punishing: an unlabelled root takes PID 1 down with `Failed to allocate manager
object` before anything can be logged into and repaired, and `/.autorelabel`
cannot save it because nothing survives long enough to act on the flag.
Once the machine is up and you are happy with it:
```sh
sudo setenforce 1 # try it for this boot
sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config
```
Or set `SELINUX_MODE=enforcing` in `config/device.env` and rebuild.
## Caveats
The 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 derived from
documentation for this machine. Findings belong in this README.