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:
122
.gitea/workflows/build-image.yaml
Normal file
122
.gitea/workflows/build-image.yaml
Normal 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
|
||||
44
.gitea/workflows/probe-runner.yaml
Normal file
44
.gitea/workflows/probe-runner.yaml
Normal 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
12
.gitignore
vendored
Normal 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
94
README.md
Normal 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
116
build/build-image.sh
Executable 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
337
build/stage2.sh
Executable 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
50
config/device.env
Normal 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
65
config/packages/base.pkgs
Normal 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
|
||||
12
config/packages/minimal.pkgs
Normal file
12
config/packages/minimal.pkgs
Normal 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
|
||||
7
config/packages/workstation.pkgs
Normal file
7
config/packages/workstation.pkgs
Normal 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
91
docs/firmware.md
Normal 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
97
docs/install.md
Normal 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
61
docs/runner-setup.md
Normal 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 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`.
|
||||
0
firmware/local/.gitkeep
Normal file
0
firmware/local/.gitkeep
Normal file
13
overlay/etc/default/grub.in
Normal file
13
overlay/etc/default/grub.in
Normal 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@
|
||||
12
overlay/etc/dracut.conf.d/10-c630.conf
Normal file
12
overlay/etc/dracut.conf.d/10-c630.conf
Normal 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 "
|
||||
47
overlay/etc/kernel/install.d/95-c630-devicetree.install.in
Executable file
47
overlay/etc/kernel/install.d/95-c630-devicetree.install.in
Executable 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
|
||||
7
overlay/etc/motd.d/c630.in
Normal file
7
overlay/etc/motd.d/c630.in
Normal 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.
|
||||
|
||||
15
overlay/etc/systemd/system/c630-growfs.service
Normal file
15
overlay/etc/systemd/system/c630-growfs.service
Normal 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
|
||||
49
overlay/usr/local/sbin/c630-firmware
Executable file
49
overlay/usr/local/sbin/c630-firmware
Executable 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
|
||||
43
overlay/usr/local/sbin/c630-growfs
Executable file
43
overlay/usr/local/sbin/c630-growfs
Executable 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"
|
||||
Reference in New Issue
Block a user