mirror of
https://github.com/ps5-linux/ps5-linux-image.git
synced 2026-07-15 21:42:27 +00:00
Adds `--distro steamos` as a first-class build target. Pattern is the
same as bazzite-deck/batocera: distros/steamos/build-rootfs.sh fetches
the upstream rootfs (Valve's Steam Deck recovery .img.bz2 from
steamdeck-images.steamos.cloud), extracts the rootfs-A + var-A btrfs
slots into $CHROOT, swaps in our linux-ps5 kernel pkg.tar.zst, and
rebuilds the initramfs with the modules amdgpu needs for the PS5
Oberon GPU.
PS5-specific fixes applied to the SteamOS rootfs:
- Replace /usr/bin/steamos-session-select with a shim that writes
/etc/sddm.conf.d/zzz-session-override.conf directly + restarts sddm.
The vendored helper goes through steamosctl / steamos-manager whose
Deck-hardware code paths silently no-op on PS5, leaving the UI stuck
at "Switching to Desktop..." with no actual session swap.
- Install a small Python dbus monitor (steam-session-switch-listener)
that catches org.freedesktop.DisplayManager.Seat.SwitchToUser from
Big Picture's "Switch to Desktop" button and writes the same
override file. No -steamos3 / -steampal needed (those trigger Steam's
A/B update reboot loop on a non-Deck box).
- Drop a Return-to-Gaming-Mode.desktop into /home/deck/Desktop pointing
at steamos-session-select gamescope.
- chown /home/deck to deck:deck (the recovery image leaves it root:root,
which breaks ~/.config writability on first Plasma launch).
- Enable sshd + set deck:deck so the box is reachable for further
iteration without dragging a USB keyboard around.
- Patch /etc/fstab: comment out the by-partsets atomic mount entries
(those paths don't exist on a flat-rootfs flash), append a LABEL-based
/efi mount so the FAT boot partition comes up cleanly. Required for
ps5-iw620-firmware.service to find the WLAN blob.
- Append amdgpu.dpm=0 + amdgpu.gpu_recovery=0 to the kernel cmdline.
modprobe.d alone isn't enough — amdgpu loads before the rootfs conf
is read, so the cmdline is the only reliable path. Without these
Switch-to-Desktop produces a black screen on the PS5 HDMI output.
- Polkit-deny the deck user reboot/poweroff to break Steam's update
reboot loop on first boot.
- Patch steam-jupiter to skip its OOBE rm -rf path. The OOBE assumes
Deck factory layout and wipes /home/deck/.local if it runs.
Verified end-to-end on real PS5 (HW-rev EA-1102): autologin lands in
gamescope, Switch-to-Desktop and Return-to-Gaming both work, sshd is
reachable, ps5-iw620 brings up internal WiFi after firmware staging.
Files:
- distros/steamos/build-rootfs.sh (646 lines)
- distros/steamos/README.md
- distros/steamos/return-to-gaming-mode.desktop
- distros/steamos/steam-session-switch-listener.{py,service}
- build_image.sh: steamos → FORMAT=arch (uses linux-ps5 pkg.tar.zst)
- .github/workflows/build-image.yml: steamos in matrix options + distros list
294 lines
11 KiB
YAML
294 lines
11 KiB
YAML
name: Build Image
|
|
run-name: Build Image (${{ inputs.distro || 'all' }})
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
distro:
|
|
description: 'Distribution'
|
|
default: 'ubuntu2604'
|
|
type: choice
|
|
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
|
|
workflow_call:
|
|
inputs:
|
|
distro:
|
|
type: string
|
|
default: 'ubuntu2604'
|
|
secrets:
|
|
R2_ACCESS_KEY_ID:
|
|
required: true
|
|
R2_SECRET_ACCESS_KEY:
|
|
required: true
|
|
R2_ENDPOINT:
|
|
required: true
|
|
R2_BUCKET:
|
|
required: true
|
|
|
|
jobs:
|
|
matrix:
|
|
runs-on: self-hosted
|
|
outputs:
|
|
distros: ${{ steps.set.outputs.distros }}
|
|
steps:
|
|
- id: set
|
|
run: |
|
|
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
|
|
if [ "$INPUT" = "all" ]; then
|
|
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
build:
|
|
needs: matrix
|
|
runs-on: self-hosted
|
|
timeout-minutes: 720
|
|
permissions:
|
|
contents: write
|
|
|
|
strategy:
|
|
fail-fast: true
|
|
max-parallel: 5
|
|
matrix:
|
|
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
|
|
|
|
env:
|
|
CCACHE_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:
|
|
path: image
|
|
|
|
- name: Build ${{ matrix.distro }} image
|
|
run: |
|
|
bash image/build_image.sh --distro "${{ matrix.distro }}"
|
|
|
|
- name: Set version info
|
|
id: version
|
|
run: |
|
|
psha=$(git -C image/work/ps5-linux-patches rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
kver=$(cat image/linux-bin/VERSION 2>/dev/null || echo "unknown")
|
|
ts=$(date -u +%Y%m%d-%H%M%S)
|
|
echo "psha=$psha" >> "$GITHUB_OUTPUT"
|
|
echo "kver=$kver" >> "$GITHUB_OUTPUT"
|
|
echo "ts=$ts" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Report
|
|
run: |
|
|
ls -lh image/output/*.img
|
|
sha256sum image/output/*.img
|
|
|
|
- name: Compress image
|
|
run: |
|
|
xz -T0 -4 -c image/output/ps5-${{ matrix.distro }}.img \
|
|
> image/output/ps5-${{ matrix.distro }}.img.xz
|
|
|
|
- name: Upload to R2
|
|
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
|
|
env:
|
|
RCLONE_CONFIG_R2_TYPE: s3
|
|
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
|
|
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
|
|
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: true
|
|
run: |
|
|
rclone copyto image/output/ps5-${{ matrix.distro }}.img.xz \
|
|
"r2:${{ secrets.R2_BUCKET }}/ps5-${{ matrix.distro }}.img.xz"
|
|
|
|
- name: Save build metadata
|
|
run: |
|
|
mkdir -p meta
|
|
sha256sum image/output/ps5-${{ matrix.distro }}.img.xz \
|
|
| sed 's|image/output/||' > meta/${{ matrix.distro }}.sha256
|
|
echo "${{ steps.version.outputs.kver }}" > meta/kver
|
|
echo "${{ steps.version.outputs.psha }}" > meta/psha
|
|
echo "${{ steps.version.outputs.ts }}" > meta/ts
|
|
|
|
- name: Upload build metadata
|
|
uses: actions/upload-artifact@v6
|
|
with:
|
|
name: meta-${{ matrix.distro }}
|
|
path: meta/
|
|
retention-days: 1
|
|
|
|
release:
|
|
needs: build
|
|
runs-on: self-hosted
|
|
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Download build metadata
|
|
uses: actions/download-artifact@v6
|
|
with:
|
|
pattern: meta-*
|
|
path: meta
|
|
merge-multiple: true
|
|
|
|
- name: Prepare release body
|
|
id: body
|
|
uses: actions/github-script@v8
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
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' },
|
|
];
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
|
|
- name: Create or update release
|
|
uses: softprops/action-gh-release@v3
|
|
with:
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
tag_name: latest
|
|
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}`);
|