Build Fedora aarch64 images for the Lenovo Yoga C630

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWRjNJMistCy6ngXH5aJLS
This commit is contained in:
2026-07-27 11:24:53 +03:00
commit 280874f564
21 changed files with 1294 additions and 0 deletions

View File

@@ -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

View File

@@ -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

12
.gitignore vendored Normal file
View File

@@ -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

94
README.md Normal file
View File

@@ -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-<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 | 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.

116
build/build-image.sh Executable file
View File

@@ -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 <<EOF
Usage: $0 [options]
--variant NAME Package variant from config/packages/ (default: minimal)
--output DIR Where to write the image (default: ./output)
--size MIB Image size in MiB (default: from config/device.env)
--image REF Build container image (default: registry.fedoraproject.org/fedora:\$FEDORA_RELEASE)
--keep-rootfs Leave the staged rootfs behind for inspection
-h, --help This message
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--variant) VARIANT="$2"; shift 2 ;;
--output) OUTPUT_DIR="$2"; shift 2 ;;
--size) export IMAGE_SIZE_MIB="$2"; shift 2 ;;
--image) CONTAINER_IMAGE="$2"; shift 2 ;;
--keep-rootfs) KEEP_ROOTFS=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&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 <<EOF
error: no binfmt_misc handler for aarch64.
This host is $(uname -m), so building an ${TARGET_ARCH} image needs qemu-user
emulation registered with the kernel. On a Fedora runner, one-time setup:
sudo dnf install -y qemu-user-static-aarch64
sudo systemctl restart systemd-binfmt
See docs/runner-setup.md.
EOF
exit 1
fi
# The handler must be flagged F (fix binary), or the interpreter is looked
# up inside the container's mount namespace, where it does not exist.
if ! grep -q '^flags:.*F' "$handler"; then
echo "error: $handler is registered without the 'F' flag; the qemu" >&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

337
build/stage2.sh Executable file
View File

@@ -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" <<EOF
UUID=${ROOT_UUID} / ext4 defaults 1 1
UUID=${BOOT_UUID} /boot ext4 defaults 1 2
UUID=${ESP_UUID} /boot/efi vfat umask=0077,shortname=winnt 0 2
EOF
# Empty (not missing) machine-id marks this as a first boot for systemd, which
# then generates a unique one per device rather than cloning the builder's.
: > "$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" <<EOF
title Fedora Linux ${FEDORA_RELEASE} (${KVER}) — ${DEVICE_DESC}
version ${KVER}
linux /vmlinuz-${KVER}
initrd /initramfs-${KVER}.img
devicetree /dtb-${KVER}/${DEVICE_DTB}
options root=UUID=${ROOT_UUID} ro ${DEVICE_CMDLINE}
grub_users \$grub_users
grub_arg --unrestricted
grub_class fedora
EOF
# A deliberately small grub.cfg. grub2-mkconfig would want to probe the running
# system's block devices, which inside this container describe the builder, not
# the C630. blscfg reads the entries above, so there is nothing else to do.
mkdir -p "$ROOTFS/boot/grub2"
cat > "$ROOTFS/boot/grub2/grub.cfg" <<EOF
set timeout=5
set default=0
insmod part_gpt
insmod ext2
insmod fat
insmod all_video
insmod gzio
insmod blscfg
search --no-floppy --fs-uuid --set=root ${BOOT_UUID}
if [ -s \$prefix/grubenv ]; then
load_env
fi
blscfg
EOF
: > "$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" <<EOF
search --no-floppy --fs-uuid --set=dev ${BOOT_UUID}
set root=\$dev
set prefix=(\$dev)/grub2
export prefix
configfile \$prefix/grub.cfg
EOF
cp "$WORK/esp/EFI/fedora/grub.cfg" "$WORK/esp/EFI/BOOT/grub.cfg"
# ---------------------------------------------------------------------------
log "Splitting /boot out of the root tree"
# ---------------------------------------------------------------------------
# Non-lazily, so the bind-mounted trees are genuinely gone before mke2fs walks
# the root filesystem — a lazy unmount would let it copy in the builder's /dev.
unbind_all
for d in dev/pts dev sys proc; do
if mountpoint -q "$ROOTFS/$d"; then
echo "error: $ROOTFS/$d is still mounted" >&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"

50
config/device.env Normal file
View File

@@ -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}"

65
config/packages/base.pkgs Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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

91
docs/firmware.md Normal file
View File

@@ -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 <each file> /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.

97
docs/install.md Normal file
View File

@@ -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.

61
docs/runner-setup.md Normal file
View File

@@ -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 4590 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`.

0
firmware/local/.gitkeep Normal file
View File

View File

@@ -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@

View File

@@ -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 "

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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"