Author SHA1 Message Date
Dan Kluser 03a8dc0f6d deprecate alpine 2026-05-18 08:16:31 +02:00
54 changed files with 137 additions and 3498 deletions
-13
View File
@@ -1,13 +0,0 @@
* text=auto
*.sh text eol=lf
*.bash text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.service text eol=lf
build_image.sh text eol=lf
boot/kexec*.sh text eol=lf
distros/**/grow-rootfs text eol=lf
distros/shared/zz-update-boot* text eol=lf
docker/** text eol=lf
+60 -158
View File
@@ -8,13 +8,19 @@ on:
description: 'Distribution'
default: 'ubuntu2604'
type: choice
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
options: [all, ubuntu2604, arch, cachyos]
workflow_call:
inputs:
distro:
type: string
default: 'ubuntu2604'
secrets:
REMOTE_SSH_KEY:
required: true
REMOTE_SSH_CONFIG:
required: true
REMOTE_SSH_KNOWN_HOSTS:
required: true
R2_ACCESS_KEY_ID:
required: true
R2_SECRET_ACCESS_KEY:
@@ -34,7 +40,7 @@ jobs:
run: |
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
if [ "$INPUT" = "all" ]; then
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
echo 'distros=["ubuntu2604","arch","cachyos"]' >> "$GITHUB_OUTPUT"
else
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
fi
@@ -42,54 +48,26 @@ jobs:
build:
needs: matrix
runs-on: self-hosted
timeout-minutes: 720
timeout-minutes: 240
permissions:
contents: write
strategy:
fail-fast: false
max-parallel: 5
max-parallel: 1
matrix:
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
env:
CCACHE_DIR: /home/opc/ccache
CACHE_DIR: /home/opc/ccache
steps:
- name: Clean image workspace
run: |
set -euo pipefail
if [ -z "${GITHUB_WORKSPACE:-}" ] || [ "$GITHUB_WORKSPACE" = "/" ]; then
echo "Invalid GITHUB_WORKSPACE" >&2
exit 1
fi
# A previous run can leave loop mounts (kpartx / losetup -P) and
# bind mounts (distrobuilder chroots) live inside image/work/.
# rm -rf then trips on "Directory not empty" because it can't
# cross the mountpoint. Unmount everything under the workspace
# first, then detach any loop devices still backed by files in
# the workspace, then rm.
WS="$GITHUB_WORKSPACE"
# Lazy-unmount any mount whose target is under the workspace.
# Reverse order so deepest mounts go first.
for m in $(findmnt -rn -o TARGET | grep "^$WS/" | sort -r || true); do
echo "unmounting stale $m"
sudo umount -l "$m" || true
done
# Detach any loop device whose backing file lives under the
# workspace (or shows '(deleted)' against one — that means the
# backing file was removed but the loop wasn't released).
for l in $(sudo losetup -l --noheadings -O NAME,BACK-FILE 2>/dev/null \
| awk -v ws="$WS" '$2 ~ "^"ws || $2 ~ ws"/" || $2 ~ "\\(deleted\\)$" {print $1}'); do
echo "detaching stale loop $l"
sudo losetup -d "$l" || true
done
sudo rm -rf "$WS/image"
- name: Checkout image builder
uses: actions/checkout@v6
with:
repository: ps5-linux/ps5-linux-image
path: image
ref: main
- name: Build ${{ matrix.distro }} image
run: |
@@ -112,7 +90,7 @@ jobs:
- name: Compress image
run: |
xz -T0 -4 -c image/output/ps5-${{ matrix.distro }}.img \
xz -T0 -9 -c image/output/ps5-${{ matrix.distro }}.img \
> image/output/ps5-${{ matrix.distro }}.img.xz
- name: Upload to R2
@@ -144,6 +122,27 @@ jobs:
path: meta/
retention-days: 1
- name: Upload to remote
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
DEST="image-${{ matrix.distro }}-${{ steps.version.outputs.ts }}"
$SSH remote mkdir -p "./$DEST"
$SCP image/output/*.img.xz "remote:./$DEST/"
$SSH remote 'ls -dt image-${{ matrix.distro }}-*/ | tail -n +5 | xargs rm -rf' || true
rm -rf "$DIR"
release:
needs: build
runs-on: self-hosted
@@ -160,112 +159,32 @@ jobs:
- name: Prepare release body
id: body
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const path = require('path');
run: |
KVER=$(cat meta/kver)
PSHA=$(cat meta/psha)
TS=$(cat meta/ts)
SUMS=$(cat meta/*.sha256)
const kver = fs.readFileSync('meta/kver', 'utf8').trim();
const psha = fs.readFileSync('meta/psha', 'utf8').trim();
const ts = fs.readFileSync('meta/ts', 'utf8').trim();
// Collect new checksums from this run
const newSums = {};
for (const f of fs.readdirSync('meta').filter(f => f.endsWith('.sha256'))) {
const distro = f.replace('.sha256', '');
newSums[distro] = fs.readFileSync(path.join('meta', f), 'utf8').trim();
}
// All known distros and their display names (order = table order)
const R2 = 'https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev';
const ALL_DISTROS = [
{ key: 'ubuntu2604', label: 'Ubuntu 26.04', file: 'ps5-ubuntu2604.img.xz' },
{ key: 'arch', label: 'Arch', file: 'ps5-arch.img.xz' },
{ key: 'cachyos', label: 'CachyOS', file: 'ps5-cachyos.img.xz' },
{ key: 'fedora', label: 'Fedora 44', file: 'ps5-fedora.img.xz' },
{ key: 'proxmox', label: 'Proxmox VE 8', file: 'ps5-proxmox.img.xz' },
{ key: 'debian', label: 'Debian 12', file: 'ps5-debian.img.xz' },
{ key: 'bazzite', label: 'Bazzite (KDE gaming)', file: 'ps5-bazzite.img.xz' },
{ key: 'bazzite-deck', label: 'Bazzite Deck UI', file: 'ps5-bazzite-deck.img.xz' },
{ key: 'steamos', label: 'SteamOS 3 (Holo)', file: 'ps5-steamos.img.xz' },
];
// Fetch existing release body to preserve distros not built this run
const existingSums = {};
try {
const { data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
});
// Extract existing checksum lines (format: "hash filename")
const sumBlock = (rel.body || '').match(/```\n([\s\S]*?)```/);
if (sumBlock) {
for (const line of sumBlock[1].trim().split('\n')) {
const m = line.match(/^(\w+)\s+ps5-(\S+)\.img\.xz$/);
if (m) existingSums[m[2]] = line.trim();
}
}
// Also preserve distros that appear in the table even without checksums
for (const d of ALL_DISTROS) {
if (rel.body && rel.body.includes(d.file) && !existingSums[d.key]) {
existingSums[d.key] = '';
}
}
} catch (e) {
core.info('No existing release found, starting fresh');
}
// Merge: new checksums override existing ones
const mergedSums = { ...existingSums };
for (const [k, v] of Object.entries(newSums)) {
mergedSums[k] = v;
}
// Build the table — only include distros that have been built at least once
const rows = ALL_DISTROS
.filter(d => d.key in mergedSums || d.key in newSums)
.map(d => `| ${d.label} | [\`${d.file}\`](${R2}/${d.file}) |`);
// Combine all checksum lines
const allSumLines = ALL_DISTROS
.filter(d => mergedSums[d.key])
.map(d => mergedSums[d.key])
.join('\n');
const body = [
'PS5 Linux images — built from latest `main`.',
'',
`Kernel: \`${kver}\``,
`Patches: [\`${psha}\`](https://github.com/ps5-linux/ps5-linux-patches/commit/${psha})`,
`Built: \`${ts}\``,
'',
'| Image | Download |',
'|-------|----------|',
...rows,
'',
'**SHA256 checksums:**',
'```',
allSumLines,
'```',
].join('\n');
core.setOutput('text', body);
- name: Move latest tag to current commit
uses: actions/github-script@v8
with:
script: |
const ref = 'tags/latest';
try {
await github.rest.git.updateRef({
...context.repo, ref, sha: context.sha, force: true,
});
} catch (e) {
if (e.status !== 422) throw e;
await github.rest.git.createRef({
...context.repo, ref: 'refs/tags/latest', sha: context.sha,
});
}
{
echo 'text<<EOF'
echo "PS5 Linux images — built from latest \`main\`."
echo ""
echo "Kernel: \`$KVER\`"
echo "Patches: [\`$PSHA\`](https://github.com/ps5-linux/ps5-linux-patches/commit/$PSHA)"
echo "Built: \`$TS\`"
echo ""
echo "| Image | Download |"
echo "|-------|----------|"
echo "| Ubuntu 26.04 | [\`ps5-ubuntu2604.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-ubuntu2604.img.xz) |"
echo "| Arch | [\`ps5-arch.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-arch.img.xz) |"
echo "| CachyOS | [\`ps5-cachyos.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-cachyos.img.xz) |"
echo ""
echo "**SHA256 checksums:**"
echo "\`\`\`"
echo "$SUMS"
echo "\`\`\`"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Create or update release
uses: softprops/action-gh-release@v3
@@ -275,20 +194,3 @@ jobs:
name: PS5 Linux Image (latest)
body: ${{ steps.body.outputs.text }}
make_latest: true
- name: Refresh release publish date
uses: actions/github-script@v8
with:
script: |
const { data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
});
// Toggling draft off->on->off re-publishes, resetting published_at
// to now while keeping the same release (reactions/comments survive).
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: true,
});
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: false, make_latest: 'true',
});
core.info(`re-published release ${rel.id}`);
-56
View File
@@ -1,56 +0,0 @@
name: Move release tag
on:
workflow_dispatch:
inputs:
commit:
description: 'Commit SHA to point `latest` at (defaults to the selected ref)'
type: string
default: ''
jobs:
move-tag:
runs-on: self-hosted
permissions:
contents: write
steps:
- name: Move latest tag to commit
uses: actions/github-script@v8
with:
script: |
const ref = 'tags/latest';
const sha = '${{ inputs.commit }}'.trim() || context.sha;
try {
await github.rest.git.updateRef({
...context.repo, ref, sha, force: true,
});
} catch (e) {
if (e.status !== 422) throw e;
await github.rest.git.createRef({
...context.repo, ref: 'refs/tags/latest', sha,
});
}
core.info(`latest -> ${sha}`);
- name: Refresh release publish date
uses: actions/github-script@v8
with:
script: |
let rel;
try {
({ data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
}));
} catch (e) {
if (e.status === 404) { core.info('no latest release yet; skipping'); return; }
throw e;
}
// Toggling draft off->on->off re-publishes, resetting published_at
// to now while keeping the same release (reactions/comments survive).
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: true,
});
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: false, make_latest: 'true',
});
core.info(`re-published release ${rel.id}`);
+34
View File
@@ -65,6 +65,24 @@ jobs:
path: meta/
retention-days: 1
- name: Upload to remote
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote mkdir -p ./test-upload
$SCP image/output/ps5-${{ matrix.distro }}.img.xz "remote:./test-upload/"
rm -rf "$DIR"
- name: Cleanup
if: always()
run: rm -rf image/output meta
@@ -136,3 +154,19 @@ jobs:
for d in ubuntu2604 arch cachyos; do
rclone delete "r2:${{ secrets.R2_BUCKET }}/test-${d}.img.xz" -v || true
done
- name: Clean up remote test files
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote rm -rf ./test-upload
rm -rf "$DIR"
+3
View File
@@ -17,6 +17,9 @@ jobs:
with:
distro: all
secrets:
REMOTE_SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
REMOTE_SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
REMOTE_SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
@@ -1,72 +0,0 @@
name: Upstream patches watch
run-name: Watch ps5-linux-patches for new kernel-* tags
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
contents: write
actions: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
// Read build_image.sh from main via API (no checkout — runner
// workspace has root-owned build leftovers EACCES checkout).
const file = await github.rest.repos.getContent({
owner, repo, path: 'build_image.sh', ref: 'main',
});
const buf = Buffer.from(file.data.content, 'base64');
let body = buf.toString();
const patchesRepo = (body.match(/^PATCHES_REPO="([^"]+)"/m) || [])[1];
if (patchesRepo !== 'https://github.com/ps5-linux/ps5-linux-patches.git') {
core.info(`PATCHES_REPO is a fork (${patchesRepo}) — skipping auto-bump.`);
return;
}
// Releases API returns by published_at (most recent first).
// Tags API order is not chronological — hex SHA suffixes sort
// alphabetically.
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: 'ps5-linux', repo: 'ps5-linux-patches', per_page: 100,
});
const re = /^kernel-\d+\.\d+\.\d+-[a-f0-9]+$/;
const latest = releases.find(r => re.test(r.tag_name))?.tag_name;
if (!latest) { core.setFailed('no kernel-*-* tags found'); return; }
const cur = (body.match(/^PATCHES_REF="([^"]+)"/m) || [])[1];
core.info(`upstream latest: ${latest}`);
core.info(`current ref: ${cur}`);
if (latest === cur) { core.info('already current — no bump.'); return; }
const updated = body.replace(
new RegExp(`^PATCHES_REF="${cur}"`, 'm'),
`PATCHES_REF="${latest}"`,
);
await github.rest.repos.createOrUpdateFileContents({
owner, repo, path: 'build_image.sh', branch: 'main',
message: `kernel: auto-bump ${cur} -> ${latest}`,
content: Buffer.from(updated).toString('base64'),
sha: file.data.sha,
committer: { name: 'ps5-linux-bot', email: 'ps5-linux-bot@users.noreply.github.com' },
author: { name: 'ps5-linux-bot', email: 'ps5-linux-bot@users.noreply.github.com' },
});
core.info(`pushed bump to ${latest}`);
// Pushes authored by GITHUB_TOKEN don't fire workflow_run /
// push events, so trigger-builds.yml won't auto-run. Kick off
// the matrix build directly.
await github.rest.actions.createWorkflowDispatch({
owner, repo, workflow_id: 'build-image.yml', ref: 'main',
inputs: { distro: 'all' },
});
core.info('dispatched build-image.yml distro=all');
+6 -13
View File
@@ -1,11 +1,11 @@
# PS5 Linux Image Builder
Builds bootable Linux USB images for PlayStation 5 using Docker containers. Supports Ubuntu 26.04, Arch, CachyOS (Gamescope + Steam), Fedora (GNOME), individually or as a multi-distro image with kexec switching.
Builds bootable Linux USB images for PlayStation 5 using Docker containers. Supports Ubuntu 26.04, Arch, and CachyOS (Gamescope + Steam), individually or as a multi-distro image with kexec switching.
## Prerequisites
- Docker (with permission to run `--privileged` containers) — install as per your distro's instructions
- ~30GB free disk space for Ubuntu, Arch, or CachyOS
- ~30GB free disk space
Once Docker is installed, add your user to the docker group and apply it without logging out:
@@ -27,13 +27,6 @@ OR
OR
OR
# Build Fedora (GNOME desktop)
./build_image.sh --distro fedora
OR
# Build a multi-distro image (ubuntu2604 + arch + cachyos)
./build_image.sh --distro all
```
@@ -50,12 +43,12 @@ sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
| Flag | Description | Default |
|------|-------------|---------|
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, `fedora`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone version selected by PS5 patch set |
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` |
| `--img-size` | Disk image size in MB | `12000` (`32000` for `all`) |
| `--clean` | Remove all cached build artifacts and start fresh | off |
| `--kernel-only` | Build and package the kernel only, then exit | off |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.2` |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.0` |
## Caching
@@ -135,7 +128,7 @@ sudo dpkg -i linux-bin/linux-ps5_*.deb
build_image.sh # Image builder (also supports --kernel-only)
docker/
kernel-builder/ # Kernel compilation container
kernel-builder-arch/ # Repackages .deb kernel as .pkg.tar.zst
kernel-builder-arch/ # Repackages .deb kernel as .pkg.tar.zst
image-builder/
Dockerfile # Image building container (distrobuilder)
entrypoint.sh # Single-distro build logic
+6 -41
View File
@@ -9,7 +9,7 @@ KERNEL_SRC=""
CLEAN=false
IMG_SIZE=12000
KERNEL_ONLY=false
PATCHES_REF="daa2e496086ae0c9fe22205f703925a2e2596185"
PATCHES_REF="v1.1"
MULTI_DISTROS="ubuntu2604 arch cachyos"
@@ -17,13 +17,13 @@ usage() {
echo "Usage: $0 [--distro <distro>] [--kernel <path>] [--img-size <MB>] [--clean]"
echo ""
echo "Options:"
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, batocera, all (default: ubuntu2604)"
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, all (default: ubuntu2604)"
echo " --kernel Path to kernel source directory (default: auto-clone to work/linux/)"
echo " --img-size Disk image size in MB (default: 12000, 32000 for --distro all)"
echo " --clean Remove all cached build artifacts and start from scratch"
echo " --clean-only Remove all cached build artifacts and exit"
echo " --kernel-only Build and package the kernel only, then exit"
echo " --patches-ref Branch, tag, or commit SHA for patches (default: v1.2)"
echo " --patches-ref Branch, tag, or commit SHA for patches (default: v1.0)"
exit 1
}
@@ -65,31 +65,8 @@ if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=32000
fi
# Bazzite assembles the OCI rootfs + an embedded /sysroot/ostree/repo/objects
# (a deduplicated second copy of the same content) + the linux-ps5 kernel —
# 12 GB is not enough headroom. Bump the default for any bazzite* target.
# Batocera unsquashes to ~6 GB; 12 GB is tight once kernel + initrd +
# /userdata defaults are added. Bump to 16 GB.
case "$DISTRO" in
bazzite*)
if [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=24000
fi
;;
batocera*)
if [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=16000
fi
;;
esac
if [ -z "$FORMAT" ]; then
case "$DISTRO" in
arch|cachyos|steamos) FORMAT="arch" ;;
fedora|bazzite*) FORMAT="rpm" ;;
all) FORMAT="all" ;;
*) FORMAT="deb" ;;
esac
case "$DISTRO" in arch|cachyos) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
fi
KERNEL_BUILDER_PLATFORM="linux/amd64"
@@ -129,7 +106,6 @@ SKIP_CHROOT=false
case "$FORMAT" in
arch) ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
rpm) ls "$KERNEL_OUT"/*.rpm 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
all) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
*) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
@@ -300,7 +276,7 @@ else
KERNEL_SRC="$(cd "$KERNEL_SRC" && pwd)"
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst "$KERNEL_OUT"/*.rpm
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst
run_stage "Build kernel builder image" \
docker build --platform "$KERNEL_BUILDER_PLATFORM" -t ps5-kernel-builder \
@@ -334,16 +310,6 @@ else
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-arch
esac
case "$FORMAT" in rpm)
run_stage "Build rpm packager image" \
docker build -t ps5-kernel-packager-rpm \
-f "$SCRIPT_DIR/docker/kernel-builder-rpm/Dockerfile" "$SCRIPT_DIR"
run_stage "Package kernel (.rpm)" \
docker run --rm --name "$DOCKER_NAME" \
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-rpm
esac
fi
if [ "$KERNEL_ONLY" = true ]; then
@@ -355,8 +321,7 @@ fi
# --- Step 2: Build distribution image ---
run_stage "Build image builder image" \
docker build --pull --no-cache \
-t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
docker build -t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
if [ "$DISTRO" = "all" ]; then
DOCKER_ARGS=(
+4 -81
View File
@@ -1,91 +1,14 @@
#!/bin/bash
# Grows the root partition + filesystem to fill the disk on first boot.
# Self-disables after success.
#
# Uses sfdisk + partx + resize2fs (all in util-linux + e2fsprogs, both
# present in the minimal Arch/SteamOS base). Earlier versions called
# growpart from cloud-utils — but that package isn't in the SteamOS
# recovery image, so the unit failed with "growpart: command not found"
# and rootfs stayed at the dd'd image size (~14 GB) on any disk larger
# than that.
# Grows the root partition and filesystem to fill the disk.
# Runs once on first boot, then disables itself.
set -e
ROOT_DEV=$(findmnt -no SOURCE /)
DISK="/dev/$(lsblk -ndo PKNAME "$ROOT_DEV")"
# PS5 M.2 drive appear as /dev/nvme*. Do not modify partition table,
# because it contains m2_init PS5 offset. Only USB drive (/dev/sd*)
# are safe for automatic grow root partition.
if [[ "$(basename "$DISK")" == nvme* ]]; then
echo "grow-rootfs: M.2 detected, skipping"
systemctl disable grow-rootfs.service
exit 0
fi
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)
# Our images dd at their built size (~14 GB) onto a much larger USB. The GPT
# backup header is therefore at byte ~built-size, not at end of disk. Move
# the backup header to end-of-disk first so partition table growth works.
sgdisk -e "$DISK"
# If a previous flash left behind partitions BEYOND our rootfs + boot pair
# (e.g. a stale `linux-home` partset at part 3 from a prior SteamOS install
# on the same drive), the partition extend refuses to grow into the space
# already claimed by the leftover partition. Delete every partition with a
# number higher than 2 — we only ever create partitions 1 (rootfs) + 2 (boot).
# Important: sgdisk only rewrites the on-disk GPT; we must ALSO drop those
# stale entries from the kernel's in-memory partition table via partx -d,
# or BLKPG_RESIZE_PARTITION below returns EBUSY because the kernel still
# thinks sda1 would have to grow into space owned by phantom sda3.
for n in $(parted -ms "$DISK" print 2>/dev/null \
| awk -F: 'NR>2 && $1+0>2 {print $1}'); do
sgdisk -d "$n" "$DISK" 2>/dev/null || true
partx -d --nr "$n" "$DISK" 2>/dev/null || true
done
# sfdisk `,+` extends partition $PART_NUM to consume all available free
# space. Works on a mounted/live partition because sfdisk only rewrites
# the GPT entry (kernel re-read of the partition table comes next).
echo ",+" | sfdisk --no-reread -N "$PART_NUM" "$DISK"
# Make the kernel pick up the new partition size without unmounting via
# BLKPG_RESIZE_PARTITION. partx -u also uses BLKPG, but on some images
# (notably SteamOS recovery loop-mounted btrfs) partx returns an error
# updating the mounted partition; the direct ioctl is more reliable.
python3 - "$DISK" "$PART_NUM" <<'PY'
import sys, os, fcntl, ctypes, subprocess
disk, pno = sys.argv[1], int(sys.argv[2])
out = subprocess.run(["sfdisk","--bytes","-q","-l",disk],
capture_output=True, text=True, check=True).stdout
start = sectors = None
for line in out.splitlines():
if line.startswith(f"{disk}{pno} ") or line.startswith(f"{disk}p{pno} "):
f = line.split()
start, sectors = int(f[1]), int(f[3])
break
if start is None:
sys.exit(f"could not parse partition {pno} from sfdisk -l {disk}")
class P(ctypes.Structure):
_fields_=[("start",ctypes.c_longlong),("length",ctypes.c_longlong),
("pno",ctypes.c_int),("devname",ctypes.c_char*64),
("volname",ctypes.c_char*64)]
class A(ctypes.Structure):
_fields_=[("op",ctypes.c_int),("flags",ctypes.c_int),
("datalen",ctypes.c_int),("data",ctypes.c_void_p)]
p = P(start=start*512, length=sectors*512, pno=pno)
a = A(op=3, flags=0, datalen=ctypes.sizeof(p), data=ctypes.addressof(p))
fd = os.open(disk, os.O_RDONLY)
try:
fcntl.ioctl(fd, 0x1269, a) # BLKPG, BLKPG_RESIZE_PARTITION
finally:
os.close(fd)
PY
# Belt-and-suspenders: also try partx in case BLKPG via python failed.
partx -u --nr "$PART_NUM" "$DISK" 2>/dev/null || true
# Online ext4 grow.
parted -s "$DISK" resizepart "$PART_NUM" 100%
partprobe "$DISK"
resize2fs "$ROOT_DEV"
systemctl disable grow-rootfs.service
+3 -12
View File
@@ -35,10 +35,6 @@ packages:
- wl-clipboard
- xorg-xwayland
# Bluetooth
- bluez
- bluez-utils
# Audio / media
- pipewire
- wireplumber
@@ -129,12 +125,9 @@ actions:
pacman-key --init
pacman-key --populate archlinux
# Keep CI on a small stable mirror set. The full active global list can
# select slow mirrors and fail pacman's low-speed download timeout.
cat > /etc/pacman.d/mirrorlist <<'EOF'
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
EOF
# Fetch up-to-date HTTPS mirrors ranked by score, force-refresh package DBs
curl -fsSL "https://archlinux.org/mirrorlist/?country=all&protocol=https&use_mirror_status=on" \
| sed 's/^#Server/Server/' > /etc/pacman.d/mirrorlist
pacman -Syy --noconfirm
@@ -170,8 +163,6 @@ actions:
ln -sf /etc/systemd/system/grow-rootfs.service \
/etc/systemd/system/local-fs.target.wants/grow-rootfs.service
systemctl enable bluetooth.service
# Create default user (ps5/ps5)
useradd -m -G wheel,seat -s /bin/bash ps5
echo "ps5:ps5" | chpasswd
-28
View File
@@ -1,28 +0,0 @@
# batocera
Adds support for [Batocera](https://batocera.org/) (Buildroot-based
retro-emulation distro) on PS5 hardware.
Batocera ships as an `.img.gz` with FAT32 boot + ext4 SHARE partitions; the
OS itself lives in a squashfs at `/boot/batocera`.
`distros/batocera/build-rootfs.sh` downloads + unsquashes that image and
swaps in the linux-ps5 kernel:
1. Download `https://mirrors.o2switch.fr/batocera/x86_64/stable/last/`
2. Loop-mount the FAT32, find the embedded squashfs
3. `unsquashfs` to `$CHROOT`
4. Extract the linux-ps5 `.deb`'s `vmlinuz` → `/boot/bzImage`
5. Patch `libretroControllers.py` (PS5 controller-mapping fix)
6. Set up first-boot SHARE partition creator (`ps5-share-init`)
7. Write fstab with `/boot vfat` (NOT `/boot/efi` — batocera-part's
SHARE auto-detection greps `/proc/mounts` for `/boot`)
## Build locally
```bash
./build_image.sh --distro batocera
```
Image size: 16 GB default (Batocera unsquashes to ~6 GB; headroom for
/userdata). Override the batocera release with `BATOCERA_VER` /
`BATOCERA_BUILD` envs (defaults track the upstream "last" channel).
-369
View File
@@ -1,369 +0,0 @@
#!/bin/bash
# distros/batocera/build-rootfs.sh — download upstream batocera image, extract
# its squashfs to $CHROOT, swap in our PS5 kernel + modules. Called from
# docker/image-builder/entrypoint.sh for DISTRO=batocera*.
#
# Batocera is Buildroot-based, shipping as a single .img.gz with FAT32 boot +
# ext4 SHARE partitions; the OS itself lives in a squashfs file at
# /boot/batocera on the FAT32. We unsquash, swap kernel + modules in, and
# the rest of the standard image-builder flow packs it onto ext4.
#
# Expects in env: DISTRO, CHROOT, KVER, ROOT_LABEL, EFI_LABEL
# Expects on disk: /kernel-debs/*.deb (linux-ps5 .deb to extract bzImage from)
set -ex
: "${CHROOT:?ERROR: \$CHROOT unset/empty}"
[ -d "$CHROOT" ] || { echo "ERROR: \$CHROOT=$CHROOT not a directory"; exit 2; }
case "$CHROOT" in /) echo "ERROR: refuse to operate on /"; exit 2 ;; esac
# Batocera is a Buildroot-based emulation distro. It ships as a
# single .img.gz with FAT32 boot + ext4 SHARE partitions; the OS
# itself lives in a squashfs file (`/boot/batocera`) on the FAT32.
# We unsquash, swap our PS5 kernel + modules in, and let the rest
# of the standard image-builder flow pack everything onto ext4.
# `last/` always points at the current build, but the .img.gz filename
# inside it bakes in the version + date (e.g. batocera-x86_64-43.1-20260529.img.gz),
# and the mirror rotates older builds out. Scrape the index to discover
# whatever's there today rather than hardcoding (the previous hardcoded
# 43-20260430 went 404 within ~4 weeks).
BATOCERA_INDEX_URL="${BATOCERA_INDEX_URL:-https://mirrors.o2switch.fr/batocera/x86_64/stable/last/}"
if [ -z "${BATOCERA_URL:-}" ]; then
IMG_NAME=$(wget -qO- "$BATOCERA_INDEX_URL" \
| grep -oE 'batocera-x86_64-[0-9.]+-[0-9]+\.img\.gz' \
| head -1)
if [ -z "$IMG_NAME" ]; then
echo "ERROR: couldn't find a batocera-x86_64-*.img.gz link at $BATOCERA_INDEX_URL"
exit 1
fi
BATOCERA_URL="${BATOCERA_INDEX_URL}${IMG_NAME}"
else
IMG_NAME="$(basename "$BATOCERA_URL")"
fi
# Pull VER + BUILD out of the discovered (or overridden) filename so the
# cache key + log lines stay informative.
BATOCERA_VER=$(echo "$IMG_NAME" | sed -E 's/^batocera-x86_64-([0-9.]+)-[0-9]+\.img\.gz$/\1/')
BATOCERA_BUILD=$(echo "$IMG_NAME" | sed -E 's/^batocera-x86_64-[0-9.]+-([0-9]+)\.img\.gz$/\1/')
echo "=== Batocera: locate / download $BATOCERA_VER ($BATOCERA_BUILD) ==="
# /build/cache is per-run temp. The workflow symlinks /build/cache/
# persistent -> /data/cache/ps5/downloads, so the .img.gz can be
# pre-staged or survive between runs. The mirror rate-limits per-IP
# to ~250KB/s sustained (4MB/s burst), so re-downloading every run
# is unacceptably slow.
# The workflow hard-links /data/cache/ps5/downloads/* into
# image/work/cache before the build container starts, so the
# image appears as /build/cache/batocera-*.img.gz inside.
CACHED="/build/cache/batocera-${BATOCERA_VER}-${BATOCERA_BUILD}.img.gz"
if [ ! -s "$CACHED" ]; then
echo ">> No cached image, downloading (this will be slow due to mirror rate-limiting)"
wget --tries=3 -O "$CACHED.part" "$BATOCERA_URL"
mv "$CACHED.part" "$CACHED"
fi
echo ">> Using $CACHED ($(du -h "$CACHED" | cut -f1))"
echo "=== Batocera: decompress + loop ==="
BAT_IMG=/build/batocera-src.img
gunzip -c "$CACHED" > "$BAT_IMG"
BATLOOP=$(losetup -Pf --show "$BAT_IMG")
sleep 1
# kpartx fallback in case partition kernel events didn't fire
[ -e "${BATLOOP}p1" ] || kpartx -av "$BATLOOP"
BAT_MNT=$(mktemp -d)
BAT_PART1=""
for p in "${BATLOOP}p1" "/dev/mapper/$(basename "$BATLOOP")p1"; do
[ -e "$p" ] && BAT_PART1="$p" && break
done
mount -o ro "$BAT_PART1" "$BAT_MNT"
BAT_SQUASH=""
for c in /boot/batocera /batocera /boot/batocera.update; do
[ -f "$BAT_MNT$c" ] && BAT_SQUASH="$BAT_MNT$c" && break
done
if [ -z "$BAT_SQUASH" ]; then
echo "ERROR: squashfs not found in batocera image:"
find "$BAT_MNT" -maxdepth 3 -type f | head -30
exit 1
fi
echo "=== Batocera: unsquashfs $BAT_SQUASH -> $CHROOT ==="
unsquashfs -f -d "$CHROOT" "$BAT_SQUASH"
# Batocera ships a SECOND squashfs (boot/rufomaculata) with the
# libretro cores, mame binary, and other emulator assets. At
# runtime it's mounted as a second overlayfs layer on top of the
# main batocera squashfs. We don't do overlay — just unsquash
# rufomaculata on top of $CHROOT so the unified view is realised
# on the ext4 root. Without this, /usr/lib/libretro/ doesn't
# exist and EmulationStation reports "no games start" because
# retroarch fails to load any core.
if [ -f "$BAT_MNT/boot/rufomaculata" ]; then
echo "=== Batocera: unsquashfs boot/rufomaculata (libretro + mame) -> $CHROOT ==="
unsquashfs -f -d "$CHROOT" "$BAT_MNT/boot/rufomaculata"
else
echo "WARN: boot/rufomaculata not found — emulator cores will be missing"
fi
umount "$BAT_MNT"
rmdir "$BAT_MNT"
kpartx -dv "$BATLOOP" 2>/dev/null || true
losetup -d "$BATLOOP"
rm -f "$BAT_IMG"
echo "=== Batocera: install linux-ps5 kernel + modules ==="
KSTAGE=/tmp/bat-kernel-staging
rm -rf "$KSTAGE"; mkdir -p "$KSTAGE"
# The kernel-builder ships a single combined linux-ps5_*.deb
# (Provides: linux-image-X) — there is no linux-image-*.deb on
# disk, so target the actual filename pattern.
shopt -s nullglob
for deb in /kernel-debs/linux-ps5*.deb /kernel-debs/linux-image-*.deb; do
[ -f "$deb" ] && dpkg-deb -x "$deb" "$KSTAGE"
done
shopt -u nullglob
KVER=$(ls -1 "$KSTAGE/lib/modules" 2>/dev/null | head -1)
if [ -z "$KVER" ]; then
echo "ERROR: no kernel modules found after dpkg-deb -x of /kernel-debs/*.deb"
ls -la /kernel-debs/
exit 1
fi
rm -rf "$CHROOT"/lib/modules/*
cp -a "$KSTAGE/lib/modules/$KVER" "$CHROOT/lib/modules/"
mkdir -p "$CHROOT/boot/efi"
cp "$KSTAGE/boot/vmlinuz-$KVER" "$CHROOT/boot/efi/bzImage"
# depmod -b runs from outside the chroot — Batocera's busybox
# depmod may not be present, and host depmod handles -b cleanly.
depmod -a -b "$CHROOT" "$KVER" || true
# Stage WLAN firmware loader + module autoload (same files the
# debian/fedora paths get from /kernel-debs/staging via .deb).
for src in usr/local/sbin etc/modules-load.d etc/systemd/system; do
[ -d "$KSTAGE/$src" ] || continue
mkdir -p "$CHROOT/$src"
cp -an "$KSTAGE/$src/." "$CHROOT/$src/" || true
done
echo "=== Batocera: PS5 modprobe quirks ==="
mkdir -p "$CHROOT/etc/modprobe.d" "$CHROOT/etc/modules-load.d"
cat > "$CHROOT/etc/modprobe.d/ps5-amdgpu.conf" <<MODPROBE
options amdgpu dpm=0 gpu_recovery=0
MODPROBE
# uinput is needed by Batocera's hotkeygen (for virtual keyboard
# events when launching games). It's not autoloaded by default on
# PS5, so hotkeygen crashes with 'UInputError: /dev/uinput does
# not exist'. Force-load on boot.
cat > "$CHROOT/etc/modules-load.d/uinput.conf" <<MODPROBE
uinput
MODPROBE
echo "=== Batocera: build initrd via host mkinitramfs ==="
# Host (image-builder, ubuntu:24.04) has initramfs-tools. Trick
# it into building for our PS5 kernel by symlinking the chroot's
# modules into /lib/modules/$KVER, then unlinking after.
#
# initramfs-tools default behaviour: autodetect kernel modules
# from /sys on the BUILD HOST — which is a docker container with
# no USB, no amdgpu, no real disks. The resulting initrd would
# ship without xhci_pci / usb_storage / ext4 / amdgpu drivers,
# and the PS5 hangs silently when the kernel tries to find the
# USB root partition. Override with an explicit modules list +
# MODULES=most so initramfs-tools includes everything the PS5
# actually needs at boot.
mkdir -p /lib/modules
ln -sfn "$CHROOT/lib/modules/$KVER" "/lib/modules/$KVER"
cat > /etc/initramfs-tools/modules <<'INITMODS'
# USB host controllers (PS5 boot drive is on USB 3 — xhci is the must-have).
xhci_pci
xhci_hcd
ehci_pci
ehci_hcd
ohci_pci
ohci_hcd
# USB storage class + UAS (faster path).
usb_storage
uas
sd_mod
# Filesystems for root + EFI.
ext4
vfat
nls_iso8859-1
nls_cp437
# Common HID so a USB keyboard works at the initramfs shell if we drop there.
usbhid
hid_generic
INITMODS
# Force MODULES=most (curated full driver set, no autodetect).
sed -i 's/^MODULES=.*/MODULES=most/' /etc/initramfs-tools/initramfs.conf
mkinitramfs -k "$KVER" -o "$CHROOT/boot/efi/initrd.img"
rm -f "/lib/modules/$KVER"
rm -rf "$KSTAGE"
echo "=== Batocera: patch configgen to bind HOTKEY combos on gamepad ==="
# Upstream Batocera's libretroControllers.py only sets
# input_enable_hotkey_btn — the hotkey "enable" button — and never
# binds input_exit_emulator_btn / input_menu_toggle_btn /
# input_save_state_btn / input_load_state_btn. The keyboard-side
# bindings (escape = exit, f1 = menu) work fine but on a DualSense
# there's no way out of a game without sshing in and pkill'ing
# retroarch. Patch the function to also bind start/select/L1/R1.
PYFILE="$CHROOT/usr/lib/python3.12/site-packages/configgen/generators/libretro/libretroControllers.py"
if [ -f "$PYFILE" ]; then
python3 - "$PYFILE" <<'PYPATCH'
import sys
p = sys.argv[1]
src = open(p).read()
old = " retroconfig.save('input_enable_hotkey_btn', controllers[0].inputs['hotkey'].id)"
extra = '''
# PS5: map HOTKEY combos to gamepad — upstream sets only the
# enable button, leaving exit-emulator unbound on gamepad. Without
# this, gamepad users can't exit a retroarch game without sshing
# in and pkill'ing retroarch.
for batocera_key, retroarch_key in [
('start', 'input_exit_emulator_btn'),
('select', 'input_menu_toggle_btn'),
('pageup', 'input_load_state_btn'),
('pagedown', 'input_save_state_btn'),
]:
if batocera_key in controllers[0].inputs:
retroconfig.save(retroarch_key, controllers[0].inputs[batocera_key].id)'''
if old in src and extra not in src:
open(p, 'w').write(src.replace(old, old + extra))
print(' patched libretroControllers.py')
else:
print(' skipped (line not found or already patched)')
PYPATCH
else
echo " WARN: $PYFILE missing — configgen patch skipped"
fi
echo "=== Batocera: fstab + users ==="
# NOTE the FAT32 boot partition is mounted at /boot (not
# /boot/efi like the other distros) because batocera-part —
# which S11share uses to autodetect the SHARE partition by
# 'partition next to /boot' — greps /proc/mounts for /boot.
# If we mount at /boot/efi the SHARE auto-detection silently
# fails and S11share falls back to a 256 MB tmpfs at
# /userdata, which won't fit Steam / save data / anything.
# PS5 loader reads bzImage / cmdline.txt from the FAT32
# partition's root regardless of where Linux mounts it.
mkdir -p "$CHROOT/boot"
cat > "$CHROOT/etc/fstab" <<FSTAB
LABEL=$ROOT_LABEL / ext4 defaults 0 1
LABEL=$EFI_LABEL /boot vfat defaults 0 1
LABEL=SHARE /userdata ext4 defaults 0 2
FSTAB
echo "=== Batocera: first-boot SHARE partition creator ==="
# Batocera's design splits the disk into:
# sda1 = rootfs (this image, ~15 GB)
# sda2 = /boot FAT32
# sda3 = /userdata SHARE (everything user-facing — games,
# BIOS, Steam flatpak, screenshots, saves)
# The image only ships sda1+sda2. On first boot, expand the
# GPT backup header to the actual disk end (so parted/sgdisk
# see the full free space) then carve sda3 = SHARE out of
# the remainder. Self-disables after running.
cat > "$CHROOT/usr/local/sbin/ps5-share-init" <<'PS5SHARE'
#!/bin/sh
# First-boot: create the SHARE partition + fs if missing, so /userdata
# is a real disk-backed mount (916 GB on a 1 TB drive) instead of the
# 256 MB tmpfs fallback in /etc/init.d/S11share.
set -e
ROOT_DEV=$(findmnt -no SOURCE /)
DISK=$(/usr/bin/batocera-part prefix "$ROOT_DEV")
SHARE_NUM=$(/usr/bin/batocera-part share_internal_num)
SHARE_DEV="${DISK}${SHARE_NUM}"
[ -b "$DISK" ] || exit 0
# already created on a previous boot?
if [ -b "$SHARE_DEV" ] && blkid -L SHARE >/dev/null 2>&1; then
exit 0
fi
echo "ps5-share-init: extending GPT + creating $SHARE_DEV"
sgdisk -e "$DISK"
partprobe "$DISK"
sleep 1
sgdisk -n "$SHARE_NUM":0:0 -c "$SHARE_NUM":share -t "$SHARE_NUM":8300 "$DISK"
partprobe "$DISK"
sleep 1
mkfs.ext4 -L SHARE -F "$SHARE_DEV"
PS5SHARE
chmod +x "$CHROOT/usr/local/sbin/ps5-share-init"
# Hook into Batocera's init order: run BEFORE S11share so
# S11share's batocera-part share_internal call finds the
# partition we just created.
cat > "$CHROOT/etc/init.d/S07ps5share" <<'INITSHARE'
#!/bin/sh
# First-boot SHARE partition creator — see /usr/local/sbin/ps5-share-init
case "$1" in
start|"") /usr/local/sbin/ps5-share-init >> /tmp/ps5-share-init.log 2>&1 ;;
stop|restart|reload|*) ;;
esac
INITSHARE
chmod +x "$CHROOT/etc/init.d/S07ps5share"
# First-boot defaults for /userdata/system/batocera.conf — set
# display.empty=1 so every system (PSP, PS1, PS2, PS3, PS4,
# Switch, etc) is visible in EmulationStation even before
# ROMs are loaded. S12 runs after S11share has populated
# /userdata. Idempotent: only sets a key if not already
# present, so the user remains free to flip it back.
cat > "$CHROOT/etc/init.d/S12ps5defaults" <<'INITDEF'
#!/bin/sh
case "$1" in
start|"")
CONF=/userdata/system/batocera.conf
[ -f "$CONF" ] || exit 0
grep -qE '^display\.empty=' "$CONF" || echo 'display.empty=1' >> "$CONF"
;;
esac
INITDEF
chmod +x "$CHROOT/etc/init.d/S12ps5defaults"
# Batocera ships root passwordless. Leave root usable (a lot of
# Batocera scripts assume root) but ALSO add a ps5 user so the
# release-page convention works.
if ! grep -q "^ps5:" "$CHROOT/etc/passwd"; then
echo "ps5:x:1000:1000:PS5:/home/ps5:/bin/sh" >> "$CHROOT/etc/passwd"
echo "ps5:!::0:99999:7:::" >> "$CHROOT/etc/shadow"
echo "ps5:x:1000:" >> "$CHROOT/etc/group"
mkdir -p "$CHROOT/home/ps5"
chroot "$CHROOT" /bin/sh -c "chown -R 1000:1000 /home/ps5" 2>/dev/null || true
fi
# Both root and ps5 get pw 'ps5' — Batocera's chpasswd is busybox.
chroot "$CHROOT" /bin/sh -c "printf 'ps5\nps5\n' | passwd ps5 2>/dev/null; printf 'ps5\nps5\n' | passwd root 2>/dev/null" || true
echo "=== Batocera: grow-rootfs first-boot service ==="
mkdir -p "$CHROOT/usr/local/sbin" "$CHROOT/etc/systemd/system"
cat > "$CHROOT/usr/local/sbin/grow-rootfs" <<'GROW'
#!/bin/sh
set -e
ROOT=$(findmnt -no SOURCE / || mount | awk '$3=="/"{print $1; exit}')
DISK=$(lsblk -no PKNAME "$ROOT" 2>/dev/null | head -1)
PARTNUM=$(echo "$ROOT" | grep -oE '[0-9]+$' || true)
[ -z "$DISK" ] || [ -z "$PARTNUM" ] && exit 0
growpart "/dev/$DISK" "$PARTNUM" || true
resize2fs "$ROOT" || true
GROW
chmod +x "$CHROOT/usr/local/sbin/grow-rootfs"
cat > "$CHROOT/etc/systemd/system/grow-rootfs.service" <<SVC
[Unit]
Description=Grow rootfs to fill disk (first boot)
ConditionPathExists=/usr/local/sbin/grow-rootfs
ConditionFirstBoot=yes
After=local-fs.target
Before=basic.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/grow-rootfs
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target
SVC
# Batocera switched to systemd in v33+. Try systemctl enable;
# tolerate buildroot quirks where /etc/systemd/system layout
# differs.
mkdir -p "$CHROOT/etc/systemd/system/sysinit.target.wants"
ln -sf ../grow-rootfs.service \
"$CHROOT/etc/systemd/system/sysinit.target.wants/grow-rootfs.service"
-1
View File
@@ -1 +0,0 @@
../bazzite/build-rootfs.sh
-1
View File
@@ -1 +0,0 @@
../bazzite/grow-rootfs
-1
View File
@@ -1 +0,0 @@
../bazzite/grow-rootfs.service
-1
View File
@@ -1 +0,0 @@
../bazzite/image.yaml
-30
View File
@@ -1,30 +0,0 @@
# bazzite / bazzite-deck
Adds support for [Bazzite](https://bazzite.gg/) (uBlue's gaming-focused
atomic Fedora) and Bazzite-Deck (Steam Deck UI variant) on PS5 hardware.
These are **OCI atomic images** — distrobuilder doesn't apply.
`distros/bazzite/build-rootfs.sh` runs in place of the distrobuilder call:
1. `skopeo copy docker://ghcr.io/ublue-os/bazzite:stable` → OCI archive
2. `umoci unpack` → flat rootfs into `$CHROOT`
3. Promote `/usr/etc` defaults into `/etc`
4. Install the linux-ps5 RPM via `rpm-ostree`/`dnf`, then mask the
rpm-ostree services (we're a flat fs now)
5. Set up grow-rootfs systemd unit + DTM-TA-race amdgpu reprobe udev rule
`bazzite-deck` is built from `ghcr.io/ublue-os/bazzite-deck:stable` via the
same script (the `case "$DISTRO" in bazzite-*)` branch generates the OCI
reference automatically). All `distros/bazzite-deck/*` files are symlinks
into `distros/bazzite/`.
## Build locally
```bash
./build_image.sh --distro bazzite
./build_image.sh --distro bazzite-deck
```
Image size bumped to 24 GB (default). Compressed output is large (~3-5 GB
`.img.xz`) — too big for a 2 GB GitHub release asset, so this image is not
auto-published by the CI workflow.
-401
View File
@@ -1,401 +0,0 @@
#!/bin/bash
# distros/bazzite/build-rootfs.sh — fetch the uBlue OCI image and prep $CHROOT.
# Called from docker/image-builder/entrypoint.sh for DISTRO=bazzite*.
#
# Bazzite is an OCI atomic image; we bypass distrobuilder entirely.
# DISTRO=bazzite -> ghcr.io/ublue-os/bazzite:stable
# DISTRO=bazzite-deck -> ghcr.io/ublue-os/bazzite-deck:stable
#
# Expects in env: DISTRO, CHROOT, KVER
# Expects on disk: /kernel-debs/*.rpm (linux-ps5 RPM), /repo/distros/bazzite/{grow-rootfs,grow-rootfs.service}
set -ex
: "${CHROOT:?ERROR: \$CHROOT unset/empty}"
[ -d "$CHROOT" ] || { echo "ERROR: \$CHROOT=$CHROOT not a directory"; exit 2; }
case "$CHROOT" in /) echo "ERROR: refuse to operate on /"; exit 2 ;; esac
# Bazzite is an OCI atomic image; bypass distrobuilder entirely.
# DISTRO=bazzite -> ghcr.io/ublue-os/bazzite:stable
# DISTRO=bazzite-deck -> ghcr.io/ublue-os/bazzite-deck:stable
# Anything else after `bazzite-` is treated as the same uBlue
# image-name pattern (bazzite-gnome, bazzite-nvidia, ...).
case "$DISTRO" in
bazzite) REF="ghcr.io/ublue-os/bazzite:stable" ;;
bazzite-*) REF="ghcr.io/ublue-os/${DISTRO}:stable" ;;
*) REF="ghcr.io/ublue-os/bazzite:stable" ;;
esac
echo "=== Bazzite: skopeo copy $REF ==="
OCI=$(mktemp -d)
skopeo copy --override-os linux --override-arch amd64 \
"docker://$REF" "oci:$OCI:bazzite"
echo "=== umoci unpack -> $CHROOT ==="
UNPACK=$(mktemp -d)
umoci unpack --keep-dirlinks --image "$OCI:bazzite" "$UNPACK"
# umoci layout: $UNPACK/{config.json, rootfs/}
mv "$UNPACK/rootfs"/* "$CHROOT/" 2>/dev/null || true
mv "$UNPACK/rootfs"/.[!.]* "$CHROOT/" 2>/dev/null || true
rm -rf "$UNPACK" "$OCI"
# ostree convention: /usr/etc holds the defaults; /etc is empty in
# the image. Promote /usr/etc to /etc so the system boots normally.
if [ -d "$CHROOT/usr/etc" ]; then
cp -an "$CHROOT/usr/etc/." "$CHROOT/etc/" || true
rm -rf "$CHROOT/usr/etc"
fi
# Stage PS5 kernel RPMs + grow-rootfs. /opt and /home are ostree
# symlinks in Bazzite, /var is a real dir — drop staging files there.
mkdir -p "$CHROOT/var/cache/ps5-rpms"
cp /kernel-debs/*.rpm "$CHROOT/var/cache/ps5-rpms/"
# /usr/local is a symlink to /var/usrlocal in ostree-based systems;
# mkdir the target before cp to avoid following-symlink-on-missing.
mkdir -p "$CHROOT/var/usrlocal/sbin"
cp /repo/distros/bazzite/grow-rootfs "$CHROOT/var/usrlocal/sbin/grow-rootfs"
chmod +x "$CHROOT/var/usrlocal/sbin/grow-rootfs"
cp /repo/distros/bazzite/grow-rootfs.service "$CHROOT/etc/systemd/system/grow-rootfs.service"
# Force NetworkManager wifi backend off iwd back to wpa_supplicant. Bazzite
# defaults to iwd; iwd is incompatible with the PS5 NXP IW620 mwifiex driver
# (no SSIDs scanned, NM hangs). wpa_supplicant works out of the box.
mkdir -p "$CHROOT/etc/NetworkManager/conf.d"
cat > "$CHROOT/etc/NetworkManager/conf.d/00-no-iwd.conf" <<'NMCONF'
[device]
wifi.backend=wpa_supplicant
NMCONF
# Chroot in: disable ostree stack, install PS5 kernel, user setup.
# Trap to always umount, even if the chroot script exits early.
cleanup_bazzite_mounts() {
for m in dev sys proc; do
mountpoint -q "$CHROOT/$m" && umount "$CHROOT/$m" || true
done
}
trap cleanup_bazzite_mounts RETURN ERR EXIT
mount --bind /proc "$CHROOT/proc"
mount --bind /sys "$CHROOT/sys"
mount --bind /dev "$CHROOT/dev"
# Bazzite has no /etc/resolv.conf inside the chroot (symlink target
# doesn't exist yet) — provide a working one so dnf can reach mirrors.
rm -f "$CHROOT/etc/resolv.conf"
cp /etc/resolv.conf "$CHROOT/etc/resolv.conf"
chroot "$CHROOT" /bin/bash -e <<"BAZIN"
# Disable rpm-ostree services — we're a flat fs now.
systemctl mask rpm-ostreed.service rpm-ostree-countme.service rpm-ostree-bootstatus.service 2>/dev/null || true
# Bazzite ships /usr/bin/dnf as a shell wrapper that refuses `install` /
# `remove` unless it detects a container or a dev-mode ostree deployment
# (points users at rpm-ostree instead). Neither condition holds on our
# flat-fs kexec-booted install, so `dnf install` prints a docs URL and
# exits 1. Replace the wrapper with a direct dnf5 symlink — with ostree
# gone the guard has no purpose here.
if [ -f /usr/bin/dnf ] && head -1 /usr/bin/dnf | grep -q "^#!.*bash"; then
rm -f /usr/bin/dnf
ln -s dnf5 /usr/bin/dnf
fi
# Drop the embedded ostree object store + deploy tree. With
# rpm-ostree masked, the running rootfs is the flat OCI
# extract — /sysroot/ostree/repo/objects/ is a deduplicated
# second copy of the same content (~5GB+), and /ostree/
# deploy/ holds yet another. Wiping them shrinks the disk
# image roughly in half. Leave the dir skeleton in case
# anything probes for it.
rm -rf /sysroot/ostree/repo/objects \
/sysroot/ostree/repo/refs \
/sysroot/ostree/deploy
mkdir -p /sysroot/ostree/repo/objects \
/sysroot/ostree/repo/refs/heads
# Bazzite/rpm-ostree convention: /root is a symlink to
# /var/roothome which doesn't exist in the OCI extract.
# dracut's hostonly enumeration follows the symlink, hits
# ENOENT, fails with `dracut-install: ERROR: installing '/root'`.
# Make /root a real dir so dracut + the kernel postinst's own
# dracut call both work.
mkdir -p /var/roothome
if [ -L /root ]; then
rm -f /root
mkdir -m 0700 /root
fi
# Install PS5 kernel via rpm --replacefiles (handles the file-
# level conflict between our /usr/include/* headers and
# Bazzite's kernel-headers; see fedora image.yaml comment).
# Bazzite ships kernel modules as a dir; our rpm wants a symlink.
rm -rf /lib/modules/*
rpm -Uvh --replacefiles --replacepkgs --nodeps /var/cache/ps5-rpms/*.rpm
rm -rf /var/cache/ps5-rpms
# cyan_skillfish (PS5 Oberon) GPU firmware MUST land in the
# rootfs uncompressed. The linux-ps5 amdgpu patches write into
# the request_firmware() buffer to skip Sony's signature header
# (gfx_v10_0_early_init + amdgpu_sdma_init_microcode). Firmware
# loaded from a .xz file is decompressed into pages the kernel
# maps PAGE_KERNEL_RO (fw_decompress_xz_pages -> fw_map_paged_buf
# -> vmap PAGE_KERNEL_RO), so the write oopses amdgpu at
# gfx_v10_0_early_init+0x415 and /dev/dri never appears.
# Distros that ship .zst (arch) or raw .bin (debian) decompress
# into writable buffers and are unaffected — this fix is for
# the rpm path only. linux-firmware dedupes blobs as symlinks
# (mec2 -> mec) and unxz refuses symlinks, so materialize the
# link targets first while the canonical .xz still exists.
# Upstream did this same fix in
# github.com/ps5-linux/ps5-linux-image@ed54e99 — same kernel
# patches, same firmware, same failure mode.
cd /usr/lib/firmware/amdgpu
for f in cyan_skillfish*.xz; do
if [ -L "$f" ]; then
tgt=$(readlink -f "$f")
rm "$f"
xz -dc "$tgt" > "${f%.xz}"
fi
done
unxz cyan_skillfish*.xz
cd /
# Pre-configure repo.etawen.dev so users can
# `dnf upgrade linux-ps5` after first boot. Per-package
# gpgcheck=0 (alien-converted RPMs aren't per-package
# signed); repodata IS signed by the mia PGP key.
cat > /etc/yum.repos.d/etawen-ps5.repo <<ETAWEN
[etawen-ps5]
name=Etawen PS5 kernel repo
baseurl=https://repo.etawen.dev/rpm/
enabled=1
gpgcheck=0
repo_gpgcheck=1
gpgkey=https://repo.etawen.dev/key.asc
ETAWEN
# amdgpu options — PS5 Oberon GPU needs dpm disabled or HDMI
# stays dark. Must land in initramfs (amdgpu loads early).
mkdir -p /etc/modprobe.d
cat > /etc/modprobe.d/ps5-amdgpu.conf <<AMDGPU
options amdgpu dpm=0 gpu_recovery=0
AMDGPU
# Build the initrd, then deploy bzImage+initrd to /boot/efi/
# for the PS5 kexec loader. (zz-update-boot is the deb-flow
# helper; bazzite never stages it, so we inline the copies.)
KVER=$(ls -1t /lib/modules | head -1)
dracut -f --kver "$KVER" "/boot/initrd.img-$KVER"
mkdir -p /boot/efi
cp "/boot/vmlinuz-$KVER" /boot/efi/bzImage
cp "/boot/initrd.img-$KVER" /boot/efi/initrd.img
# Suppress first-boot wizards. plasma-setup.service runs on every
# boot until /etc/plasma-setup-done exists, and its bootutil
# rewrites SDDM autologin to User=plasma-setup (clobbering our
# User=ps5) and starts the Plasma OOBE wizard, which prompts the
# user to create a fresh account. Our build pre-creates ps5; the
# wizard is unwanted.
touch /etc/plasma-setup-done
systemctl mask plasma-setup.service 2>/dev/null || true
# bazzite-hardware-setup.service runs on every boot until the
# marker files in /etc/bazzite/ match the image-info.json. Seed
# them so the script exits at its early-return; also mask it
# outright since the script calls `rpm-ostree kargs` which fails
# against our masked rpm-ostreed. The script's other work
# (zram, IOMMU karg, hw-specific kargs) isn't applicable on PS5
# anyway — we set our own cmdline in /boot/efi/cmdline.txt.
mkdir -p /etc/bazzite
jq -r '."image-name"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/image_name
jq -r '."image-branch"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/image_branch
jq -r '."fedora-version"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/fedora_version
grep -oP '^HWS_VER=\K[0-9]+' /usr/libexec/bazzite-hardware-setup > /etc/bazzite/hws_version
systemctl mask bazzite-hardware-setup.service 2>/dev/null || true
# User setup. Bazzite exposes video/audio/input/render via
# systemd-userdbd, so `getent group video` returns a row —
# and `groupadd -f` short-circuits as "already exists" and
# does nothing. But useradd reads /etc/group directly (no
# NSS), sees an empty file, and bails with "group X does not
# exist". Materialize each group into /etc/group ourselves,
# preserving the NSS-assigned GID when there is one so
# existing file ownerships in the rootfs stay correct.
passwd -l root
# Ensure /etc/gshadow exists with the right perms; useradd
# refuses to "prepare new entry" silently if it's missing.
[ -e /etc/gshadow ] || { touch /etc/gshadow; chmod 0 /etc/gshadow; }
for g in wheel video audio input render; do
if ! grep -q "^${g}:" /etc/group; then
gid=$(getent group "$g" 2>/dev/null | cut -d: -f3 || true)
if [ -z "$gid" ]; then
# Pick the next free system gid (100-999).
gid=$(awk -F: 'BEGIN{m=100} $3>=100 && $3<1000 && $3>m {m=$3} END{print m+1}' /etc/group)
fi
echo "${g}:x:${gid}:" >> /etc/group
fi
# Always make sure /etc/gshadow has a row.
grep -q "^${g}:" /etc/gshadow || echo "${g}:!::" >> /etc/gshadow
done
if ! id ps5 >/dev/null 2>&1; then
useradd -m -s /bin/bash -G wheel,video,audio,input,render ps5
fi
echo "ps5:ps5" | chpasswd
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoers || true
# Install pieces Bazzite's slim OCI image is missing.
# cloud-utils-growpart + parted: grow-rootfs needs growpart
# and partprobe — without them the rootfs stays sized to
# the build image (~10GB) on whatever USB it lands on.
# plasma-systemmonitor + ksystemstats: standard Plasma
# "System Monitor" app. Bazzite's container drops it.
# kdiff3 / gwenview / ark / okular / spectacle: rest of
# the Plasma utilities most people expect.
# chrony: NTP. PS5's RTC is wrong on boot; without an NTP
# client the system clock is years off and TLS breaks.
dnf install -y --setopt=install_weak_deps=False \
cloud-utils-growpart parted \
plasma-systemmonitor ksystemstats \
kdiff3 gwenview ark okular spectacle \
chrony \
|| echo "WARN: dnf install failed; some pkgs may be missing"
# Services
systemctl enable grow-rootfs.service NetworkManager sshd 2>/dev/null || true
# Time sync. Prefer systemd-timesyncd if present (lighter);
# fall back to chrony (which we just dnf-installed).
systemctl enable systemd-timesyncd 2>/dev/null \
|| systemctl enable chronyd 2>/dev/null || true
# Virtual terminals. Bazzite's preset disables getty@tty2-6;
# explicitly enable them so Ctrl+Alt+F2..F6 give text consoles.
for n in 2 3 4 5 6; do
systemctl enable getty@tty${n}.service 2>/dev/null || true
done
# Default DM (Bazzite ships KDE Plasma + SDDM)
systemctl enable sddm 2>/dev/null || systemctl enable gdm 2>/dev/null || true
# resolv.conf -> systemd-resolved stub
rm -f /etc/resolv.conf
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# Steam Deck UI's "Switch to Desktop" button calls SteamOS-
# Manager's SetTemporarySession(s) dbus method, which writes
# Session=<bare-alias> (literally "desktop"/"gamescope") into
# /etc/sddm.conf.d/zzt-steamos-temp-login.conf. That conf
# sorts AFTER zz-steamos-autologin.conf so it wins precedence
# at autologin time — but SDDM has no `desktop.desktop`
# session to resolve the alias to, so the button silently
# no-ops and the user stays on gamescope. (The bash
# steamos-session-select tool works fine because it resolves
# aliases itself before writing — only the dbus path is
# broken.) Fix it with alias symlinks SDDM can follow.
for cand in plasma-steamos-wayland-oneshot.desktop \
gnome-wayland-oneshot.desktop plasma.desktop; do
if [ -e "/usr/share/wayland-sessions/$cand" ]; then
ln -sf "$cand" /usr/share/wayland-sessions/desktop.desktop
break
fi
done
for cand in gamescope-session.desktop gamescope-session-plus.desktop; do
if [ -e "/usr/share/wayland-sessions/$cand" ]; then
ln -sf "$cand" /usr/share/wayland-sessions/gamescope.desktop
break
fi
done
# Autologin straight into Bazzite's gamescope session (Steam
# Big-Picture / Deck UI) — Bazzite is gaming-focused, and a
# field report said it landed on the Plasma desktop instead
# of gamemode. Pick whichever gamescope session file exists,
# fall back to plasma if Bazzite stripped them.
mkdir -p /etc/sddm.conf.d
SESSION=plasma
for s in gamescope-session-plus.desktop gamescope-session.desktop steam-wayland.desktop; do
if [ -e "/usr/share/wayland-sessions/$s" ] || [ -e "/usr/share/xsessions/$s" ]; then
SESSION="${s%.desktop}"
break
fi
done
cat > /etc/sddm.conf.d/autologin.conf <<SDDM
[Autologin]
User=ps5
Session=$SESSION
SDDM
# Gamescope-session fallback. Field report: bazzite-deck boots
# to a black screen on PS5 because gamescope can't grab the
# display (PSP/TA + Salina HDMI bridge weirdness — workaround
# is `steamos-session-select plasma` from a VT). Automate it:
# first-boot oneshot waits 60s for a gamescope process; if
# nothing shows up, flip the session to plasma and bounce
# SDDM. Only arm this when the chosen session is gamescope-
# flavoured. After first boot the user owns session choice
# via the standard steamos-session-select tool + the desktop
# shortcut we drop below.
case "$SESSION" in gamescope*|steam-wayland*)
mkdir -p /usr/local/sbin /etc/systemd/system/graphical.target.wants
cat > /usr/local/sbin/ps5-gamescope-recovery <<'POKE'
#!/bin/bash
# Wait up to 60s for gamescope to actually grab a display. If it doesn't,
# the user is staring at a black screen — fall back to plasma and bounce
# the display manager so they get a usable login session.
for _ in $(seq 1 60); do
sleep 1
pgrep -x gamescope >/dev/null 2>&1 && exit 0
done
logger -t ps5-gamescope-recovery "gamescope didn't start within 60s, switching to plasma"
runuser -u ps5 -- steamos-session-select plasma 2>/dev/null \
|| sed -i 's/^Session=.*/Session=plasma/' /etc/sddm.conf.d/autologin.conf
systemctl restart sddm
POKE
chmod +x /usr/local/sbin/ps5-gamescope-recovery
cat > /etc/systemd/system/ps5-gamescope-recovery.service <<RECOV
[Unit]
Description=Fall back to plasma if gamescope can't grab a display (first boot)
After=graphical.target
ConditionFirstBoot=yes
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ps5-gamescope-recovery
RemainAfterExit=no
[Install]
WantedBy=graphical.target
RECOV
ln -sf ../ps5-gamescope-recovery.service \
/etc/systemd/system/graphical.target.wants/ps5-gamescope-recovery.service
# Desktop shortcut so the user can opt back into gamescope
# after a recovery (or after switching to plasma manually).
mkdir -p /home/ps5/Desktop
cat > /home/ps5/Desktop/Switch-to-Gamescope.desktop <<DESK
[Desktop Entry]
Version=1.0
Type=Application
Name=Switch to Gamescope (Big Picture)
Comment=Switch the autologin session back to gamescope / Steam Deck UI
Exec=bash -c 'steamos-session-select gamescope && systemctl restart sddm'
Icon=steam
Terminal=false
Categories=System;
DESK
chmod +x /home/ps5/Desktop/Switch-to-Gamescope.desktop
chown -R ps5:ps5 /home/ps5/Desktop 2>/dev/null || \
chown -R 1000:1000 /home/ps5/Desktop
;;
esac
# DTM TA race workaround. amdgpu's display-topology TA
# (Trusted Application) loads async via PSP; if DRM probes
# connectors before that finishes, you get
# [drm] Failed to add display topology, DTM TA is not initialized
# and the screen stays dark until the user manually toggles
# VT (ctrl+alt+F7 -> ctrl+alt+F1) which forces a re-probe.
# Mimic that automatically: after amdgpu binds, wait a beat
# then re-trigger DRM connector detection.
mkdir -p /usr/local/sbin /etc/udev/rules.d
cat > /usr/local/sbin/ps5-amdgpu-reprobe <<'POKE'
#!/bin/sh
# Wait for PSP/TA firmware to settle, then re-probe DRM connectors.
# Equivalent of the ctrl+alt+F7 / ctrl+alt+F1 dance.
(
sleep 3
for c in /sys/class/drm/card*-*/status; do
[ -w "$c" ] && echo detect > "$c"
done
) &
POKE
chmod +x /usr/local/sbin/ps5-amdgpu-reprobe
cat > /etc/udev/rules.d/70-ps5-amdgpu-reprobe.rules <<'UDEV'
# Re-trigger DRM hotplug after amdgpu binds, so the DTM TA-not-initialized
# race doesn't leave the user with a dark screen until they manually VT-cycle.
SUBSYSTEM=="drm", ACTION=="add", KERNEL=="card[0-9]*", RUN+="/usr/local/sbin/ps5-amdgpu-reprobe"
UDEV
BAZIN
# explicit cleanup (the trap covers the failure path)
cleanup_bazzite_mounts
trap - RETURN ERR EXIT
-18
View File
@@ -1,18 +0,0 @@
#!/bin/bash
# Grows the root partition and filesystem to fill the disk.
# Runs once on first boot, then disables itself.
ROOT_DEV=$(findmnt -no SOURCE /) || { echo "Cannot find root device"; exit 1; }
DISK=$(lsblk -ndo PKNAME "$ROOT_DEV")
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition 2>/dev/null)
if [ -z "$DISK" ] || [ -z "$PART_NUM" ]; then
echo "Cannot determine disk layout (DISK=$DISK PART_NUM=$PART_NUM)"
exit 1
fi
growpart "/dev/$DISK" "$PART_NUM" || true
partprobe "/dev/$DISK"
resize2fs "$ROOT_DEV"
systemctl disable grow-rootfs.service
-14
View File
@@ -1,14 +0,0 @@
[Unit]
Description=Grow root filesystem to fill disk
After=systemd-remount-fs.service
# grow-rootfs uses findmnt + growpart on the live /, which needs the
# rootfs mounted RW and userspace tooling available. Drop the
# initramfs-era ordering the previous version used.
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/grow-rootfs
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

Some files were not shown because too many files have changed in this diff Show More