mirror of
https://github.com/ps5-linux/ps5-linux-image.git
synced 2026-07-16 06:00:42 +00:00
Compare commits
77 Commits
feature/de
...
latest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38c61af4a0 | ||
|
|
94284d3c14 | ||
|
|
3c3be20587 | ||
|
|
b67f9a5a47 | ||
|
|
34fa9da3f1 | ||
|
|
74f978b1bb | ||
|
|
d763b8496f | ||
|
|
babdcf0762 | ||
|
|
3db2fb65b3 | ||
|
|
415d6e9ee8 | ||
|
|
d7d3b22c1f | ||
|
|
7b8a097651 | ||
|
|
7f8d0302b7 | ||
|
|
44bd2b4cf5 | ||
|
|
d891b0c01e | ||
|
|
f05b110a46 | ||
|
|
6a10749945 | ||
|
|
4c27d97ce0 | ||
|
|
b17ce46319 | ||
|
|
af8c7d472f | ||
|
|
bfdb43f1a1 | ||
|
|
33c6bc2660 | ||
|
|
7f4b0009d0 | ||
|
|
9f9fd3fd7f | ||
|
|
eafb7fbd56 | ||
|
|
21f86f562e | ||
|
|
de5d60da56 | ||
|
|
9fa102e098 | ||
|
|
096675b60a | ||
|
|
00d89868c8 | ||
|
|
b1aa5e5b50 | ||
|
|
b2c77d5cb2 | ||
|
|
f8d0b65b36 | ||
|
|
875bb35fef | ||
|
|
7e42201d88 | ||
|
|
0367dcb89c | ||
|
|
98892eec6b | ||
|
|
9350227d7b | ||
|
|
b10c0b2855 | ||
|
|
bf96770651 | ||
|
|
7f5dc8df0c | ||
|
|
7fbb7df59d | ||
|
|
247e4ea01b | ||
|
|
71bce4c5cb | ||
|
|
a32505e94d | ||
|
|
fe4e686d62 | ||
|
|
e1f17f7e38 | ||
|
|
f090f170a0 | ||
|
|
9682cada8d | ||
|
|
ccc30f19b4 | ||
|
|
34df7b0b64 | ||
|
|
76824ba725 | ||
|
|
7d5efa0583 | ||
|
|
76d3ba281e | ||
|
|
177cc5cfbf | ||
|
|
ddfda4de88 | ||
|
|
ed54e99482 | ||
|
|
50d8ec0b03 | ||
|
|
5f7cfddd3f | ||
|
|
47b35f48dc | ||
|
|
2ea14c3e0c | ||
|
|
f624aef764 | ||
|
|
1c629a881e | ||
|
|
90f06ee77b | ||
|
|
b68aa667d1 | ||
|
|
a1e2bc92e5 | ||
|
|
0adaf1c6d6 | ||
|
|
d0efec98fe | ||
|
|
7f905df12c | ||
|
|
daf634a6ed | ||
|
|
703dc736e9 | ||
|
|
dc365dda15 | ||
|
|
8160bc649b | ||
|
|
1ea662d598 | ||
|
|
04378b78f4 | ||
|
|
0e1219f422 | ||
|
|
33189d7e7b |
13
.gitattributes
vendored
Normal file
13
.gitattributes
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
* 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
|
||||
218
.github/workflows/build-image.yml
vendored
218
.github/workflows/build-image.yml
vendored
@@ -8,19 +8,13 @@ on:
|
||||
description: 'Distribution'
|
||||
default: 'ubuntu2604'
|
||||
type: choice
|
||||
options: [all, ubuntu2604, arch, cachyos]
|
||||
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
|
||||
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:
|
||||
@@ -40,7 +34,7 @@ jobs:
|
||||
run: |
|
||||
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
|
||||
if [ "$INPUT" = "all" ]; then
|
||||
echo 'distros=["ubuntu2604","arch","cachyos"]' >> "$GITHUB_OUTPUT"
|
||||
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -48,26 +42,54 @@ jobs:
|
||||
build:
|
||||
needs: matrix
|
||||
runs-on: self-hosted
|
||||
timeout-minutes: 240
|
||||
timeout-minutes: 720
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
|
||||
|
||||
env:
|
||||
CACHE_DIR: /home/opc/ccache
|
||||
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:
|
||||
repository: ps5-linux/ps5-linux-image
|
||||
path: image
|
||||
ref: main
|
||||
|
||||
- name: Build ${{ matrix.distro }} image
|
||||
run: |
|
||||
@@ -90,7 +112,7 @@ jobs:
|
||||
|
||||
- name: Compress image
|
||||
run: |
|
||||
xz -T0 -9 -c image/output/ps5-${{ matrix.distro }}.img \
|
||||
xz -T0 -4 -c image/output/ps5-${{ matrix.distro }}.img \
|
||||
> image/output/ps5-${{ matrix.distro }}.img.xz
|
||||
|
||||
- name: Upload to R2
|
||||
@@ -122,27 +144,6 @@ 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
|
||||
@@ -159,32 +160,112 @@ jobs:
|
||||
|
||||
- name: Prepare release body
|
||||
id: body
|
||||
run: |
|
||||
KVER=$(cat meta/kver)
|
||||
PSHA=$(cat meta/psha)
|
||||
TS=$(cat meta/ts)
|
||||
SUMS=$(cat meta/*.sha256)
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
{
|
||||
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"
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
- name: Create or update release
|
||||
uses: softprops/action-gh-release@v3
|
||||
@@ -194,3 +275,20 @@ 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
.github/workflows/move-release-tag.yml
vendored
Normal file
56
.github/workflows/move-release-tag.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
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
.github/workflows/test-uploads.yml
vendored
34
.github/workflows/test-uploads.yml
vendored
@@ -65,24 +65,6 @@ 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
|
||||
@@ -154,19 +136,3 @@ 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
.github/workflows/trigger-builds.yml
vendored
3
.github/workflows/trigger-builds.yml
vendored
@@ -17,9 +17,6 @@ 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 }}
|
||||
|
||||
72
.github/workflows/upstream-patches-watch.yml
vendored
Normal file
72
.github/workflows/upstream-patches-watch.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
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');
|
||||
19
README.md
19
README.md
@@ -1,11 +1,11 @@
|
||||
# PS5 Linux Image Builder
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker (with permission to run `--privileged` containers) — install as per your distro's instructions
|
||||
- ~30GB free disk space
|
||||
- ~30GB free disk space for Ubuntu, Arch, or CachyOS
|
||||
|
||||
Once Docker is installed, add your user to the docker group and apply it without logging out:
|
||||
|
||||
@@ -27,6 +27,13 @@ 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
|
||||
```
|
||||
@@ -43,12 +50,12 @@ sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, or `all` | `ubuntu2604` |
|
||||
| `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` |
|
||||
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, `fedora`, or `all` | `ubuntu2604` |
|
||||
| `--kernel` | Path to kernel source directory | auto-clone version selected by PS5 patch set |
|
||||
| `--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.0` |
|
||||
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.2` |
|
||||
|
||||
## Caching
|
||||
|
||||
@@ -128,7 +135,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
|
||||
|
||||
@@ -9,7 +9,7 @@ KERNEL_SRC=""
|
||||
CLEAN=false
|
||||
IMG_SIZE=12000
|
||||
KERNEL_ONLY=false
|
||||
PATCHES_REF="v1.1"
|
||||
PATCHES_REF="kernel-7.1.3-e81e281"
|
||||
|
||||
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, all (default: ubuntu2604)"
|
||||
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, batocera, 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.0)"
|
||||
echo " --patches-ref Branch, tag, or commit SHA for patches (default: v1.2)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -65,8 +65,31 @@ 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) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
|
||||
case "$DISTRO" in
|
||||
arch|cachyos|steamos) FORMAT="arch" ;;
|
||||
fedora|bazzite*) FORMAT="rpm" ;;
|
||||
all) FORMAT="all" ;;
|
||||
*) FORMAT="deb" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
KERNEL_BUILDER_PLATFORM="linux/amd64"
|
||||
@@ -106,6 +129,7 @@ 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 ;;
|
||||
@@ -276,7 +300,7 @@ else
|
||||
|
||||
KERNEL_SRC="$(cd "$KERNEL_SRC" && pwd)"
|
||||
|
||||
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst
|
||||
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst "$KERNEL_OUT"/*.rpm
|
||||
|
||||
run_stage "Build kernel builder image" \
|
||||
docker build --platform "$KERNEL_BUILDER_PLATFORM" -t ps5-kernel-builder \
|
||||
@@ -310,6 +334,16 @@ 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
|
||||
@@ -321,7 +355,8 @@ fi
|
||||
|
||||
# --- Step 2: Build distribution image ---
|
||||
run_stage "Build image builder image" \
|
||||
docker build -t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
|
||||
docker build --pull --no-cache \
|
||||
-t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
|
||||
|
||||
if [ "$DISTRO" = "all" ]; then
|
||||
DOCKER_ARGS=(
|
||||
|
||||
@@ -1,14 +1,81 @@
|
||||
#!/bin/bash
|
||||
# Grows the root partition and filesystem to fill the disk.
|
||||
# Runs once on first boot, then disables itself.
|
||||
# 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.
|
||||
set -e
|
||||
|
||||
ROOT_DEV=$(findmnt -no SOURCE /)
|
||||
DISK="/dev/$(lsblk -ndo PKNAME "$ROOT_DEV")"
|
||||
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)
|
||||
|
||||
parted -s "$DISK" resizepart "$PART_NUM" 100%
|
||||
partprobe "$DISK"
|
||||
# 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.
|
||||
resize2fs "$ROOT_DEV"
|
||||
|
||||
systemctl disable grow-rootfs.service
|
||||
|
||||
@@ -35,6 +35,10 @@ packages:
|
||||
- wl-clipboard
|
||||
- xorg-xwayland
|
||||
|
||||
# Bluetooth
|
||||
- bluez
|
||||
- bluez-utils
|
||||
|
||||
# Audio / media
|
||||
- pipewire
|
||||
- wireplumber
|
||||
@@ -125,9 +129,12 @@ actions:
|
||||
pacman-key --init
|
||||
pacman-key --populate archlinux
|
||||
|
||||
# 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
|
||||
# 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
|
||||
pacman -Syy --noconfirm
|
||||
|
||||
|
||||
@@ -163,6 +170,8 @@ 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
distros/batocera/README.md
Normal file
28
distros/batocera/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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
distros/batocera/build-rootfs.sh
Executable file
369
distros/batocera/build-rootfs.sh
Executable file
@@ -0,0 +1,369 @@
|
||||
#!/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
distros/bazzite-deck/build-rootfs.sh
Symbolic link
1
distros/bazzite-deck/build-rootfs.sh
Symbolic link
@@ -0,0 +1 @@
|
||||
../bazzite/build-rootfs.sh
|
||||
1
distros/bazzite-deck/grow-rootfs
Symbolic link
1
distros/bazzite-deck/grow-rootfs
Symbolic link
@@ -0,0 +1 @@
|
||||
../bazzite/grow-rootfs
|
||||
1
distros/bazzite-deck/grow-rootfs.service
Symbolic link
1
distros/bazzite-deck/grow-rootfs.service
Symbolic link
@@ -0,0 +1 @@
|
||||
../bazzite/grow-rootfs.service
|
||||
1
distros/bazzite-deck/image.yaml
Symbolic link
1
distros/bazzite-deck/image.yaml
Symbolic link
@@ -0,0 +1 @@
|
||||
../bazzite/image.yaml
|
||||
30
distros/bazzite/README.md
Normal file
30
distros/bazzite/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# 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
distros/bazzite/build-rootfs.sh
Executable file
401
distros/bazzite/build-rootfs.sh
Executable file
@@ -0,0 +1,401 @@
|
||||
#!/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
distros/bazzite/grow-rootfs
Executable file
18
distros/bazzite/grow-rootfs
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/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
distros/bazzite/grow-rootfs.service
Normal file
14
distros/bazzite/grow-rootfs.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[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
|
||||
13
distros/bazzite/image.yaml
Normal file
13
distros/bazzite/image.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# Bazzite is an ostree/OCI atomic image — distrobuilder doesn't handle it.
|
||||
# This yaml is a placeholder for documentation; the actual build is custom
|
||||
# in docker/image-builder/entrypoint.sh under the `bazzite*)` case (skopeo
|
||||
# pull + umoci unpack + dnf-install our PS5 kernel RPM).
|
||||
image:
|
||||
name: ps5-bazzite
|
||||
distribution: bazzite
|
||||
release: stable
|
||||
description: Bazzite (uBlue gaming Fedora) with PS5 kernel — ostree-flattened
|
||||
architecture: x86_64
|
||||
|
||||
# Source ref consumed by entrypoint.sh:
|
||||
# upstream_ref: ghcr.io/ublue-os/bazzite:stable
|
||||
@@ -19,10 +19,17 @@
|
||||
export LIBSEAT_BACKEND=logind
|
||||
export WLR_LIBINPUT_NO_DEVICES=1
|
||||
|
||||
# Launch flags:
|
||||
# -gamepadui : controller-friendly Steam UI (Big Picture-like)
|
||||
# NOT -steamos3 : would enable SteamOS atomic-OS-update polling. CachyOS is
|
||||
# pacman-rolling, no atomupd manifest server, so Steam loops
|
||||
# "Updater apply error: 2: null" and the OOBE blocks on the
|
||||
# "Software updates" step. (Field-reported by users.) Use
|
||||
# `sudo pacman -Syu` for OS updates.
|
||||
exec /usr/bin/gamescope \
|
||||
--backend drm \
|
||||
-e \
|
||||
--prefer-output DP-1 \
|
||||
--sdr-gamut-wideness 0 \
|
||||
-- \
|
||||
/usr/bin/steam -gamepadui -steamos3
|
||||
/usr/bin/steam -gamepadui
|
||||
|
||||
23
distros/cachyos/files/steamos-session-select
Normal file → Executable file
23
distros/cachyos/files/steamos-session-select
Normal file → Executable file
@@ -65,14 +65,27 @@ sudo "$(realpath "$0")" "$session"
|
||||
|
||||
case "$session" in
|
||||
gamescope)
|
||||
# Plasma's compositor + shell run under systemd --user (session 2),
|
||||
# not the tty1 login session — terminate-session alone leaves them
|
||||
# holding DRM and gamescope can't grab the display. Kill them first,
|
||||
# then close the tty1 session so profile.d exec's gamescope-session-ps5.
|
||||
pkill -u steam plasmashell || true
|
||||
pkill -u steam kwin_wayland || true
|
||||
pkill -u steam kwin_x11 || true
|
||||
pkill -u steam Xwayland || true
|
||||
if [[ -n "${XDG_SESSION_ID:-}" ]]; then
|
||||
loginctl terminate-session "$XDG_SESSION_ID"
|
||||
else
|
||||
pkill -u steam kwin_wayland || true
|
||||
pkill -u steam kwin_x11 || true
|
||||
fi
|
||||
;;
|
||||
desktop|plasma|plasma-wayland|plasmax11)
|
||||
exec sudo systemctl reboot
|
||||
# Soft handoff: terminate the current session; agetty respawns
|
||||
# tty1 and profile.d reads next-session=plasma. Avoids the reboot
|
||||
# tax on return-to-desktop.
|
||||
if [[ -n "${XDG_SESSION_ID:-}" ]]; then
|
||||
loginctl terminate-session "$XDG_SESSION_ID"
|
||||
else
|
||||
pkill -u steam gamescope-session-ps5 || true
|
||||
pkill -u steam gamescope || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
esac
|
||||
@@ -43,6 +43,10 @@ packages:
|
||||
- konsole
|
||||
- dolphin
|
||||
|
||||
# Bluetooth
|
||||
- bluez
|
||||
- bluez-utils
|
||||
|
||||
# Audio / media
|
||||
- pipewire
|
||||
- wireplumber
|
||||
@@ -186,8 +190,12 @@ actions:
|
||||
# Enable #[multilib] / #Include block (\\\\ in YAML would break the sed address regex)
|
||||
sed -i '/^#\[multilib\]/,/^#Include/s/^#//' /etc/pacman.conf
|
||||
|
||||
curl -fsSL "https://archlinux.org/mirrorlist/?country=all&protocol=https&use_mirror_status=on" \
|
||||
| sed 's/^#Server/Server/' > /etc/pacman.d/mirrorlist
|
||||
# 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
|
||||
|
||||
pacman -Syy --noconfirm
|
||||
|
||||
@@ -226,6 +234,8 @@ actions:
|
||||
ln -sf /etc/systemd/system/grow-rootfs.service \
|
||||
/etc/systemd/system/local-fs.target.wants/grow-rootfs.service
|
||||
|
||||
systemctl enable bluetooth.service
|
||||
|
||||
# Stub out steamos-update so Steam's -steamos3 OOBE skips the SteamOS OTA
|
||||
# step cleanly. Exit 7 = "no update available" per Valve's convention.
|
||||
printf '#!/bin/bash\n# Stub: no SteamOS OTA on this image\nexit 7\n' \
|
||||
|
||||
1
distros/debian/cmdline.txt
Normal file
1
distros/debian/cmdline.txt
Normal file
@@ -0,0 +1 @@
|
||||
root=LABEL=__DISTRO__ rw rootwait console=ttyTitania0 console=tty0 mitigations=off idle=halt preempt=full
|
||||
19
distros/debian/grow-rootfs
Normal file
19
distros/debian/grow-rootfs
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Grows the root partition and filesystem to fill the disk.
|
||||
# Runs once on first boot, then disables itself.
|
||||
# If resize fails, the service stays enabled and retries next boot.
|
||||
|
||||
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
|
||||
10
distros/debian/grow-rootfs.service
Normal file
10
distros/debian/grow-rootfs.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Grow root filesystem to fill disk
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/grow-rootfs
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
252
distros/debian/image.yaml
Normal file
252
distros/debian/image.yaml
Normal file
@@ -0,0 +1,252 @@
|
||||
image:
|
||||
name: ps5-debian
|
||||
distribution: debian
|
||||
release: bookworm
|
||||
description: Debian 12 XFCE desktop for PS5
|
||||
architecture: x86_64
|
||||
|
||||
source:
|
||||
downloader: debootstrap
|
||||
url: http://deb.debian.org/debian
|
||||
variant: minbase
|
||||
keyserver: keyserver.ubuntu.com
|
||||
|
||||
packages:
|
||||
manager: apt
|
||||
update: true
|
||||
cleanup: true
|
||||
|
||||
repositories:
|
||||
- name: sources.list
|
||||
url: |-
|
||||
deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
|
||||
deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
|
||||
architectures:
|
||||
- amd64
|
||||
|
||||
sets:
|
||||
- packages:
|
||||
# XFCE desktop (proven on PS5 via the Kali image)
|
||||
- task-xfce-desktop
|
||||
- lightdm
|
||||
- lightdm-gtk-greeter
|
||||
- xserver-xorg
|
||||
- xserver-xorg-video-amdgpu
|
||||
- dbus-x11
|
||||
- x11-xserver-utils
|
||||
|
||||
# Bluetooth
|
||||
- bluetooth
|
||||
- bluez
|
||||
- bluez-tools
|
||||
|
||||
# Audio / media
|
||||
- pipewire
|
||||
- pipewire-pulse
|
||||
- wireplumber
|
||||
- pavucontrol
|
||||
|
||||
# GPU
|
||||
- mesa-vulkan-drivers
|
||||
- mesa-va-drivers
|
||||
|
||||
# Networking
|
||||
- network-manager
|
||||
- network-manager-gnome
|
||||
- openssh-server
|
||||
- iputils-ping
|
||||
- iw
|
||||
- wpasupplicant
|
||||
- wireless-regdb
|
||||
- rfkill
|
||||
|
||||
# Firmware + PS5 boot support
|
||||
- firmware-linux
|
||||
- firmware-linux-nonfree
|
||||
- firmware-amd-graphics
|
||||
- firmware-realtek
|
||||
- firmware-atheros
|
||||
- firmware-misc-nonfree
|
||||
- initramfs-tools
|
||||
- kexec-tools
|
||||
- kmod
|
||||
- busybox
|
||||
- zstd
|
||||
- systemd-zram-generator
|
||||
|
||||
# Apps
|
||||
- firefox-esr
|
||||
- thunar
|
||||
- mousepad
|
||||
- ristretto
|
||||
- xfce4-terminal
|
||||
- xfce4-screenshooter
|
||||
- file-roller
|
||||
- synaptic
|
||||
- gnome-disk-utility
|
||||
- htop
|
||||
|
||||
# Base utilities
|
||||
- sudo
|
||||
- cloud-guest-utils
|
||||
- parted
|
||||
- e2fsprogs
|
||||
- usbutils
|
||||
- pciutils
|
||||
- util-linux
|
||||
- curl
|
||||
- wget
|
||||
- git
|
||||
- ca-certificates
|
||||
- vim
|
||||
- nano
|
||||
- tmux
|
||||
- ethtool
|
||||
|
||||
# Build tools (for WiFi module — prebuilt, but keep headers usable)
|
||||
- build-essential
|
||||
- bc
|
||||
- bison
|
||||
- flex
|
||||
- libssl-dev
|
||||
- libelf-dev
|
||||
|
||||
# Fonts
|
||||
- fonts-dejavu
|
||||
- fonts-liberation2
|
||||
- fonts-noto
|
||||
- fontconfig
|
||||
action: install
|
||||
|
||||
files:
|
||||
- path: /etc/hostname
|
||||
generator: hostname
|
||||
|
||||
- path: /etc/hosts
|
||||
generator: hosts
|
||||
|
||||
- path: /etc/machine-id
|
||||
generator: dump
|
||||
|
||||
- path: /etc/kernel/postinst.d/zz-update-boot
|
||||
generator: copy
|
||||
source: /tmp/build-staging/zz-update-boot
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/fstab
|
||||
generator: copy
|
||||
source: /tmp/build-staging/fstab
|
||||
|
||||
- path: /usr/local/sbin/grow-rootfs
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/systemd/system/grow-rootfs.service
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs.service
|
||||
|
||||
- path: /opt/debs/
|
||||
generator: copy
|
||||
source: /tmp/build-staging/debs
|
||||
|
||||
actions:
|
||||
- trigger: post-unpack
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
echo 'Acquire::Retries "10";' > /etc/apt/apt.conf.d/80retry
|
||||
apt-get update
|
||||
|
||||
- trigger: post-update
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
passwd -l root
|
||||
|
||||
- trigger: post-packages
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Default user (ps5/ps5)
|
||||
useradd -m -G sudo,video,input,render -s /bin/bash ps5
|
||||
echo "ps5:ps5" | chpasswd
|
||||
|
||||
printf 'ps5\n' > /etc/hostname
|
||||
if grep -q '^127\.0\.1\.1' /etc/hosts; then
|
||||
sed -i 's/^127\.0\.1\.1.*/127.0.1.1 ps5/' /etc/hosts
|
||||
else
|
||||
printf '127.0.1.1 ps5\n' >> /etc/hosts
|
||||
fi
|
||||
|
||||
# LightDM autologin into XFCE
|
||||
mkdir -p /etc/lightdm/lightdm.conf.d
|
||||
cat > /etc/lightdm/lightdm.conf.d/50-ps5-autologin.conf <<'EOF'
|
||||
[Seat:*]
|
||||
autologin-user=ps5
|
||||
autologin-user-timeout=0
|
||||
user-session=xfce
|
||||
EOF
|
||||
|
||||
# SSH off by default (public default credentials)
|
||||
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
|
||||
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
systemctl disable ssh.service || true
|
||||
systemctl disable ssh.socket || true
|
||||
|
||||
systemctl set-default graphical.target
|
||||
systemctl enable NetworkManager
|
||||
systemctl enable lightdm
|
||||
systemctl enable bluetooth.service
|
||||
|
||||
ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
printf 'Etc/UTC\n' > /etc/timezone
|
||||
|
||||
# zram swap
|
||||
printf '[zram0]\nzram-size = ram / 2\ncompression-algorithm = zstd\n' > /etc/systemd/zram-generator.conf
|
||||
|
||||
# Disable screen blanking for demos
|
||||
install -d -m 0755 /home/ps5/.config/autostart
|
||||
cat > /home/ps5/.config/autostart/ps5-display.desktop <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=PS5 Display Defaults
|
||||
Exec=sh -c 'xset s off -dpms; xset s noblank'
|
||||
X-GNOME-Autostart-enabled=true
|
||||
EOF
|
||||
chown -R ps5:ps5 /home/ps5
|
||||
|
||||
- trigger: post-files
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
systemctl enable grow-rootfs.service
|
||||
mkdir -p /boot/efi
|
||||
|
||||
mkdir -p /etc/initramfs-tools/conf.d
|
||||
printf 'RESUME=none\n' > /etc/initramfs-tools/conf.d/resume
|
||||
|
||||
# Install the PS5-patched kernel
|
||||
dpkg -i /opt/debs/*.deb
|
||||
rm -rf /opt/debs
|
||||
apt-mark hold linux-ps5 || true
|
||||
|
||||
KVER=$(ls -1t /lib/modules | head -1)
|
||||
depmod -a "$KVER"
|
||||
|
||||
# NOTE: PS5 WiFi (IW620) is not built here. The linux-ps5 headers ship
|
||||
# host tools prebuilt on Ubuntu (needs glibc 2.38+) which Debian 12's
|
||||
# glibc 2.36 can't run. WiFi modules will be added as prebuilt artifacts
|
||||
# in a future update. Use ethernet or a USB WiFi dongle for now.
|
||||
|
||||
# Front-load amdgpu for PS5 display
|
||||
grep -qxF amdgpu /etc/initramfs-tools/modules || printf '\namdgpu\n' >> /etc/initramfs-tools/modules
|
||||
printf 'MODULES=most\nBUSYBOX=y\n' > /etc/initramfs-tools/conf.d/ps5-amdgpu
|
||||
update-initramfs -c -k "$KVER"
|
||||
/etc/kernel/postinst.d/zz-update-boot "$KVER"
|
||||
|
||||
mappings:
|
||||
architecture_map: debian
|
||||
1
distros/fedora/cmdline.txt
Normal file
1
distros/fedora/cmdline.txt
Normal file
@@ -0,0 +1 @@
|
||||
root=LABEL=__DISTRO__ rw rootwait console=ttyTitania0 console=tty0 video=DP-1:1920x1080@60 mitigations=off idle=halt preempt=full selinux=0
|
||||
19
distros/fedora/grow-rootfs
Normal file
19
distros/fedora/grow-rootfs
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Grows the root partition and filesystem to fill the disk.
|
||||
# Runs once on first boot, then disables itself.
|
||||
# If resize fails, the service stays enabled and retries next boot.
|
||||
|
||||
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
|
||||
10
distros/fedora/grow-rootfs.service
Normal file
10
distros/fedora/grow-rootfs.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Grow root filesystem to fill disk
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/grow-rootfs
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
280
distros/fedora/image.yaml
Normal file
280
distros/fedora/image.yaml
Normal file
@@ -0,0 +1,280 @@
|
||||
image:
|
||||
name: ps5-fedora
|
||||
distribution: fedora
|
||||
release: "44"
|
||||
description: Fedora GNOME (Wayland) desktop for PS5
|
||||
architecture: x86_64
|
||||
|
||||
source:
|
||||
downloader: fedora-http
|
||||
url: https://kojipkgs.fedoraproject.org
|
||||
skip_verification: true
|
||||
|
||||
packages:
|
||||
manager: dnf
|
||||
update: true
|
||||
cleanup: true
|
||||
|
||||
sets:
|
||||
- packages:
|
||||
# Full GNOME desktop + GDM display manager.
|
||||
- gnome-shell
|
||||
- gnome-session
|
||||
- gnome-terminal
|
||||
- gdm
|
||||
- mutter
|
||||
- nautilus
|
||||
- gnome-control-center
|
||||
- gnome-settings-daemon
|
||||
- gnome-keyring
|
||||
- gnome-backgrounds
|
||||
- gnome-text-editor
|
||||
- gnome-system-monitor
|
||||
- gnome-disk-utility
|
||||
- gnome-calculator
|
||||
- gnome-tweaks
|
||||
- gnome-extensions-app
|
||||
- gnome-software
|
||||
- gnome-initial-setup
|
||||
- file-roller
|
||||
- loupe
|
||||
- evince
|
||||
- baobab
|
||||
- xdg-desktop-portal-gnome
|
||||
- xdg-user-dirs-gtk
|
||||
- xorg-x11-server-Xwayland
|
||||
- polkit
|
||||
|
||||
# Bluetooth
|
||||
- bluez
|
||||
- bluez-tools
|
||||
|
||||
# Audio / media
|
||||
- pipewire
|
||||
- wireplumber
|
||||
- pipewire-pulseaudio
|
||||
|
||||
# Display / GPU (AMD) — userspace GL/Vulkan only; the kernel amdgpu driver
|
||||
# comes from the linux-ps5 rpm. amdgpu is loaded LATE (by udev after pivot),
|
||||
# exactly like the working Arch/Ubuntu images — NOT forced into the initramfs.
|
||||
- mesa-dri-drivers
|
||||
- mesa-vulkan-drivers
|
||||
- mesa-va-drivers
|
||||
- libinput
|
||||
|
||||
# Networking
|
||||
- NetworkManager
|
||||
- NetworkManager-wifi
|
||||
- network-manager-applet
|
||||
- linux-firmware
|
||||
- iw
|
||||
- wpa_supplicant
|
||||
- wireless-regdb
|
||||
- util-linux
|
||||
|
||||
# System
|
||||
- glibc-langpack-en
|
||||
- openssh-server
|
||||
- sudo
|
||||
- nano
|
||||
- dracut
|
||||
- kmod
|
||||
- e2fsprogs
|
||||
- parted
|
||||
- cloud-utils-growpart
|
||||
- kexec-tools
|
||||
- zram-generator-defaults
|
||||
- firefox
|
||||
|
||||
# Fonts
|
||||
- dejavu-sans-fonts
|
||||
- liberation-fonts
|
||||
- google-noto-sans-fonts
|
||||
- google-noto-emoji-fonts
|
||||
- fontconfig
|
||||
action: install
|
||||
|
||||
files:
|
||||
- path: /etc/hostname
|
||||
generator: hostname
|
||||
|
||||
- path: /etc/hosts
|
||||
generator: hosts
|
||||
|
||||
- path: /etc/machine-id
|
||||
generator: dump
|
||||
|
||||
- path: /etc/kernel/postinst.d/zz-update-boot
|
||||
generator: copy
|
||||
source: /tmp/build-staging/zz-update-boot
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/fstab
|
||||
generator: copy
|
||||
source: /tmp/build-staging/fstab
|
||||
|
||||
- path: /usr/local/sbin/grow-rootfs
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/systemd/system/grow-rootfs.service
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs.service
|
||||
|
||||
- path: /opt/rpms/
|
||||
generator: copy
|
||||
source: /tmp/build-staging/rpms
|
||||
|
||||
actions:
|
||||
- trigger: post-unpack
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
echo "retries=10" >> /etc/dnf/dnf.conf
|
||||
dnf makecache || true
|
||||
|
||||
- trigger: post-update
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
passwd -l root
|
||||
|
||||
- trigger: post-packages
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
fc-cache -f 2>/dev/null || true
|
||||
|
||||
# SSH — password auth only, root login via sudo
|
||||
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
|
||||
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
systemctl enable sshd
|
||||
systemctl enable NetworkManager
|
||||
systemctl enable gdm
|
||||
systemctl enable bluetooth.service
|
||||
systemctl set-default graphical.target
|
||||
|
||||
# The PS5 kernel patch writes 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, so that write oopses and /dev/dri never appears.
|
||||
# Fedora is the only distro shipping .xz firmware — Ubuntu/Arch use .zst
|
||||
# (writable vzalloc buffer) and Debian/Kali ship raw .bin, which is why
|
||||
# only this image black-screened. Keep the PS5 GPU firmware uncompressed.
|
||||
# linux-firmware dedupes identical blobs as symlinks (mec2 -> mec) and
|
||||
# unxz refuses symlinks, so materialize links first while targets exist.
|
||||
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
|
||||
ls -la cyan_skillfish*
|
||||
|
||||
- trigger: post-files
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
systemctl enable grow-rootfs.service
|
||||
|
||||
# Built in a chroot → filesystem never SELinux-labelled. Disable SELinux.
|
||||
mkdir -p /etc/selinux
|
||||
if [ -f /etc/selinux/config ]; then
|
||||
sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
|
||||
else
|
||||
printf 'SELINUX=disabled\nSELINUXTYPE=targeted\n' > /etc/selinux/config
|
||||
fi
|
||||
|
||||
# --- Default user + GDM autologin ---
|
||||
useradd -m -G wheel,video,input,render -s /bin/bash ps5
|
||||
echo "ps5:ps5" | chpasswd
|
||||
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/wheel
|
||||
|
||||
# GDM autologin so the GNOME desktop appears without a login prompt.
|
||||
mkdir -p /etc/gdm
|
||||
sed -i 's/^\[daemon\]/[daemon]\nAutomaticLoginEnable=True\nAutomaticLogin=ps5/' /etc/gdm/custom.conf 2>/dev/null || \
|
||||
cat > /etc/gdm/custom.conf <<'EOF'
|
||||
[daemon]
|
||||
AutomaticLoginEnable=True
|
||||
AutomaticLogin=ps5
|
||||
|
||||
[security]
|
||||
|
||||
[xdmcp]
|
||||
|
||||
[chooser]
|
||||
|
||||
[debug]
|
||||
EOF
|
||||
|
||||
# Skip gnome-initial-setup for the autologin user.
|
||||
mkdir -p /home/ps5/.config
|
||||
echo 'yes' > /home/ps5/.config/gnome-initial-setup-done
|
||||
chown -R ps5:ps5 /home/ps5
|
||||
|
||||
# zram swap
|
||||
printf '[zram0]\nzram-size = ram / 2\ncompression-algorithm = zstd\n' > /etc/systemd/zram-generator.conf
|
||||
|
||||
# --- Install custom kernel rpm (its %post builds initramfs + deploys boot) -
|
||||
# amdgpu is NOT forced into the initramfs — it loads late via udev, exactly
|
||||
# like the working Arch/Ubuntu images.
|
||||
mkdir -p /boot/efi
|
||||
rpm -ivh /opt/rpms/*.rpm
|
||||
rm -rf /opt/rpms
|
||||
KVER=$(ls -1t /lib/modules | head -1)
|
||||
[ -f "/boot/efi/bzImage" ] && [ -f "/boot/efi/initrd.img" ]
|
||||
|
||||
# --- PS5 internal WiFi: firmware-stage + autoload service ---
|
||||
# The mlan.ko/moal.ko modules and their modprobe.d options are already
|
||||
# shipped by linux-ps5.rpm (built by kernel-builder/build.sh, which now
|
||||
# applies all PS5 mwifiex patches in one place). The rpm ALSO ships
|
||||
# /etc/modules-load.d/moal which would auto-load moal at boot before
|
||||
# firmware has been copied from /boot/efi — kill that and let our
|
||||
# ps5-iw620.service do the copy+load in the right order.
|
||||
rm -f /etc/modules-load.d/moal
|
||||
install -d /usr/local/sbin /etc/systemd/system /etc/modprobe.d /usr/share/doc/ps5-iw620
|
||||
cat > /etc/modprobe.d/ps5-iw620-noautoload.conf <<'EOF'
|
||||
blacklist moal
|
||||
blacklist mlan
|
||||
EOF
|
||||
cat > /usr/local/sbin/ps5-iw620-load <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
FW_NAME=nxp/pcieuartiw620_combo_v1.bin
|
||||
FW_DST=/lib/firmware/$FW_NAME
|
||||
FW_SRC=
|
||||
for candidate in "/boot/efi/lib/$FW_NAME" "/boot/lib/$FW_NAME" "$FW_DST"; do
|
||||
[ -f "$candidate" ] && { FW_SRC="$candidate"; break; }
|
||||
done
|
||||
[ -z "$FW_SRC" ] && FW_SRC="$(find /boot /boot/efi -path "*/$FW_NAME" -type f 2>/dev/null | head -n 1)"
|
||||
[ -n "$FW_SRC" ] && [ -f "$FW_SRC" ] && install -D -m 0644 "$FW_SRC" "$FW_DST"
|
||||
[ -f "$FW_DST" ] || { echo "PS5 IW620 firmware not found; skipping"; exit 0; }
|
||||
modprobe -r moal mlan 2>/dev/null || true
|
||||
modprobe cfg80211 || true
|
||||
modprobe moal
|
||||
rfkill unblock all 2>/dev/null || true
|
||||
nmcli radio wifi on 2>/dev/null || true
|
||||
EOF
|
||||
chmod 0755 /usr/local/sbin/ps5-iw620-load
|
||||
cat > /etc/systemd/system/ps5-iw620.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Load PS5 IW620 internal WiFi
|
||||
RequiresMountsFor=/boot/efi
|
||||
Wants=systemd-udev-settle.service
|
||||
After=local-fs.target systemd-udev-settle.service
|
||||
Before=NetworkManager.service network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ps5-iw620-load
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
depmod -a "$KVER"
|
||||
systemctl enable ps5-iw620.service
|
||||
1
distros/proxmox/cmdline.txt
Normal file
1
distros/proxmox/cmdline.txt
Normal file
@@ -0,0 +1 @@
|
||||
root=LABEL=__DISTRO__ rw rootwait console=ttyTitania0 console=tty0 mitigations=off idle=halt preempt=full
|
||||
19
distros/proxmox/grow-rootfs
Normal file
19
distros/proxmox/grow-rootfs
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Grows the root partition and filesystem to fill the disk.
|
||||
# Runs once on first boot, then disables itself.
|
||||
# If resize fails, the service stays enabled and retries next boot.
|
||||
|
||||
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
|
||||
10
distros/proxmox/grow-rootfs.service
Normal file
10
distros/proxmox/grow-rootfs.service
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Grow root filesystem to fill disk
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/grow-rootfs
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
319
distros/proxmox/image.yaml
Normal file
319
distros/proxmox/image.yaml
Normal file
@@ -0,0 +1,319 @@
|
||||
image:
|
||||
name: ps5-proxmox
|
||||
distribution: debian
|
||||
release: bookworm
|
||||
description: Proxmox VE 8 (headless hypervisor) for PS5
|
||||
architecture: x86_64
|
||||
|
||||
source:
|
||||
downloader: debootstrap
|
||||
url: http://deb.debian.org/debian
|
||||
variant: minbase
|
||||
keyserver: keyserver.ubuntu.com
|
||||
|
||||
packages:
|
||||
manager: apt
|
||||
update: true
|
||||
cleanup: true
|
||||
|
||||
repositories:
|
||||
- name: sources.list
|
||||
url: |-
|
||||
deb http://deb.debian.org/debian bookworm main contrib non-free non-free-firmware
|
||||
deb http://deb.debian.org/debian bookworm-updates main contrib non-free non-free-firmware
|
||||
deb http://security.debian.org/debian-security bookworm-security main contrib non-free non-free-firmware
|
||||
architectures:
|
||||
- amd64
|
||||
- name: pve-no-subscription.list
|
||||
url: |-
|
||||
deb [arch=amd64] http://download.proxmox.com/debian/pve bookworm pve-no-subscription
|
||||
architectures:
|
||||
- amd64
|
||||
|
||||
sets:
|
||||
# NOTE: proxmox-ve itself is NOT installed here. It hard-depends on a Proxmox
|
||||
# kernel that cannot boot the PS5, so a post-packages action first installs an
|
||||
# equivs stub that satisfies that dependency, then installs proxmox-ve.
|
||||
- packages:
|
||||
# Build tooling for the equivs kernel stub + out-of-tree WiFi module
|
||||
- equivs
|
||||
- build-essential
|
||||
- bc
|
||||
- bison
|
||||
- flex
|
||||
- libssl-dev
|
||||
- libelf-dev
|
||||
- git
|
||||
|
||||
# Bluetooth
|
||||
- bluetooth
|
||||
- bluez
|
||||
- bluez-tools
|
||||
|
||||
# Networking (Proxmox uses ifupdown2 + a vmbr0 bridge)
|
||||
- ifupdown2
|
||||
- openssh-server
|
||||
- iproute2
|
||||
- iputils-ping
|
||||
- curl
|
||||
- wget
|
||||
- ca-certificates
|
||||
- chrony
|
||||
|
||||
# Firmware + PS5 boot support
|
||||
- firmware-linux
|
||||
- firmware-linux-nonfree
|
||||
- firmware-amd-graphics
|
||||
- initramfs-tools
|
||||
- kexec-tools
|
||||
- kmod
|
||||
- busybox
|
||||
- zstd
|
||||
- systemd-zram-generator
|
||||
|
||||
# Base utilities
|
||||
- sudo
|
||||
- cloud-guest-utils
|
||||
- parted
|
||||
- e2fsprogs
|
||||
- usbutils
|
||||
- pciutils
|
||||
- rfkill
|
||||
- util-linux
|
||||
- vim
|
||||
- nano
|
||||
- tmux
|
||||
- ethtool
|
||||
action: install
|
||||
|
||||
files:
|
||||
- path: /etc/hostname
|
||||
generator: hostname
|
||||
|
||||
- path: /etc/hosts
|
||||
generator: hosts
|
||||
|
||||
- path: /etc/machine-id
|
||||
generator: dump
|
||||
|
||||
- path: /etc/kernel/postinst.d/zz-update-boot
|
||||
generator: copy
|
||||
source: /tmp/build-staging/zz-update-boot
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/fstab
|
||||
generator: copy
|
||||
source: /tmp/build-staging/fstab
|
||||
|
||||
- path: /etc/apt/trusted.gpg.d/proxmox-release-bookworm.gpg
|
||||
generator: copy
|
||||
source: /tmp/build-staging/proxmox-release-bookworm.gpg
|
||||
|
||||
- path: /usr/local/sbin/grow-rootfs
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs
|
||||
mode: "0755"
|
||||
|
||||
- path: /etc/systemd/system/grow-rootfs.service
|
||||
generator: copy
|
||||
source: /tmp/build-staging/grow-rootfs.service
|
||||
|
||||
- path: /opt/debs/
|
||||
generator: copy
|
||||
source: /tmp/build-staging/debs
|
||||
|
||||
actions:
|
||||
- trigger: post-unpack
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
echo 'Acquire::Retries "10";' > /etc/apt/apt.conf.d/80retry
|
||||
# The pve repo is active during this first apt-get update, so its signing
|
||||
# key must be present now (the files: key copy happens later). Inline it.
|
||||
install -d /etc/apt/trusted.gpg.d
|
||||
cat > /etc/apt/trusted.gpg.d/proxmox-release-bookworm.asc <<'PVEKEY'
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Comment: Use "gpg --dearmor" for unpacking
|
||||
|
||||
mQINBGODZZwBEADMA2dbTBXHRkvaOApNhPSRhyuhVfImTCGrUEFMMaUZ0vrEZRf7
|
||||
wpG7MTVlrQ2gOMshieGU1Oo+Kat5z0MN3g5Q+tck/OG43NQXkoXUkfsV3fiGZ34d
|
||||
MyiNEYDJB3EcVnX+99OWYmhP2ZcY0rgkSxBFKYpwphclw0gTu7osFu8FB+xFykgi
|
||||
qqT5PjJryLg8ltE/srt7XTLRusMvPHdrw3OUF6xDIu7YsCsZ2CQu/5BlbWmbhG6J
|
||||
t3Du7la6t17RFa/jhdRuRPL37VXMLnvc4hQXxsyQgP13kkKXzSzNwgVKcJxzAz4Y
|
||||
eADAjtQmrnYnwRQahfiob7snTqtxdgE1pPBvSZS/1MXdjGU2nYFcuaOjXJKy2f8n
|
||||
tpjtXTkTiEDB36OF78K2E9OifrTuqliHylVrF5fPdNax993xcY/VA9DRaUp1WzQy
|
||||
7Aa95v25vfmdzRlEnlEmGKmXA0XJhUs+dy0vy+9uWwES9z9pL056FcH7NKfST/nF
|
||||
DwamTVWugKzhmADRSTIdiJ4hW9CfN7gFJxHsodmqUQ80EtJvjzzmtqXMNAv6Yu4o
|
||||
0H/9dNXlBZP4O4yazRWmZ9hcETbaupaP1sPGKdbYPaeU+eGDZkbhjAnYQXlg3h87
|
||||
nRlQUbWw/oa2CqBA7Z4udpQoeaTfogcHHiZSBIozy/LC5QfPKk/gu3PYPQARAQAB
|
||||
tDpQcm94bW94IEJvb2t3b3JtIFJlbGVhc2UgS2V5IDxwcm94bW94LXJlbGVhc2VA
|
||||
cHJveG1veC5jb20+iQJUBBMBCgA+FiEE9OE2xnzc5Brm3m/IEUCvj2OeDDkFAmOD
|
||||
ZZwCGwMFCRLMAwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQEUCvj2OeDDm5
|
||||
9w/8CCIfeNtNkrs9Q6WFZEd4Ot+an3UxU00M3QO74LAeLPj8wbCRG1iN3j19sv0e
|
||||
d6vSyLz9UX79HkiiAHta9GA15MmZa6uTrABBfF8xPpDUPadpPXSAQmaUhr3NgLIB
|
||||
6jUVWEoBuHpxSwE3DEGgNwypTqgAr0f30mr+iCOd3DcwgkhfIPwWX6GBRWEn8QUj
|
||||
U7M7jSm9ExtLGy+sBoXsFc4h8I2Q9Yrfe85oRZIHCRKsc1o8TuxvCB3YPntOSZJU
|
||||
VjV+o8PzTTjWhCjuY+OMKyiiOgbfrtsRhB4PzQ6ZG8655Q+QjAy9+boN0OO/lzRb
|
||||
/Jpup6zpOvOIWGouvZ77FtCquPzwBiOvxHm7wE2TTVTE48DJDdmzKNFaXf1DQMHA
|
||||
THLEiU4iI6KoBw+MYPCKSauas77dw4Ftm6jQA+BjtulzBMLT4nq65oQ8lAB7ukBH
|
||||
CYrI1qQIoGq0c3VuYcO36uW9kRI9InNSM6jymeZJ+SvrREvh+Izzwm1zf+oWtrw2
|
||||
cyFFF5pCtuaB3i2B1L0tPxi9NWEF7d3e43bkg10TK19Ea0UqgdnMdCHFvHDFz+BA
|
||||
LuYbTFey1WjOXavOEVfWkC0fpyGjFiMNWUp6FGrxfnOiG25hln+eCWWiWzcTgVJ6
|
||||
7mqm2XbzSMa8Z+7u6L+BTa8P7OZmuPyCjzIJtE1E1CXH+/o=
|
||||
=YZF1
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
PVEKEY
|
||||
apt-get update
|
||||
|
||||
- trigger: post-update
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
# Proxmox is headless and its web UI / console log in as root@pam, so root
|
||||
# MUST have a usable password (do NOT lock it like the desktop images).
|
||||
echo "root:proxmox" | chpasswd
|
||||
|
||||
- trigger: post-packages
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Proxmox requires the hostname to resolve to a non-loopback-style address.
|
||||
# The image is assembled inside Docker (hostname = container id), so pin a
|
||||
# stable name and a hosts entry BEFORE proxmox-ve's postinst runs.
|
||||
printf 'ps5pve\n' > /etc/hostname
|
||||
if grep -q '^127\.0\.1\.1' /etc/hosts; then
|
||||
sed -i 's/^127\.0\.1\.1.*/127.0.1.1 ps5pve.local ps5pve/' /etc/hosts
|
||||
else
|
||||
printf '127.0.1.1 ps5pve.local ps5pve\n' >> /etc/hosts
|
||||
fi
|
||||
hostname ps5pve || true
|
||||
|
||||
# Satisfy proxmox-ve's hard kernel dependency with an equivs stub so the
|
||||
# real (un-bootable on PS5) Proxmox kernel is never installed. The PS5
|
||||
# kernel .deb is installed later in post-files and provides the modules.
|
||||
cat > /tmp/ps5-kernel-stub.ctl <<'EOF'
|
||||
Section: misc
|
||||
Priority: optional
|
||||
Standards-Version: 3.9.2
|
||||
Package: ps5-proxmox-kernel-stub
|
||||
Version: 1.0
|
||||
Provides: proxmox-default-kernel, proxmox-kernel-6.8, pve-firmware, linux-image-amd64
|
||||
Description: PS5 kernel stub so proxmox-ve accepts the PS5-patched kernel
|
||||
Lets proxmox-ve install without pulling a Proxmox kernel that cannot boot
|
||||
on the PS5. The PS5-patched kernel is installed separately.
|
||||
EOF
|
||||
( cd /tmp && equivs-build ps5-kernel-stub.ctl )
|
||||
apt-get install -y /tmp/ps5-proxmox-kernel-stub_1.0_all.deb
|
||||
rm -f /tmp/ps5-proxmox-kernel-stub_1.0_all.deb /tmp/ps5-kernel-stub.ctl
|
||||
|
||||
# postfix would prompt interactively; preseed a no-op local config.
|
||||
echo "postfix postfix/main_mailer_type select No configuration" | debconf-set-selections
|
||||
|
||||
# Install Proxmox VE (no recommends keeps the image lean; the stub blocks
|
||||
# the PVE kernel + pve-firmware).
|
||||
apt-get install -y --no-install-recommends \
|
||||
-o Dpkg::Options::=--force-confold \
|
||||
proxmox-ve postfix open-iscsi
|
||||
|
||||
# Allow root password login (test image; default creds root/proxmox).
|
||||
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
||||
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
||||
|
||||
systemctl set-default multi-user.target
|
||||
systemctl enable ssh.service || true
|
||||
systemctl enable chrony || true
|
||||
systemctl enable bluetooth.service || true
|
||||
|
||||
# zram swap: half of RAM, zstd compressed
|
||||
printf '[zram0]\nzram-size = ram / 2\ncompression-algorithm = zstd\n' > /etc/systemd/zram-generator.conf
|
||||
|
||||
# Keep the shared image timezone-neutral.
|
||||
ln -sf /usr/share/zoneinfo/Etc/UTC /etc/localtime
|
||||
printf 'Etc/UTC\n' > /etc/timezone
|
||||
|
||||
- trigger: post-files
|
||||
action: |-
|
||||
#!/bin/bash
|
||||
set -eux
|
||||
systemctl enable grow-rootfs.service
|
||||
mkdir -p /boot/efi
|
||||
|
||||
# zram, not disk hibernation — don't embed a build-container resume target.
|
||||
mkdir -p /etc/initramfs-tools/conf.d
|
||||
printf 'RESUME=none\n' > /etc/initramfs-tools/conf.d/resume
|
||||
|
||||
# Install the PS5-patched kernel (image + modules + headers).
|
||||
dpkg -i /opt/debs/*.deb
|
||||
rm -rf /opt/debs
|
||||
apt-mark hold linux-ps5 || true
|
||||
|
||||
KVER=$(ls -1t /lib/modules | head -1)
|
||||
depmod -a "$KVER"
|
||||
|
||||
# --- First-boot network: bridge the PS5 ethernet into vmbr0 (DHCP) -------
|
||||
# Proxmox needs a vmbr0 bridge for VMs/containers. The PS5 ethernet name
|
||||
# varies, so detect it on first boot and write /etc/network/interfaces.
|
||||
cat > /etc/network/interfaces <<'EOF'
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
EOF
|
||||
cat > /usr/local/sbin/ps5-pve-net <<'EOF'
|
||||
#!/bin/bash
|
||||
# Detect the first wired interface and bridge it into vmbr0 with DHCP.
|
||||
set -eu
|
||||
IF=$(for i in /sys/class/net/en*; do [ -e "$i" ] || continue; basename "$i"; done | head -n1)
|
||||
[ -z "${IF:-}" ] && IF=$(for i in /sys/class/net/eth*; do [ -e "$i" ] || continue; basename "$i"; done | head -n1)
|
||||
if [ -n "${IF:-}" ] && ! grep -q 'vmbr0' /etc/network/interfaces; then
|
||||
cat >> /etc/network/interfaces <<NET
|
||||
|
||||
auto $IF
|
||||
iface $IF inet manual
|
||||
|
||||
auto vmbr0
|
||||
iface vmbr0 inet dhcp
|
||||
bridge-ports $IF
|
||||
bridge-stp off
|
||||
bridge-fd 0
|
||||
NET
|
||||
ifup vmbr0 || systemctl restart networking || true
|
||||
fi
|
||||
systemctl disable ps5-pve-net.service || true
|
||||
EOF
|
||||
chmod 0755 /usr/local/sbin/ps5-pve-net
|
||||
cat > /etc/systemd/system/ps5-pve-net.service <<'EOF'
|
||||
[Unit]
|
||||
Description=PS5 Proxmox first-boot network bridge setup
|
||||
After=local-fs.target
|
||||
Before=pveproxy.service
|
||||
ConditionPathExists=!/etc/network/interfaces.d/.ps5-net-done
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ps5-pve-net
|
||||
ExecStartPost=/bin/sh -c 'mkdir -p /etc/network/interfaces.d && touch /etc/network/interfaces.d/.ps5-net-done'
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable ps5-pve-net.service
|
||||
|
||||
# NOTE: PS5 internal WiFi (IW620) is intentionally NOT built for Proxmox.
|
||||
# Proxmox is a headless hypervisor whose VM/CT networking runs over the
|
||||
# vmbr0 ethernet bridge (WiFi interfaces cannot be bridged reliably), and
|
||||
# the trimmed linux-ps5 headers can't rebuild the kernel host tools on
|
||||
# Debian 12's older glibc. Connect the PS5 via ethernet. (The desktop
|
||||
# images that need WiFi run on newer-glibc bases where this works.)
|
||||
|
||||
depmod -a "$KVER"
|
||||
|
||||
# Front-load amdgpu so the PS5 console comes up during boot.
|
||||
grep -qxF amdgpu /etc/initramfs-tools/modules || printf '\namdgpu\n' >> /etc/initramfs-tools/modules
|
||||
printf 'MODULES=most\nBUSYBOX=y\n' > /etc/initramfs-tools/conf.d/ps5-amdgpu
|
||||
update-initramfs -c -k "$KVER"
|
||||
/etc/kernel/postinst.d/zz-update-boot "$KVER"
|
||||
|
||||
mappings:
|
||||
architecture_map: debian
|
||||
BIN
distros/proxmox/proxmox-release-bookworm.gpg
Normal file
BIN
distros/proxmox/proxmox-release-bookworm.gpg
Normal file
Binary file not shown.
@@ -5,6 +5,13 @@ set -e
|
||||
BOOT_PART="/boot/efi"
|
||||
KVER="$1"
|
||||
[ -z "$KVER" ] && KVER="$(ls -1t /lib/modules | head -1)"
|
||||
|
||||
if [ -f "$BOOT_PART/bzImage" ]; then
|
||||
mv "$BOOT_PART/bzImage" "$BOOT_PART/bzImage.old"
|
||||
fi
|
||||
if [ -f "$BOOT_PART/initrd.img" ]; then
|
||||
mv "$BOOT_PART/initrd.img" "$BOOT_PART/initrd.img.old"
|
||||
fi
|
||||
cp "/boot/vmlinuz-$KVER" "$BOOT_PART/bzImage"
|
||||
cp "/boot/initrd.img-$KVER" "$BOOT_PART/initrd.img"
|
||||
echo ">> Kernel $KVER deployed to $BOOT_PART"
|
||||
|
||||
@@ -9,11 +9,20 @@ KVER="$1"
|
||||
DISTRO="unknown"
|
||||
[ -f /etc/ps5-distro ] && DISTRO="$(cat /etc/ps5-distro)"
|
||||
|
||||
if [ -f "$BOOT_PART/bzImage" ]; then
|
||||
mv "$BOOT_PART/bzImage" "$BOOT_PART/bzImage.old"
|
||||
fi
|
||||
if [ -f "$BOOT_PART/initrd-${DISTRO}.img" ]; then
|
||||
mv "$BOOT_PART/initrd-${DISTRO}.img" "$BOOT_PART/initrd-${DISTRO}.img.old"
|
||||
fi
|
||||
cp "/boot/vmlinuz-$KVER" "$BOOT_PART/bzImage"
|
||||
cp "/boot/initrd.img-$KVER" "$BOOT_PART/initrd-${DISTRO}.img"
|
||||
|
||||
# Ubuntu 26.04 is default boot — also update the generic initrd.img
|
||||
if [ "$DISTRO" = "ubuntu2604" ]; then
|
||||
if [ -f "$BOOT_PART/initrd.img" ]; then
|
||||
mv "$BOOT_PART/initrd.img" "$BOOT_PART/initrd.img.old"
|
||||
fi
|
||||
cp "/boot/initrd.img-$KVER" "$BOOT_PART/initrd.img"
|
||||
fi
|
||||
|
||||
|
||||
42
distros/steamos/README.md
Normal file
42
distros/steamos/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# steamos
|
||||
|
||||
SteamOS 3 image variant for PS5, built by extracting the rootfs from
|
||||
Valve's official Steam Deck recovery image and swapping in our
|
||||
`linux-ps5` kernel.
|
||||
|
||||
## Source
|
||||
|
||||
- Upstream: <https://steamdeck-images.steamos.cloud/recovery/steamdeck-repair-latest.img.bz2>
|
||||
(Steam Deck recovery image; SteamOS Holo Arch-based.)
|
||||
- ~3.4 GB compressed bz2, ~10 GB decompressed.
|
||||
- Valve CDN — no per-IP throttle observed; first-time cache pull on a
|
||||
cold runner is ~3-5 min, subsequent runs are instant via
|
||||
`/data/cache/ps5/downloads/`.
|
||||
|
||||
## What the build does
|
||||
|
||||
1. Resolve `steamdeck-repair-latest.img.bz2` → its dated filename so
|
||||
the cache key is stable.
|
||||
2. Decompress → losetup -P.
|
||||
3. Mount the first **erofs** partition (rootfs-A; A and B are
|
||||
identical) read-only, `cp -a` to `$CHROOT`.
|
||||
4. Mount the first **ext4 < 2 GB** partition that contains `/lib`
|
||||
(var-A), `cp -a` to `$CHROOT/var` — SteamOS splits `/var` off the
|
||||
rootfs and first-boot fails without it.
|
||||
5. Extract our `linux-ps5-*.pkg.tar.zst` and merge `usr/`, `boot/`,
|
||||
`etc/` into `$CHROOT`.
|
||||
6. Delete SteamOS's `linux-neptune` kernel + modules so grub boots
|
||||
ours.
|
||||
7. `depmod -a $KVER`.
|
||||
|
||||
The standard image-builder flow then packs `$CHROOT` into an ext4
|
||||
image with our EFI partition.
|
||||
|
||||
## Caveats
|
||||
|
||||
- The Steam UI session expects the Deck's `jupiter` hardware quirks
|
||||
(gamepad, ALS, fan curve daemons) — most of those won't apply on
|
||||
PS5. Falling back to plasma desktop on the first session is normal.
|
||||
- SteamOS is read-only by design (steamos-readonly enable). The image
|
||||
we build here is regular ext4; if you want the read-only A/B atomic
|
||||
experience, that needs a separate variant.
|
||||
650
distros/steamos/build-rootfs.sh
Executable file
650
distros/steamos/build-rootfs.sh
Executable file
@@ -0,0 +1,650 @@
|
||||
#!/bin/bash
|
||||
# distros/steamos/build-rootfs.sh — fetch Valve's Steam Deck recovery image,
|
||||
# extract its rootfs to $CHROOT, swap in our linux-ps5 kernel. Called from
|
||||
# docker/image-builder/entrypoint.sh for DISTRO=steamos.
|
||||
#
|
||||
# SteamOS 3 (Holo, Arch-based) ships as a single .img.bz2 with multiple
|
||||
# partitions: A/B rootfs (btrfs on recovery, erofs on production), A/B
|
||||
# var (ext4), an EFI partition, and a home partition. We take the
|
||||
# rootfs-A + var-A slots, copy them flat to $CHROOT, then drop in our
|
||||
# linux-ps5 kernel pkg.tar.zst and rebuild the initramfs with the
|
||||
# modules amdgpu actually needs to bring up the PS5 Oberon GPU.
|
||||
#
|
||||
# Expects in env: DISTRO, CHROOT, KVER, ROOT_LABEL, EFI_LABEL
|
||||
# Expects on disk: /kernel-debs/linux-ps5-*.pkg.tar.zst,
|
||||
# /repo/distros/steamos/{return-to-gaming-mode.desktop,
|
||||
# steam-session-switch-listener.{py,service}},
|
||||
# /repo/distros/arch/{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
|
||||
|
||||
STEAMOS_URL="${STEAMOS_URL:-https://steamdeck-images.steamos.cloud/recovery/steamdeck-repair-latest.img.bz2}"
|
||||
|
||||
echo "=== SteamOS: resolve latest image URL ==="
|
||||
# python3 is in the upstream image-builder Dockerfile; curl isn't.
|
||||
RESOLVED_URL=$(python3 -c "import urllib.request; print(urllib.request.urlopen('$STEAMOS_URL').geturl())")
|
||||
IMG_NAME=$(basename "$RESOLVED_URL")
|
||||
echo ">> resolved $IMG_NAME"
|
||||
|
||||
CACHED="/build/cache/$IMG_NAME"
|
||||
if [ ! -s "$CACHED" ]; then
|
||||
echo ">> downloading $IMG_NAME (~3.4 GB from Valve CDN)"
|
||||
wget --tries=3 -O "$CACHED.part" "$RESOLVED_URL"
|
||||
mv "$CACHED.part" "$CACHED"
|
||||
fi
|
||||
echo ">> using $CACHED ($(du -h "$CACHED" | cut -f1))"
|
||||
|
||||
echo "=== SteamOS: decompress + loop ==="
|
||||
STEAMOS_IMG=/build/steamos-src.img
|
||||
bunzip2 -kc "$CACHED" > "$STEAMOS_IMG"
|
||||
SLOOP=$(losetup -Pf --show "$STEAMOS_IMG")
|
||||
sleep 1
|
||||
# kpartx fallback when loopNpX devices didn't appear under /dev/
|
||||
if [ ! -e "${SLOOP}p1" ]; then
|
||||
kpartx -av "$SLOOP"
|
||||
sleep 1
|
||||
fi
|
||||
# Build candidate partition list — either /dev/loopNpX or
|
||||
# /dev/mapper/loopNpX depending on how kpartx mapped them.
|
||||
SLOOP_BASE=$(basename "$SLOOP")
|
||||
PART_PATHS=()
|
||||
for cand in "${SLOOP}"p* "/dev/mapper/${SLOOP_BASE}"p*; do
|
||||
[ -e "$cand" ] && PART_PATHS+=("$cand")
|
||||
done
|
||||
|
||||
# The RECOVERY image uses btrfs for the rootfs (production
|
||||
# SteamOS uses erofs A/B; recovery needs writes so it's btrfs).
|
||||
# Take the first btrfs partition; that's rootfs. Fall back to
|
||||
# erofs in case Valve ever changes it.
|
||||
SROOT=""
|
||||
SROOT_FS=""
|
||||
for fstype in btrfs erofs; do
|
||||
for p in "${PART_PATHS[@]}"; do
|
||||
t=$(blkid -o value -s TYPE "$p" 2>/dev/null || echo "")
|
||||
if [ "$t" = "$fstype" ]; then
|
||||
SROOT="$p"; SROOT_FS="$fstype"; break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ -z "$SROOT" ]; then
|
||||
echo "ERROR: no btrfs/erofs rootfs in SteamOS image. partitions seen:"
|
||||
for p in "${PART_PATHS[@]}"; do
|
||||
echo " $p type=$(blkid -o value -s TYPE "$p" 2>/dev/null) size=$(blockdev --getsize64 "$p" 2>/dev/null)"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
echo ">> rootfs slot: $SROOT ($SROOT_FS)"
|
||||
|
||||
SMNT=$(mktemp -d)
|
||||
mount -t "$SROOT_FS" -o ro "$SROOT" "$SMNT"
|
||||
|
||||
# If btrfs, look for SteamOS's @rootfs subvolume — the top-level
|
||||
# btrfs view may be empty / hold only subvol entries, in which
|
||||
# case we need to remount with subvol=@rootfs (or @, depending
|
||||
# on the image's layout).
|
||||
if [ "$SROOT_FS" = "btrfs" ] && [ ! -d "$SMNT/usr" ]; then
|
||||
echo ">> top-level btrfs has no /usr; scanning subvolumes"
|
||||
btrfs subvolume list "$SMNT" 2>&1 | head -20 || true
|
||||
umount "$SMNT"
|
||||
for sub in @rootfs @ rootfs root; do
|
||||
if mount -t btrfs -o "ro,subvol=$sub" "$SROOT" "$SMNT" 2>/dev/null; then
|
||||
if [ -d "$SMNT/usr" ]; then
|
||||
echo ">> rootfs at subvol=$sub"
|
||||
break
|
||||
fi
|
||||
umount "$SMNT"
|
||||
fi
|
||||
done
|
||||
if [ ! -d "$SMNT/usr" ]; then
|
||||
echo "ERROR: couldn't locate rootfs in btrfs at any known subvol"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "=== SteamOS: copy rootfs ($(du -sh "$SMNT" | cut -f1)) -> \$CHROOT ==="
|
||||
cp -a "$SMNT"/. "$CHROOT/"
|
||||
umount "$SMNT"
|
||||
rmdir "$SMNT"
|
||||
|
||||
# SteamOS splits /var off the rootfs onto its own partition.
|
||||
# First-boot fails without /var/lib/dbus etc.
|
||||
VARMNT=$(mktemp -d)
|
||||
for p in "${PART_PATHS[@]}"; do
|
||||
t=$(blkid -o value -s TYPE "$p" 2>/dev/null || echo "")
|
||||
sz=$(blockdev --getsize64 "$p" 2>/dev/null || echo 0)
|
||||
if [ "$t" = "ext4" ] && [ "$sz" -lt $((2 * 1024 * 1024 * 1024)) ]; then
|
||||
if mount -o ro "$p" "$VARMNT" 2>/dev/null; then
|
||||
if [ -d "$VARMNT/lib" ] || [ -d "$VARMNT/log" ]; then
|
||||
echo "=== SteamOS: copy var ($(du -sh "$VARMNT" | cut -f1)) -> \$CHROOT/var ==="
|
||||
mkdir -p "$CHROOT/var"
|
||||
cp -a "$VARMNT"/. "$CHROOT/var/" || true
|
||||
umount "$VARMNT"
|
||||
break
|
||||
fi
|
||||
umount "$VARMNT" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
rmdir "$VARMNT"
|
||||
|
||||
kpartx -dv "$SLOOP" 2>/dev/null || true
|
||||
losetup -d "$SLOOP"
|
||||
rm -f "$STEAMOS_IMG"
|
||||
|
||||
echo "=== SteamOS: install linux-ps5 kernel pkg ==="
|
||||
PKG=$(ls /kernel-debs/linux-ps5-*.pkg.tar.zst 2>/dev/null | head -1)
|
||||
[ -z "$PKG" ] && { echo "ERROR: no linux-ps5 pkg.tar.zst in /kernel-debs/"; exit 1; }
|
||||
TMP=$(mktemp -d)
|
||||
# Upstream image-builder doesn't ship libarchive-tools (bsdtar). GNU tar
|
||||
# 1.32+ handles .pkg.tar.zst via --zstd; image-builder runs Ubuntu 24.04
|
||||
# with tar 1.35.
|
||||
tar --zstd -xf "$PKG" -C "$TMP"
|
||||
for d in usr boot etc; do
|
||||
[ -d "$TMP/$d" ] && cp -a "$TMP/$d"/. "$CHROOT/$d/" 2>/dev/null || true
|
||||
done
|
||||
rm -rf "$TMP"
|
||||
|
||||
# Redetect KVER from the installed module dir — the entrypoint's
|
||||
# top-level detect_kver pulls a bare semver like '7.0.10' out of the
|
||||
# pkg filename, but the linux-ps5 pkg actually installs modules under
|
||||
# something like '7.0.10-ps5' (with a localversion suffix). Use what's
|
||||
# actually on disk so depmod / mkinitramfs find the modules.
|
||||
KVER=$(ls -1 "$CHROOT/usr/lib/modules" 2>/dev/null \
|
||||
| grep -v "$(ls -1 "$CHROOT/usr/lib/modules" 2>/dev/null \
|
||||
| grep -E 'neptune|^[0-9]+\.[0-9]+\.[0-9]+-[^-]+$' \
|
||||
| grep -v 'ps5' || true)" | head -1)
|
||||
[ -z "$KVER" ] && KVER=$(ls -1 "$CHROOT/usr/lib/modules" 2>/dev/null | grep ps5 | head -1)
|
||||
[ -z "$KVER" ] && KVER=$(ls -1 "$CHROOT/usr/lib/modules" 2>/dev/null | head -1)
|
||||
echo ">> KVER=$KVER"
|
||||
|
||||
# Drop SteamOS's own kernel(s) so grub boots ours
|
||||
rm -f "$CHROOT"/boot/vmlinuz-linux* "$CHROOT"/boot/initrd*-neptune* \
|
||||
"$CHROOT"/boot/initramfs-linux*.img 2>/dev/null || true
|
||||
for d in "$CHROOT"/usr/lib/modules/*; do
|
||||
[ -d "$d" ] || continue
|
||||
[ "$(basename "$d")" = "$KVER" ] && continue
|
||||
rm -rf "$d"
|
||||
done
|
||||
|
||||
if [ -x "$CHROOT/usr/bin/depmod" ]; then
|
||||
chroot "$CHROOT" /usr/bin/depmod -a "$KVER" || true
|
||||
else
|
||||
depmod -b "$CHROOT" "$KVER" || true
|
||||
fi
|
||||
|
||||
[ -f "$CHROOT/boot/vmlinuz-$KVER" ] || \
|
||||
cp "$CHROOT"/boot/vmlinuz-* "$CHROOT/boot/vmlinuz-$KVER" 2>/dev/null || true
|
||||
mkdir -p "$CHROOT/boot/efi"
|
||||
cp "$CHROOT/boot/vmlinuz-$KVER" "$CHROOT/boot/efi/bzImage" 2>/dev/null || true
|
||||
|
||||
# Symlink chroot's modules into host /lib/modules so the build
|
||||
# container's mkinitramfs/depmod can see them. PS5 kernel pkg
|
||||
# installs modules under /usr/lib/modules.
|
||||
echo "=== SteamOS: build initrd via host mkinitramfs ==="
|
||||
mkdir -p /lib/modules
|
||||
ln -sfn "$CHROOT/usr/lib/modules/$KVER" "/lib/modules/$KVER"
|
||||
|
||||
cat > /etc/initramfs-tools/modules <<'INITMODS'
|
||||
# USB host controllers (PS5 boot drive is USB 3 — xhci must be present).
|
||||
xhci_pci
|
||||
xhci_hcd
|
||||
ehci_pci
|
||||
ehci_hcd
|
||||
ohci_pci
|
||||
ohci_hcd
|
||||
# Storage
|
||||
usb-storage
|
||||
uas
|
||||
# Filesystems we may root on
|
||||
ext4
|
||||
btrfs
|
||||
vfat
|
||||
# Block stack
|
||||
sd_mod
|
||||
loop
|
||||
dm_mod
|
||||
# AMD GPU + display for early framebuffer
|
||||
amdgpu
|
||||
INITMODS
|
||||
|
||||
# Force MODULES=most so we include everything for the PS5
|
||||
# regardless of what's loaded in the build container.
|
||||
sed -i 's/^MODULES=.*/MODULES=most/' /etc/initramfs-tools/initramfs.conf
|
||||
|
||||
# amdgpu options for the PS5 Oberon GPU: dpm=0 keeps HDMI alive
|
||||
# (DPM transitions stall the DP→HDMI bridge into a stuck-low
|
||||
# state, displayed as a black screen the moment KWin tries to
|
||||
# take DRM master from gamescope — exact symptom: 'failed to
|
||||
# display topology' in Plasma + black screen with cursor after
|
||||
# Switch-to-Desktop). gpu_recovery=0 avoids the linux-ps5
|
||||
# patched recovery path that hard-faults on Oberon. Bazzite
|
||||
# already ships these for the same reason.
|
||||
#
|
||||
# Land in both initramfs and rootfs so amdgpu picks them up at
|
||||
# both probe times (early initramfs phase + post-switch_root).
|
||||
mkdir -p "$CHROOT/etc/modprobe.d" /etc/modprobe.d
|
||||
cat > "$CHROOT/etc/modprobe.d/ps5-amdgpu.conf" <<'AMDGPU'
|
||||
options amdgpu dpm=0 gpu_recovery=0
|
||||
AMDGPU
|
||||
cp "$CHROOT/etc/modprobe.d/ps5-amdgpu.conf" /etc/modprobe.d/ps5-amdgpu.conf
|
||||
|
||||
mkinitramfs -k "$KVER" -o "$CHROOT/boot/efi/initrd.img"
|
||||
# Also drop a copy at the standard location inside the rootfs.
|
||||
cp "$CHROOT/boot/efi/initrd.img" "$CHROOT/boot/initramfs-$KVER.img"
|
||||
|
||||
rm -f "/lib/modules/$KVER"
|
||||
|
||||
# SteamOS recovery image ships without a default.target symlink and
|
||||
# relies on the cmdline to set systemd.unit=. Without either,
|
||||
# systemd hangs at "solid cursor on tty1" forever. Fix both so
|
||||
# either path reaches graphical.target.
|
||||
ln -sf /usr/lib/systemd/system/graphical.target "$CHROOT/etc/systemd/system/default.target"
|
||||
|
||||
# sshd: recovery image ships it disabled. We want it on by
|
||||
# default so we can ssh in from the LAN without poking the
|
||||
# console. Enable + set deck's password to 'deck' (matches
|
||||
# the build-time convention used for the other distros).
|
||||
mkdir -p "$CHROOT/etc/systemd/system/multi-user.target.wants"
|
||||
ln -sf /usr/lib/systemd/system/sshd.service \
|
||||
"$CHROOT/etc/systemd/system/multi-user.target.wants/sshd.service"
|
||||
chroot "$CHROOT" /usr/bin/bash -c 'echo "deck:deck" | chpasswd' 2>/dev/null || \
|
||||
chroot "$CHROOT" /bin/sh -c 'echo "deck:deck" | chpasswd' 2>/dev/null || true
|
||||
# also unlock the account in case it's locked
|
||||
chroot "$CHROOT" /usr/bin/passwd -u deck 2>/dev/null || true
|
||||
|
||||
# Enable sddm. The recovery image ships sddm.service in
|
||||
# /usr/lib/systemd/system but NEVER symlinks it into
|
||||
# graphical.target.wants — relies on a first-boot Valve setup
|
||||
# script that we've cut out. Without this, graphical.target
|
||||
# is reached but no display manager starts → black screen
|
||||
# even with working GPU init.
|
||||
mkdir -p "$CHROOT/etc/systemd/system/graphical.target.wants"
|
||||
ln -sf /usr/lib/systemd/system/sddm.service \
|
||||
"$CHROOT/etc/systemd/system/graphical.target.wants/sddm.service"
|
||||
|
||||
# Switch-to-Desktop / Return-to-Gaming buttons: gamescope/Steam
|
||||
# and steamos-manager may invoke a session named literally
|
||||
# "desktop" or "gamescope" (bare aliases) — those land both in
|
||||
# SDDM's session lookup AND in our steamos-session-select shim
|
||||
# below. Add symlinks so the SDDM lookup path resolves, and
|
||||
# match the same naming below in the shim's case statement.
|
||||
ln -sfn plasma.desktop "$CHROOT/usr/share/wayland-sessions/desktop.desktop"
|
||||
ln -sfn plasmax11.desktop "$CHROOT/usr/share/xsessions/desktop.desktop"
|
||||
ln -sfn gamescope-wayland.desktop "$CHROOT/usr/share/wayland-sessions/gamescope.desktop"
|
||||
|
||||
# AMD GPU firmware comes from the SteamOS rootfs as .bin.zst
|
||||
# (zstd-compressed). Our linux-ps5 7.0.10 kernel is built
|
||||
# WITHOUT CONFIG_FW_LOADER_COMPRESS_ZSTD=y so the kernel's
|
||||
# firmware loader looks for the bare .bin only, fails -2,
|
||||
# amdgpu logs "Fatal error during GPU init", no display.
|
||||
# Decompress every firmware blob in place; keep the .zst
|
||||
# alongside (cheap, harmless) so the kernel finds the .bin
|
||||
# without us having to enumerate which specific blobs amdgpu
|
||||
# asks for on this revision (currently pfp + sdma; could
|
||||
# grow). Side benefit: covers wifi/bluetooth/etc firmware
|
||||
# for free.
|
||||
echo "=== SteamOS: decompress *.bin.zst firmware (kernel can't load compressed) ==="
|
||||
fw_count=0
|
||||
while IFS= read -r -d '' zst; do
|
||||
out="${zst%.zst}"
|
||||
if [ ! -e "$out" ]; then
|
||||
zstd -dq -o "$out" "$zst" && fw_count=$((fw_count+1))
|
||||
fi
|
||||
done < <(find "$CHROOT/lib/firmware" "$CHROOT/usr/lib/firmware" \
|
||||
-name '*.bin.zst' -print0 2>/dev/null)
|
||||
echo ">> decompressed $fw_count firmware files"
|
||||
|
||||
# mkinitramfs runs BEFORE this point in the script and bakes
|
||||
# the amdgpu module into the initramfs (per our modules list).
|
||||
# But Debian's mkinitramfs does NOT copy /lib/firmware/* into
|
||||
# the initramfs by default — so when amdgpu probes in early
|
||||
# userspace (initramfs phase, before switch_root), it requests
|
||||
# firmware from initramfs's /lib/firmware/ which doesn't have
|
||||
# it → ENOENT → fatal GPU init → no display, identical
|
||||
# symptom to the .zst-only-firmware bug above. Inject the
|
||||
# decompressed amdgpu blobs INTO the initrd image now (it's
|
||||
# a zstd cpio newc archive — extract, add, repack).
|
||||
INITRD="$CHROOT/boot/efi/initrd.img"
|
||||
if [ -f "$INITRD" ] && [ -d "$CHROOT/lib/firmware/amdgpu" ]; then
|
||||
echo "=== SteamOS: inject amdgpu firmware into initramfs ==="
|
||||
IWORK=$(mktemp -d)
|
||||
( cd "$IWORK" && zstd -dc "$INITRD" | cpio -idmu --quiet --no-absolute-filenames )
|
||||
mkdir -p "$IWORK/usr/lib/firmware/amdgpu"
|
||||
cp "$CHROOT/lib/firmware/amdgpu"/*.bin "$IWORK/usr/lib/firmware/amdgpu/" 2>/dev/null || true
|
||||
# Some kernels probe /lib/firmware first, some /usr/lib/firmware.
|
||||
# Make both reachable.
|
||||
ln -sfn usr/lib/firmware "$IWORK/lib/firmware" 2>/dev/null || true
|
||||
( cd "$IWORK" && find . | cpio -o -H newc --quiet | zstd -19 --quiet ) > "$INITRD.new"
|
||||
mv "$INITRD.new" "$INITRD"
|
||||
cp "$INITRD" "$CHROOT/boot/initramfs-$KVER.img"
|
||||
rm -rf "$IWORK"
|
||||
echo ">> initramfs repacked with firmware ($(du -h "$INITRD" | cut -f1))"
|
||||
fi
|
||||
|
||||
# Persistent journal so post-boot failures are debuggable from
|
||||
# outside (read it by mounting the rootfs). Needs the
|
||||
# systemd-journal group ownership + setgid bit or journald
|
||||
# refuses to use it and silently stays volatile.
|
||||
mkdir -p "$CHROOT/var/log/journal"
|
||||
chown 0:980 "$CHROOT/var/log/journal" 2>/dev/null || \
|
||||
chroot "$CHROOT" chown 0:systemd-journal /var/log/journal 2>/dev/null || true
|
||||
chmod 2755 "$CHROOT/var/log/journal"
|
||||
|
||||
# SteamOS fstab references its A/B atomic partset paths
|
||||
# (/dev/disk/by-partsets/{self,shared}/...) which don't exist
|
||||
# on a flat-rootfs flash — every mount unit fails and stalls
|
||||
# local-fs.target dependencies (NetworkManager etc.).
|
||||
sed -i '/by-partsets/s|^|#FLAT-ROOTFS-DISABLED# |' "$CHROOT/etc/fstab"
|
||||
|
||||
# …but we still need /efi mounted at boot. ps5-stage-firmware
|
||||
# reads the NXP IW620 WLAN blob from /boot/efi/lib/nxp/, and
|
||||
# cmdline.txt / kexec.sh / bzImage live there too. Append a
|
||||
# label-based mount line so the FAT boot partition is up
|
||||
# before sysinit.target. /boot/efi is a symlink to /efi in
|
||||
# the recovery image; mount the canonical target.
|
||||
cat >> "$CHROOT/etc/fstab" <<FSTAB
|
||||
LABEL=$EFI_LABEL /efi vfat defaults,nofail,umask=0077 0 2
|
||||
FSTAB
|
||||
|
||||
# steamos-customizations also ships explicit .mount units for
|
||||
# esp/efi/home that pull in partset-aware fsck + blockdev
|
||||
# targets. Mask esp.mount + home.mount (we don't have those
|
||||
# partitions), but leave efi.mount alone — it'd race the
|
||||
# fstab line above. systemd just generates a fresh efi.mount
|
||||
# from our fstab entry.
|
||||
for u in esp.mount home.mount; do
|
||||
ln -sf /dev/null "$CHROOT/etc/systemd/system/$u"
|
||||
done
|
||||
|
||||
# The recovery image ships /etc/sddm.conf.d/zz-steamos-autologin.conf
|
||||
# with Session=plasma — it's normally written when a Deck user
|
||||
# picks "Switch to Desktop" from Steam, but the recovery image
|
||||
# bakes it in pre-set to KDE. With it present, SDDM autologs
|
||||
# into Plasma instead of the gamescope-wayland session.
|
||||
#
|
||||
# Just `rm` isn't enough: on first boot ps5-gamescope-recovery
|
||||
# (or steam-jupiter falling back to KDE because gamescope can't
|
||||
# grab a display) writes this file back with Session=plasma,
|
||||
# and we land in Plasma on every subsequent boot too.
|
||||
# Overwrite with gamescope-wayland.desktop so even when the
|
||||
# file gets recreated by Steam/recovery, we still autologin
|
||||
# into Big Picture.
|
||||
mkdir -p "$CHROOT/etc/sddm.conf.d"
|
||||
cat > "$CHROOT/etc/sddm.conf.d/zz-steamos-autologin.conf" <<'EOF'
|
||||
[Autologin]
|
||||
Session=gamescope-wayland.desktop
|
||||
EOF
|
||||
|
||||
# Replace /usr/bin/steamos-session-select with a dumb shim that
|
||||
# writes the sddm autologin file directly + restarts sddm. The
|
||||
# vendored version delegates to steamosctl/steamos-manager whose
|
||||
# Steam Deck hardware code paths silently no-op on PS5 — the
|
||||
# symptom is "Return to Gaming Mode just reloads Desktop" because
|
||||
# the autologin Session= never actually changes. This shim bypasses
|
||||
# the whole DBus dance.
|
||||
if [ -f "$CHROOT/usr/bin/steamos-session-select" ]; then
|
||||
mv "$CHROOT/usr/bin/steamos-session-select" \
|
||||
"$CHROOT/usr/bin/steamos-session-select.orig"
|
||||
cat > "$CHROOT/usr/bin/steamos-session-select" <<'SHIM'
|
||||
#!/bin/bash
|
||||
# PS5-flat-rootfs shim: write a sddm autologin override directly +
|
||||
# restart sddm. Bypasses the vendored script that delegates to
|
||||
# steamosctl/steamos-manager (which silently no-ops on PS5).
|
||||
#
|
||||
# Uses sudo (deck has passwordless sudo on this build) rather than
|
||||
# the recovery image's pkexec → steamos-priv-write path: that helper
|
||||
# only whitelists /sys/class/backlight and similar paths, NOT
|
||||
# /etc/sddm.conf.d, so any pkexec write attempt fails with an
|
||||
# unbound-variable error from the priv-write script. Sudo is simpler
|
||||
# and works from any context (Konsole, desktop shortcut, even SSH).
|
||||
#
|
||||
# Conf filename 'zzz-session-override.conf':
|
||||
# * sorts AFTER zz-steamos-autologin.conf so this file wins precedence
|
||||
# * does NOT match sddm's ExecStartPre 'rm /etc/sddm.conf.d/zzt-...'
|
||||
# drop-in pattern, so the override survives systemctl restart sddm
|
||||
set -e
|
||||
session="${1:-gamescope}"
|
||||
case "$session" in
|
||||
plasma-wayland|plasma-wayland-persistent) target="plasma.desktop" ;;
|
||||
plasma|plasma-x11|plasma-x11-persistent) target="plasmax11.desktop" ;;
|
||||
desktop) target="plasma.desktop" ;;
|
||||
gamescope) target="gamescope-wayland.desktop" ;;
|
||||
*) target="gamescope-wayland.desktop" ;;
|
||||
esac
|
||||
sudo tee /etc/sddm.conf.d/zzz-session-override.conf >/dev/null <<EOF
|
||||
[Autologin]
|
||||
User=deck
|
||||
Session=$target
|
||||
Relogin=true
|
||||
EOF
|
||||
sudo systemctl restart sddm
|
||||
SHIM
|
||||
chmod 755 "$CHROOT/usr/bin/steamos-session-select"
|
||||
fi
|
||||
|
||||
# Steam Big Picture's Power-Menu "Switch to Desktop" button
|
||||
# fires a dbus call:
|
||||
# dbus-send --system --dest=org.freedesktop.DisplayManager \
|
||||
# /org/freedesktop/DisplayManager/Seat0 \
|
||||
# org.freedesktop.DisplayManager.Seat.SwitchToUser \
|
||||
# string:doorstop string:<session>
|
||||
# On the real Steam Deck this lands in Valve's patched SDDM
|
||||
# which writes /etc/sddm.conf.d/zzt-steamos-temp-login.conf.
|
||||
# Stock Arch SDDM (what the recovery image ships) accepts the
|
||||
# call but does nothing useful with it — the button is a no-op,
|
||||
# and re-adding -steamos3 to the Steam launcher to route the
|
||||
# button through our shim triggers Steam's A/B atomic-update
|
||||
# reboot loop.
|
||||
#
|
||||
# Workaround: a tiny daemon that BecomeMonitor()s the system
|
||||
# bus, catches the SwitchToUser call, and runs the same
|
||||
# zzz-session-override.conf write + sddm restart that our shim
|
||||
# does. Steam's button now works without the -steamos3 flag.
|
||||
mkdir -p "$CHROOT/usr/local/sbin" "$CHROOT/etc/systemd/system"
|
||||
install -m 755 /repo/distros/steamos/steam-session-switch-listener.py \
|
||||
"$CHROOT/usr/local/sbin/steam-session-switch-listener" 2>/dev/null \
|
||||
|| cp /repo/distros/steamos/steam-session-switch-listener.py \
|
||||
"$CHROOT/usr/local/sbin/steam-session-switch-listener"
|
||||
chmod +x "$CHROOT/usr/local/sbin/steam-session-switch-listener"
|
||||
cp /repo/distros/steamos/steam-session-switch-listener.service \
|
||||
"$CHROOT/etc/systemd/system/steam-session-switch-listener.service"
|
||||
mkdir -p "$CHROOT/etc/systemd/system/multi-user.target.wants"
|
||||
ln -sf ../steam-session-switch-listener.service \
|
||||
"$CHROOT/etc/systemd/system/multi-user.target.wants/steam-session-switch-listener.service"
|
||||
|
||||
# Replace the recovery image's broken Return.desktop. The vendor
|
||||
# shortcut delegates to steamosctl, which silently no-ops on
|
||||
# PS5. Ours calls steamos-session-select gamescope directly
|
||||
# (our shim above does the real work). Drop it on deck's
|
||||
# Desktop AND in /usr/share/applications so it's discoverable
|
||||
# from the app menu too.
|
||||
mkdir -p "$CHROOT/home/deck/Desktop" "$CHROOT/usr/share/applications"
|
||||
cp /repo/distros/steamos/return-to-gaming-mode.desktop \
|
||||
"$CHROOT/home/deck/Desktop/Return.desktop"
|
||||
cp /repo/distros/steamos/return-to-gaming-mode.desktop \
|
||||
"$CHROOT/usr/share/applications/return-to-gaming-mode.desktop"
|
||||
chmod 755 "$CHROOT/home/deck/Desktop/Return.desktop"
|
||||
|
||||
# The recovery image ships /home/deck owned by root:root (the
|
||||
# vendor install flow chowns it later during first-boot OOBE,
|
||||
# which we've masked out). Without this fix, KDE Plasma fails
|
||||
# to create /home/deck/.config on first login — error popup:
|
||||
# "Configuration file /home/deck/.config/kcminitrc not writable.
|
||||
# Please contact your system administrator."
|
||||
# Chown the whole tree to deck:deck (uid/gid 1000) so Plasma
|
||||
# init can write its caches/configs.
|
||||
chroot "$CHROOT" chown -R deck:deck /home/deck 2>/dev/null || \
|
||||
chown -R 1000:1000 "$CHROOT/home/deck"
|
||||
|
||||
# The recovery image's pacman trust DB is populated for
|
||||
# archlinux only — not the holo (SteamOS Valve CI) keys —
|
||||
# so `pacman -Syu` on first boot fails every package with
|
||||
# "unknown trust" from the GitLab CI Package Builder key.
|
||||
# The holo-keyring package IS installed; just needs --populate.
|
||||
chroot "$CHROOT" pacman-key --init 2>/dev/null || true
|
||||
chroot "$CHROOT" pacman-key --populate holo 2>/dev/null || true
|
||||
|
||||
# First-boot rootfs grow. Reuses the arch distro's grow-rootfs +
|
||||
# service (same Arch-userland recipe — growpart + resize2fs at
|
||||
# boot). Our build pins a fixed-size rootfs (~14 GB); the user
|
||||
# flashes onto multi-hundred-GB USB drives. Without this, sda1
|
||||
# stays 11 GB and Steam/flatpak run out of space within an hour.
|
||||
install -m 755 /repo/distros/arch/grow-rootfs \
|
||||
"$CHROOT/usr/local/sbin/grow-rootfs"
|
||||
install -m 644 /repo/distros/arch/grow-rootfs.service \
|
||||
"$CHROOT/etc/systemd/system/grow-rootfs.service"
|
||||
mkdir -p "$CHROOT/etc/systemd/system/local-fs.target.wants"
|
||||
ln -sf ../grow-rootfs.service \
|
||||
"$CHROOT/etc/systemd/system/local-fs.target.wants/grow-rootfs.service"
|
||||
|
||||
# Disable the atomic OS updater. The Steam UI calls
|
||||
# /usr/bin/steamos-update on session start, which tries to
|
||||
# apply Valve's A/B atomic OTA via partsets we don't have +
|
||||
# neptune kernel != our linux-ps5. Result: "Unable to
|
||||
# download the required update" nag every session. Replace
|
||||
# with a noop that returns "no update available" (exit 7)
|
||||
# while honoring the --supports-duplicate-detection probe.
|
||||
if [ -f "$CHROOT/usr/bin/steamos-update" ]; then
|
||||
mv "$CHROOT/usr/bin/steamos-update" "$CHROOT/usr/bin/steamos-update.orig"
|
||||
cat > "$CHROOT/usr/bin/steamos-update" <<'SHIM'
|
||||
#!/bin/bash
|
||||
# Disabled — flat-rootfs PS5 build can't apply SteamOS A/B atomic OTAs.
|
||||
if [ "$1" = "--supports-duplicate-detection" ]; then
|
||||
echo "supports duplicate detection" >&2
|
||||
exit 0
|
||||
fi
|
||||
echo "No update available (disabled — flat-rootfs PS5 build)" >&2
|
||||
exit 7
|
||||
SHIM
|
||||
chmod 755 "$CHROOT/usr/bin/steamos-update"
|
||||
fi
|
||||
|
||||
# Even with steamos-update shimmed, the Steam Big Picture UI
|
||||
# downloads the OS update tarball directly from
|
||||
# update.steamos.cloud and calls steamos-reboot --reboot-other
|
||||
# to apply it. --reboot-other is Valve's "switch active A/B
|
||||
# partset slot then reboot" — since we have no partsets it
|
||||
# just reboots into the same image, Steam sees the update
|
||||
# still pending, downloads again, reboots. Infinite loop.
|
||||
# Shim steamos-reboot so --reboot-other is a silent noop;
|
||||
# plain reboot still works.
|
||||
# Strip -steamos3 + -steampal from the gamescope-session launcher.
|
||||
# These two flags are what makes the Steam client believe it's
|
||||
# running on a real Deck with the A/B atomic update model, so
|
||||
# after every bootstrap download Steam calls logind.Reboot to
|
||||
# "apply" the update — on a flat-rootfs build the reboot does
|
||||
# nothing and we just come back to the same image, loop forever.
|
||||
# Keep -steamdeck (Big Picture UI fidelity) and -gamepadui
|
||||
# (gamepad-driven UI). Also tack on -noverifyfiles so the
|
||||
# 493 MB client redownload doesn't repeat every session.
|
||||
sed -i 's|^steamargs=("-steamos3" "-steampal" "-steamdeck" "-gamepadui")|steamargs=("-steamdeck" "-gamepadui" "-noverifyfiles")|' \
|
||||
"$CHROOT/usr/lib/steamos/steam-launcher" 2>/dev/null || true
|
||||
|
||||
# /usr/bin/steam → /usr/bin/steam-jupiter (symlink). The
|
||||
# steam-jupiter wrapper is Valve's OOBE script that does
|
||||
# `rm -rf ~/.steam ~/.local/share/Steam` at the top of every
|
||||
# launch — because the recovery image is meant to live as a
|
||||
# single-shot first-boot installer and the A/B atomic system
|
||||
# handles persistence. On our flat-rootfs build that wipe is
|
||||
# the cause of the "Steam downloads ~493 MB every session"
|
||||
# loop. Replace with one that just execs the real Steam
|
||||
# binary with the deck flags.
|
||||
if [ -f "$CHROOT/usr/bin/steam-jupiter" ]; then
|
||||
mv "$CHROOT/usr/bin/steam-jupiter" "$CHROOT/usr/bin/steam-jupiter.orig"
|
||||
cat > "$CHROOT/usr/bin/steam-jupiter" <<'SHIM'
|
||||
#!/bin/bash
|
||||
# Patched for flat-rootfs PS5 build:
|
||||
# - skip OOBE rm -rf so Steam state persists across the reboot Steam
|
||||
# would otherwise fire post-bootstrap
|
||||
# - drop -steamdeck so this desktop entry doesn't trigger the Deck-
|
||||
# specific reboot-to-apply update path either. Gaming Mode entry
|
||||
# path keeps its UI flags via /usr/lib/steamos/steam-launcher.
|
||||
set -euo pipefail
|
||||
exec /usr/lib/steam/steam -skipinitialbootstrap "$@"
|
||||
SHIM
|
||||
chmod 755 "$CHROOT/usr/bin/steam-jupiter"
|
||||
fi
|
||||
|
||||
# Steam binary calls logind.Reboot via DBus directly after the
|
||||
# bootstrap finishes downloading (its "apply the update by
|
||||
# rebooting" flow that assumes Deck-style A/B atomic). The
|
||||
# reboot happens BEFORE the bootstrap can mark itself
|
||||
# complete, so next boot Steam still thinks installed=0 and
|
||||
# re-downloads — loop. Block reboot/power-off from the deck
|
||||
# user via polkit so Steam can't trigger it. Manual reboots
|
||||
# via SSH/Konsole as root still work.
|
||||
mkdir -p "$CHROOT/etc/polkit-1/rules.d"
|
||||
cat > "$CHROOT/etc/polkit-1/rules.d/05-disable-deck-reboot.rules" <<'PK'
|
||||
// Block Steam (running as deck) from issuing reboot/poweroff via DBus.
|
||||
// On a real Steam Deck the A/B atomic update model wants Steam to be
|
||||
// able to "reboot to apply" — here it traps us in a download loop
|
||||
// because nothing actually applies. Manual reboots still work as root
|
||||
// (sudo reboot via SSH or Konsole in Desktop Mode).
|
||||
polkit.addRule(function(action, subject) {
|
||||
if (subject.user === "deck" && (
|
||||
action.id === "org.freedesktop.login1.reboot" ||
|
||||
action.id === "org.freedesktop.login1.reboot-multiple-sessions" ||
|
||||
action.id === "org.freedesktop.login1.power-off" ||
|
||||
action.id === "org.freedesktop.login1.power-off-multiple-sessions" ||
|
||||
action.id === "org.freedesktop.login1.set-reboot-to-firmware-setup"
|
||||
)) {
|
||||
return polkit.Result.NO;
|
||||
}
|
||||
});
|
||||
PK
|
||||
chmod 644 "$CHROOT/etc/polkit-1/rules.d/05-disable-deck-reboot.rules"
|
||||
|
||||
if [ -f "$CHROOT/usr/bin/steamos-reboot" ]; then
|
||||
mv "$CHROOT/usr/bin/steamos-reboot" "$CHROOT/usr/bin/steamos-reboot.orig"
|
||||
cat > "$CHROOT/usr/bin/steamos-reboot" <<'SHIM'
|
||||
#!/bin/bash
|
||||
# Shimmed for flat-rootfs PS5 build — no A/B partsets, so --reboot-other
|
||||
# (Steam OS-update apply path) restarts SDDM instead of doing the A/B
|
||||
# slot switch + reboot. Gamescope-session relaunches cleanly into Steam
|
||||
# login. Plain reboot still works.
|
||||
case "${1:-}" in
|
||||
--reboot-other)
|
||||
# Clear pending atomupd state so Steam does not immediately
|
||||
# re-prompt for the same "update" on the next session.
|
||||
rm -rf /var/lib/steamos-atomupd/.cache \
|
||||
/home/.steamos/offload/var/lib/steamos-atomupd/.cache \
|
||||
2>/dev/null || true
|
||||
echo "steamos-reboot: --reboot-other -> sddm restart (no A/B partsets)" >&2
|
||||
systemctl restart sddm
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
exec /sbin/reboot "$@"
|
||||
;;
|
||||
esac
|
||||
SHIM
|
||||
chmod 755 "$CHROOT/usr/bin/steamos-reboot"
|
||||
fi
|
||||
|
||||
# FINAL OVERRIDE — the earlier zz-steamos-autologin.conf write
|
||||
# isn't sticking on the flashed image (file shows the recovery image's
|
||||
# stock mtime / Session=plasma after boot). Something between that
|
||||
# write and the partition copy in the standard image-builder flow is
|
||||
# overlaying it back. Last-write-wins: redo it here at the end of the
|
||||
# script, and ALSO drop a sentinel zzz-session-override.conf that
|
||||
# sorts even later — same one our shim writes — so SDDM merge order
|
||||
# absolutely lands on gamescope-wayland for first boot.
|
||||
mkdir -p "$CHROOT/etc/sddm.conf.d"
|
||||
cat > "$CHROOT/etc/sddm.conf.d/zz-steamos-autologin.conf" <<'AUTOLOGIN_END'
|
||||
[Autologin]
|
||||
Session=gamescope-wayland.desktop
|
||||
AUTOLOGIN_END
|
||||
cat > "$CHROOT/etc/sddm.conf.d/zzz-session-override.conf" <<'OVERRIDE_END'
|
||||
[Autologin]
|
||||
User=deck
|
||||
Session=gamescope-wayland.desktop
|
||||
Relogin=true
|
||||
OVERRIDE_END
|
||||
echo "=== verify autologin file content ==="
|
||||
ls -la "$CHROOT/etc/sddm.conf.d/"
|
||||
grep -H Session= "$CHROOT/etc/sddm.conf.d/"*.conf || true
|
||||
|
||||
echo "=== SteamOS: rootfs prepared at \$CHROOT ($(du -sh "$CHROOT" | cut -f1)), initrd $(du -h "$CHROOT/boot/efi/initrd.img" | cut -f1) ==="
|
||||
8
distros/steamos/return-to-gaming-mode.desktop
Normal file
8
distros/steamos/return-to-gaming-mode.desktop
Normal file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Return to Gaming Mode
|
||||
Comment=Switch the autologin session back to gamescope / Steam Deck UI
|
||||
Exec=bash -c 'steamos-session-select gamescope'
|
||||
Icon=steamdeck-gaming-return
|
||||
Terminal=false
|
||||
Categories=System;
|
||||
84
distros/steamos/steam-session-switch-listener.py
Normal file
84
distros/steamos/steam-session-switch-listener.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Listens on the system bus for Steam Big Picture's Switch-to-Desktop
|
||||
dbus call (org.freedesktop.DisplayManager.Seat.SwitchToUser) and performs
|
||||
the session swap directly — runs as root so no pkexec / polkit needed.
|
||||
|
||||
Flow:
|
||||
1. Steam UI's Power-Menu Switch-to-Desktop fires
|
||||
dbus-send --system --dest=org.freedesktop.DisplayManager \\
|
||||
/org/freedesktop/DisplayManager/Seat0 \\
|
||||
org.freedesktop.DisplayManager.Seat.SwitchToUser \\
|
||||
string:doorstop string:plasma
|
||||
2. This listener catches the call (BecomeMonitor mode on system bus)
|
||||
3. Writes /etc/sddm.conf.d/zzt-steamos-temp-login.conf for plasma
|
||||
4. Runs 'steam -shutdown' as the deck user — gamescope-session ends
|
||||
cleanly because Steam (gamescope's child) exits, DRM master is
|
||||
released the gentle way, sddm bounces and autologs into plasma.
|
||||
5. The zzt- conf is one-shot — sddm's ExecStartPre wipes it on the
|
||||
next sddm restart, so closing plasma drops back to gamescope.
|
||||
"""
|
||||
import dbus, dbus.mainloop.glib, subprocess, sys, traceback
|
||||
from gi.repository import GLib
|
||||
|
||||
LOG = "[steam-session-switch-listener]"
|
||||
# zzz- sorts AFTER zz-steamos-autologin.conf (so it overrides), AND it
|
||||
# doesn't match the ExecStartPre rm pattern that only deletes 'zzt-...'
|
||||
# — so the conf survives `systemctl restart sddm` cycles.
|
||||
ZZT = "/etc/sddm.conf.d/zzz-session-override.conf"
|
||||
|
||||
SESSION_MAP = {
|
||||
"plasma": "plasma.desktop",
|
||||
"plasma-wayland": "plasma.desktop",
|
||||
"plasma-wayland-persistent": "plasma.desktop",
|
||||
"desktop": "plasma.desktop",
|
||||
"plasma-x11": "plasmax11.desktop",
|
||||
"plasma-x11-persistent": "plasmax11.desktop",
|
||||
"gamescope": "gamescope-wayland.desktop",
|
||||
}
|
||||
|
||||
def handle_switch(session):
|
||||
target = SESSION_MAP.get(session, "gamescope-wayland.desktop")
|
||||
print(f"{LOG} writing zzt- conf -> {target}", flush=True)
|
||||
with open(ZZT, "w") as f:
|
||||
f.write(f"[Autologin]\nUser=deck\nSession={target}\nRelogin=true\n")
|
||||
|
||||
# Restart sddm directly. Since we used zzz-session-override.conf,
|
||||
# the ExecStartPre that only kills zzt-... doesn't wipe it. sddm
|
||||
# restart reads the conf (zzz- sorts after zz-steamos-autologin.conf,
|
||||
# so it wins) and autologs into the requested session.
|
||||
print(f"{LOG} systemctl restart sddm", flush=True)
|
||||
subprocess.Popen(["systemctl", "restart", "sddm"], stdin=subprocess.DEVNULL)
|
||||
|
||||
def on_message(_bus, message):
|
||||
try:
|
||||
if message.get_interface() != "org.freedesktop.DisplayManager.Seat":
|
||||
return
|
||||
if message.get_member() != "SwitchToUser":
|
||||
return
|
||||
args = list(message.get_args_list())
|
||||
if len(args) < 2:
|
||||
return
|
||||
user, session = str(args[0]), str(args[1])
|
||||
print(f"{LOG} caught SwitchToUser({user!r}, {session!r})", flush=True)
|
||||
handle_switch(session)
|
||||
except Exception:
|
||||
# NEVER let an exception kill the monitor — log and continue
|
||||
traceback.print_exc()
|
||||
|
||||
def main():
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
bus = dbus.SystemBus()
|
||||
bus_obj = bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus")
|
||||
monitoring = dbus.Interface(bus_obj, "org.freedesktop.DBus.Monitoring")
|
||||
monitoring.BecomeMonitor(
|
||||
["type='method_call',"
|
||||
"interface='org.freedesktop.DisplayManager.Seat',"
|
||||
"member='SwitchToUser'"],
|
||||
dbus.UInt32(0),
|
||||
)
|
||||
bus.add_message_filter(on_message)
|
||||
print(f"{LOG} listening on system bus for SwitchToUser", flush=True)
|
||||
GLib.MainLoop().run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
14
distros/steamos/steam-session-switch-listener.service
Normal file
14
distros/steamos/steam-session-switch-listener.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=Translate Steam Big Picture Switch-to-Desktop dbus call into steamos-session-select
|
||||
Documentation=https://gist.github.com/Rishikant181/e26fb23d4c57db74bddaa0a57b26cd26
|
||||
After=dbus.service sddm.service
|
||||
PartOf=sddm.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/sbin/steam-session-switch-listener
|
||||
Restart=on-failure
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -36,6 +36,9 @@ packages:
|
||||
- openssh-server
|
||||
- build-essential
|
||||
- network-manager
|
||||
- bluetooth
|
||||
- bluez
|
||||
- bluez-tools
|
||||
- linux-firmware
|
||||
- initramfs-tools
|
||||
- kmod
|
||||
@@ -132,6 +135,7 @@ actions:
|
||||
set -eux
|
||||
systemctl enable grow-rootfs.service
|
||||
systemctl enable systemd-resolved
|
||||
systemctl enable bluetooth.service
|
||||
mkdir -p /boot/efi
|
||||
# Disable kdump postinst hook — it fails in chroot (can't resolve root device)
|
||||
rm -f /etc/kernel/postinst.d/kdump-tools
|
||||
|
||||
@@ -9,6 +9,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
parted dosfstools e2fsprogs kmod \
|
||||
initramfs-tools fdisk gdisk udev kpartx \
|
||||
xz-utils bzip2 zstd \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Go (Ubuntu 24.04 only has 1.22, distrobuilder needs newer)
|
||||
@@ -26,6 +27,19 @@ RUN mkdir -p /root/go/src/github.com/lxc/ && \
|
||||
cp /root/go/bin/distrobuilder /usr/local/bin/ && \
|
||||
rm -rf /root/go/src /root/go/pkg
|
||||
|
||||
# Fedora 41+ base images ship as OCI archives; distrobuilder's fedora-http
|
||||
# downloader unpacks them with umoci/skopeo. Ubuntu's apt umoci (0.4.7) is
|
||||
# built with an old Go that crashes under qemu on the self-hosted arm64 runner
|
||||
# ("fatal error: lfstack.push invalid packing"), because the emulated amd64
|
||||
# process inherits the host's 52-bit-VA high addresses. Build umoci from source
|
||||
# with the Go toolchain installed above (>=1.21 packs high addresses correctly).
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
skopeo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN go install github.com/opencontainers/umoci/cmd/umoci@v0.5.0 && \
|
||||
cp /root/go/bin/umoci /usr/local/bin/umoci && \
|
||||
rm -rf /root/go/pkg
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY docker/image-builder/entrypoint.sh /entrypoint.sh
|
||||
|
||||
@@ -10,8 +10,34 @@ EFI_LABEL="boot"
|
||||
CHROOT="/build/chroot"
|
||||
IMG="/output/ps5-${DISTRO}.img"
|
||||
|
||||
# Detect kernel version from the staged kernel package (used by bazzite +
|
||||
# batocera rootfs builders which install the linux-ps5 kernel manually).
|
||||
detect_kver() {
|
||||
for f in /kernel-debs/linux-ps5-*.rpm /kernel-debs/linux-ps5_*.deb \
|
||||
/kernel-debs/linux-ps5-*.pkg.tar.zst; do
|
||||
[ -f "$f" ] || continue
|
||||
echo "$(basename "$f")" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
|
||||
return 0
|
||||
done
|
||||
}
|
||||
KVER="$(detect_kver)"
|
||||
[ -n "$KVER" ] || { echo "WARN: could not detect KVER from /kernel-debs/"; }
|
||||
export KVER
|
||||
|
||||
if [ "$SKIP_CHROOT" = "true" ] && [ -d "$CHROOT/bin" ]; then
|
||||
echo "=== Reusing cached $DISTRO rootfs ==="
|
||||
elif [ -x "/repo/distros/${DISTRO}/build-rootfs.sh" ]; then
|
||||
# Distros with their own build-rootfs.sh (bazzite, bazzite-deck, batocera, ...)
|
||||
# are responsible for populating $CHROOT themselves — distrobuilder is
|
||||
# bypassed entirely. Most often used for OCI atomic images (bazzite via
|
||||
# skopeo+umoci) or pre-built rootfs images (batocera squashfs).
|
||||
echo "=== Building $DISTRO rootfs via distros/${DISTRO}/build-rootfs.sh ==="
|
||||
[ -n "$CHROOT" ] || { echo "FATAL: \$CHROOT empty"; exit 1; }
|
||||
mkdir -p "$CHROOT"
|
||||
rm -rf "$CHROOT"/* "$CHROOT"/.[!.]* 2>/dev/null || true
|
||||
DISTRO="$DISTRO" CHROOT="$CHROOT" KVER="$KVER" \
|
||||
ROOT_LABEL="$ROOT_LABEL" EFI_LABEL="$EFI_LABEL" \
|
||||
bash "/repo/distros/${DISTRO}/build-rootfs.sh"
|
||||
else
|
||||
echo "=== Building $DISTRO rootfs ==="
|
||||
# --- Stage files for distrobuilder's copy generators ---
|
||||
@@ -34,6 +60,23 @@ EOF
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
|
||||
cp /kernel-debs/*.deb "$STAGING/debs/"
|
||||
;;
|
||||
proxmox)
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
|
||||
cp /repo/distros/${DISTRO}/proxmox-release-bookworm.gpg "$STAGING/"
|
||||
cp /kernel-debs/*.deb "$STAGING/debs/"
|
||||
;;
|
||||
debian)
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
|
||||
cp /kernel-debs/*.deb "$STAGING/debs/"
|
||||
;;
|
||||
fedora)
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
|
||||
mkdir -p "$STAGING/rpms"
|
||||
cp /kernel-debs/*.rpm "$STAGING/rpms/"
|
||||
;;
|
||||
arch)
|
||||
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
|
||||
cp /repo/distros/arch/grow-rootfs.service "$STAGING/"
|
||||
@@ -58,6 +101,12 @@ EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
find "$STAGING" -type f \
|
||||
! -path "$STAGING/debs/*" \
|
||||
! -path "$STAGING/pkgs/*" \
|
||||
! -path "$STAGING/rpms/*" \
|
||||
-exec sed -i 's/\r$//' {} +
|
||||
|
||||
# --- Build rootfs ---
|
||||
rm -rf "$CHROOT"/* "$CHROOT"/.[!.]* 2>/dev/null || true
|
||||
|
||||
@@ -75,8 +124,10 @@ esac
|
||||
|
||||
# --- Create GPT disk image ---
|
||||
echo "=== Creating ${IMG_SIZE}MB disk image ==="
|
||||
TMPIMG="/build/ps5-${DISTRO}.img"
|
||||
dd if=/dev/zero of="$TMPIMG" bs=1M count=$IMG_SIZE conv=fsync status=progress
|
||||
TMPIMG="/output/.ps5-${DISTRO}.img.tmp"
|
||||
rm -f "$TMPIMG"
|
||||
truncate -s "${IMG_SIZE}M" "$TMPIMG"
|
||||
sync
|
||||
|
||||
parted -s "$TMPIMG" mklabel gpt
|
||||
parted -s "$TMPIMG" mkpart primary ext4 500MiB 100%
|
||||
@@ -114,8 +165,20 @@ cp -a "$CHROOT"/* /tmp/usb_root/
|
||||
sync
|
||||
|
||||
echo "=== Assembling boot partition ==="
|
||||
mv /tmp/usb_root/boot/efi/* /tmp/usb_efi/ 2>/dev/null || true
|
||||
sed "s|__DISTRO__|$ROOT_LABEL|" /repo/boot/cmdline.txt > /tmp/usb_efi/cmdline.txt
|
||||
# Batocera mounts the FAT32 partition at /boot (not /boot/efi) so its
|
||||
# batocera-part SHARE auto-detection works (greps /proc/mounts for /boot).
|
||||
# Detect either layout and copy from the right place.
|
||||
if [ -d /tmp/usb_root/boot/efi ] && [ -n "$(ls -A /tmp/usb_root/boot/efi 2>/dev/null)" ]; then
|
||||
mv /tmp/usb_root/boot/efi/* /tmp/usb_efi/ 2>/dev/null || true
|
||||
elif [ -f /tmp/usb_root/boot/bzImage ]; then
|
||||
# batocera-style: bzImage + cmdline + everything else lives directly
|
||||
# in /boot. Move everything into the EFI partition so the PS5 loader
|
||||
# can find bzImage/initrd.img/cmdline.txt at the FAT root.
|
||||
mv /tmp/usb_root/boot/* /tmp/usb_efi/ 2>/dev/null || true
|
||||
fi
|
||||
CMDLINE_TEMPLATE="/repo/distros/${DISTRO}/cmdline.txt"
|
||||
[ -f "$CMDLINE_TEMPLATE" ] || CMDLINE_TEMPLATE="/repo/boot/cmdline.txt"
|
||||
sed "s|__DISTRO__|$ROOT_LABEL|" "$CMDLINE_TEMPLATE" > /tmp/usb_efi/cmdline.txt
|
||||
cp /repo/boot/vram.txt /tmp/usb_efi/
|
||||
cp /repo/boot/kexec.sh /tmp/usb_efi/
|
||||
sync
|
||||
|
||||
@@ -28,10 +28,20 @@ cp /out/staging/.config "$STAGING/boot/config-$KVER"
|
||||
mkdir -p "$STAGING/usr/lib/modules"
|
||||
cp -a "/out/staging/lib/modules/$KVER" "$STAGING/usr/lib/modules/"
|
||||
|
||||
# Userspace bits the kernel-builder staged at /out/staging/{etc,usr/local}:
|
||||
# ps5-stage-firmware service + script (bridges nxp blob from /boot/efi),
|
||||
# ps5-iw620 modprobe.d options. Without these the WLAN driver loads but
|
||||
# request_firmware() returns -2 and wlan_pcie probe fails.
|
||||
for d in etc usr/local; do
|
||||
[ -d "/out/staging/$d" ] && { mkdir -p "$STAGING/$d"; cp -a "/out/staging/$d/." "$STAGING/$d/"; }
|
||||
done
|
||||
|
||||
# Kernel headers (for out-of-tree module builds)
|
||||
if [ -d /out/staging/headers ]; then
|
||||
# UAPI headers (/usr/include/linux/, /usr/include/asm/, etc.)
|
||||
cp -a /out/staging/headers/usr "$STAGING/usr"
|
||||
# $STAGING/usr already exists (modules were copied there), so merge
|
||||
# contents instead of creating a nested usr/usr/include/ directory.
|
||||
cp -a /out/staging/headers/usr/. "$STAGING/usr/"
|
||||
# Build headers (/usr/lib/modules/$KVER/build/)
|
||||
mkdir -p "$STAGING/usr/lib/modules/$KVER"
|
||||
cp -a /out/staging/headers/lib/modules/$KVER/build "$STAGING/usr/lib/modules/$KVER/build"
|
||||
|
||||
15
docker/kernel-builder-rpm/Dockerfile
Normal file
15
docker/kernel-builder-rpm/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM --platform=linux/amd64 fedora:44
|
||||
|
||||
# Only packaging tools needed — kernel is already compiled
|
||||
RUN dnf install -y rpm-build kmod && dnf clean all
|
||||
|
||||
WORKDIR /out/staging
|
||||
|
||||
# Pre-built artifacts are bind-mounted at /out/staging
|
||||
# Output packages go to /out
|
||||
|
||||
COPY docker/kernel-builder-rpm/build.sh /build.sh
|
||||
COPY docker/kernel-builder-rpm/linux-ps5.spec /linux-ps5.spec
|
||||
RUN chmod +x /build.sh
|
||||
|
||||
CMD ["/build.sh"]
|
||||
63
docker/kernel-builder-rpm/build.sh
Normal file
63
docker/kernel-builder-rpm/build.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Packages pre-built kernel artifacts as an rpm.
|
||||
# Runs inside Docker as root.
|
||||
# Expects build artifacts staged in /out/staging by the kernel build step.
|
||||
set -e
|
||||
|
||||
if [ ! -f /out/staging/boot/bzImage ]; then
|
||||
echo "Error: no staged artifacts in /out/staging — run the kernel build step first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine version from staged modules directory
|
||||
KVER=$(ls /out/staging/lib/modules/)
|
||||
PKGNAME="linux-ps5"
|
||||
VERSION=${KVER%%-*}
|
||||
|
||||
echo "==> Packaging kernel $KVER as rpm"
|
||||
|
||||
RPMROOT=$(mktemp -d)
|
||||
STAGE="$RPMROOT/stage"
|
||||
|
||||
# Copy staged boot artifacts
|
||||
mkdir -p "$STAGE/boot"
|
||||
cp /out/staging/boot/bzImage "$STAGE/boot/vmlinuz-$KVER"
|
||||
cp /out/staging/System.map "$STAGE/boot/System.map-$KVER"
|
||||
cp /out/staging/.config "$STAGE/boot/config-$KVER"
|
||||
|
||||
# Copy pre-installed modules (Fedora uses /usr/lib/modules)
|
||||
mkdir -p "$STAGE/usr/lib/modules"
|
||||
cp -a "/out/staging/lib/modules/$KVER" "$STAGE/usr/lib/modules/"
|
||||
|
||||
# Build headers for out-of-tree module builds. UAPI headers (/usr/include)
|
||||
# are intentionally excluded — they would conflict with Fedora's
|
||||
# kernel-headers package.
|
||||
if [ -d "/out/staging/headers/lib/modules/$KVER/build" ]; then
|
||||
cp -a "/out/staging/headers/lib/modules/$KVER/build" \
|
||||
"$STAGE/usr/lib/modules/$KVER/build"
|
||||
fi
|
||||
|
||||
# Userspace bits staged by kernel-builder at /out/staging/{etc,usr/local}:
|
||||
# ps5-stage-firmware.service + ps5-bt-quiet.service + their /usr/local/sbin
|
||||
# helpers + /etc/modprobe.d/moal.conf + /etc/modules-load.d/moal. Without
|
||||
# these, moal loads but request_firmware() returns -2 (firmware still on
|
||||
# ESP, never staged) and there's no boot-time BT phantom-hci cleanup. The
|
||||
# .deb and .pkg.tar.zst packagers already copy these; this was the missing
|
||||
# symmetric step on the rpm side.
|
||||
for d in etc usr/local; do
|
||||
if [ -d "/out/staging/$d" ]; then
|
||||
mkdir -p "$STAGE/$d"
|
||||
cp -a "/out/staging/$d/." "$STAGE/$d/"
|
||||
fi
|
||||
done
|
||||
|
||||
rpmbuild -bb \
|
||||
--define "_topdir $RPMROOT/rpmbuild" \
|
||||
--define "stagedir $STAGE" \
|
||||
--define "kver $KVER" \
|
||||
--define "ver $VERSION" \
|
||||
/linux-ps5.spec
|
||||
|
||||
cp "$RPMROOT/rpmbuild/RPMS/x86_64/${PKGNAME}-${VERSION}-1.x86_64.rpm" /out/
|
||||
|
||||
echo "==> Done: /out/${PKGNAME}-${VERSION}-1.x86_64.rpm"
|
||||
62
docker/kernel-builder-rpm/linux-ps5.spec
Normal file
62
docker/kernel-builder-rpm/linux-ps5.spec
Normal file
@@ -0,0 +1,62 @@
|
||||
# Binary repackaging of pre-built kernel artifacts — no compilation here.
|
||||
# Invoked by build.sh with: stagedir, kver, ver defined.
|
||||
%global debug_package %{nil}
|
||||
%global _build_id_links none
|
||||
%global __os_install_post %{nil}
|
||||
%global __strip /bin/true
|
||||
|
||||
Name: linux-ps5
|
||||
Version: %{ver}
|
||||
Release: 1
|
||||
Summary: PS5 Linux kernel %{kver} (image + modules + headers)
|
||||
License: GPL-2.0-only
|
||||
URL: https://kernel.org
|
||||
ExclusiveArch: x86_64
|
||||
AutoReqProv: no
|
||||
|
||||
Provides: kernel = %{ver}
|
||||
Provides: kernel-core = %{ver}
|
||||
Provides: kernel-modules = %{ver}
|
||||
Provides: kernel-devel = %{ver}
|
||||
|
||||
%description
|
||||
Linux kernel %{kver} with PlayStation 5 support patches
|
||||
(https://github.com/ps5-linux/ps5-linux-patches).
|
||||
|
||||
%install
|
||||
cp -a %{stagedir}/. %{buildroot}/
|
||||
|
||||
%files
|
||||
/boot/vmlinuz-%{kver}
|
||||
/boot/System.map-%{kver}
|
||||
/boot/config-%{kver}
|
||||
/usr/lib/modules/%{kver}
|
||||
%config(noreplace) /etc/modprobe.d/moal.conf
|
||||
%config(noreplace) /etc/modules-load.d/moal
|
||||
/etc/systemd/system/ps5-stage-firmware.service
|
||||
/etc/systemd/system/ps5-bt-quiet.service
|
||||
/etc/systemd/system/sysinit.target.wants/ps5-stage-firmware.service
|
||||
/etc/systemd/system/multi-user.target.wants/ps5-bt-quiet.service
|
||||
/usr/local/sbin/ps5-stage-firmware
|
||||
/usr/local/sbin/ps5-bt-quiet
|
||||
|
||||
%post
|
||||
echo ">> linux-ps5 post-install: kernel %{kver}"
|
||||
depmod -a %{kver} || true
|
||||
|
||||
# Rebuild initramfs (hardware-independent — the build host is not the PS5)
|
||||
if command -v dracut >/dev/null 2>&1; then
|
||||
echo ">> Rebuilding initramfs with dracut for %{kver}"
|
||||
dracut --force --no-hostonly "/boot/initrd.img-%{kver}" %{kver}
|
||||
fi
|
||||
|
||||
# Copy kernel + initrd to EFI partition
|
||||
if [ -d /boot/efi ]; then
|
||||
echo ">> Copying /boot/vmlinuz-%{kver} -> /boot/efi/bzImage"
|
||||
cp "/boot/vmlinuz-%{kver}" /boot/efi/bzImage
|
||||
echo ">> Copying /boot/initrd.img-%{kver} -> /boot/efi/initrd.img"
|
||||
cp "/boot/initrd.img-%{kver}" /boot/efi/initrd.img
|
||||
echo ">> Kernel %{kver} deployed to /boot/efi"
|
||||
else
|
||||
echo ">> /boot/efi not found, skipping EFI deploy"
|
||||
fi
|
||||
@@ -4,9 +4,9 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential bc bison dwarves flex kmod cpio gawk \
|
||||
libdw-dev libelf-dev libssl-dev \
|
||||
libdw-dev libelf-dev libssl-dev zlib1g-dev \
|
||||
python3 rsync dpkg-dev debhelper zstd \
|
||||
perl git xz-utils ccache \
|
||||
perl git ca-certificates xz-utils ccache \
|
||||
gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu libc6-dev-amd64-cross \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -34,4 +34,129 @@ make headers_install INSTALL_HDR_PATH="$HDR/usr"
|
||||
export srctree=/src SRCARCH=x86
|
||||
CC="${CROSS_COMPILE}gcc" HOSTCC=gcc MAKE=make /src/scripts/package/install-extmod-build "$HDR/lib/modules/$KVER/build"
|
||||
|
||||
# ---- NXP IW620 mwifiex driver (the PS5's onboard wifi) ----
|
||||
# Build the out-of-tree moal+mlan modules against the kernel we just built
|
||||
# and stage them into /out/staging so they get bundled in linux-ps5.{deb,rpm,
|
||||
# pkg.tar.zst}. Without this, users get no wifi until they manually run
|
||||
# ps5-linux-mwifiex/install.sh on the target (which needs gcc + headers).
|
||||
echo "=== Building NXP IW620 mwifiex driver ==="
|
||||
MWIFIEX_REPO="${MWIFIEX_REPO:-https://github.com/ps5-linux/ps5-linux-mwifiex.git}"
|
||||
MWIFIEX_REF="${MWIFIEX_REF:-main}"
|
||||
MWIFIEX_NXP_REPO=https://github.com/nxp-imx/mwifiex.git
|
||||
MWIFIEX_NXP_REF=lf-6.18.2_1.0.0
|
||||
|
||||
PS5_MW=/tmp/ps5-linux-mwifiex
|
||||
NXP_MW=/tmp/nxp-mwifiex
|
||||
rm -rf "$PS5_MW" "$NXP_MW"
|
||||
git clone "$MWIFIEX_REPO" "$PS5_MW"
|
||||
git -C "$PS5_MW" checkout "$MWIFIEX_REF" 2>/dev/null || true
|
||||
git clone --depth 1 --branch "$MWIFIEX_NXP_REF" "$MWIFIEX_NXP_REPO" "$NXP_MW"
|
||||
git -C "$NXP_MW" apply "$PS5_MW/ps5-iw620.patch"
|
||||
git -C "$NXP_MW" apply "$PS5_MW/ps5-iw620-cmd-timeout-recover.patch"
|
||||
git -C "$NXP_MW" apply "$PS5_MW/ps5-iw620-kernel71-compat.patch"
|
||||
|
||||
# Build against the kernel source we just built (out-of-tree build needs
|
||||
# the in-tree build dir, not just headers; same as install-extmod-build).
|
||||
make -C "$NXP_MW" CONFIG_OBJTOOL= KERNELDIR=/src ARCH=x86 -j"$(nproc)"
|
||||
[ -f "$NXP_MW/mlan.ko" ] && [ -f "$NXP_MW/moal.ko" ] \
|
||||
|| { echo "ERROR: mwifiex build did not produce mlan.ko/moal.ko"; exit 1; }
|
||||
|
||||
EXTRA_DIR="/out/staging/lib/modules/$KVER/extra/ps5-iw620"
|
||||
mkdir -p "$EXTRA_DIR"
|
||||
install -m 0644 "$NXP_MW/mlan.ko" "$EXTRA_DIR/mlan.ko"
|
||||
install -m 0644 "$NXP_MW/moal.ko" "$EXTRA_DIR/moal.ko"
|
||||
|
||||
# Modprobe + modules-load.d so the driver auto-loads at boot.
|
||||
mkdir -p /out/staging/etc/modprobe.d /out/staging/etc/modules-load.d
|
||||
echo moal > /out/staging/etc/modules-load.d/moal
|
||||
cat > /out/staging/etc/modprobe.d/moal.conf <<'MPCONF'
|
||||
# PS5 IW620 mwifiex (NXP moal/mlan, built out-of-tree by kernel-builder).
|
||||
softdep moal pre: cfg80211 mlan
|
||||
options moal fw_name=nxp/pcieuartiw620_combo_v1.bin pcie_int_mode=1 drv_mode=1 cfg80211_wext=4 sta_name=mlan ext_scan=1 auto_fw_reload=0 wifi_reset_config=0 sched_scan=0 ps_mode=2 auto_ds=2 amsdu_disable=1
|
||||
MPCONF
|
||||
|
||||
# Rebuild module index so modprobe moal works without depmod -a post-install.
|
||||
depmod -b /out/staging "$KVER"
|
||||
|
||||
# ---- PS5 runtime helpers packaged with the kernel ----
|
||||
mkdir -p /out/staging/usr/local/sbin
|
||||
mkdir -p /out/staging/etc/systemd/system/sysinit.target.wants
|
||||
mkdir -p /out/staging/etc/systemd/system/multi-user.target.wants
|
||||
|
||||
cat > /out/staging/usr/local/sbin/ps5-stage-firmware <<'HELPER'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
FW=nxp/pcieuartiw620_combo_v1.bin
|
||||
if [ -f /run/ostree-booted ]; then
|
||||
DST_DIR=/etc/firmware
|
||||
echo "$DST_DIR" > /sys/module/firmware_class/parameters/path 2>/dev/null || true
|
||||
else
|
||||
DST_DIR=/lib/firmware
|
||||
fi
|
||||
DST=$DST_DIR/$FW
|
||||
[ -e "$DST" ] || for d in /efi /boot/efi /boot; do
|
||||
if [ -f "$d/lib/$FW" ]; then
|
||||
install -Dm644 "$d/lib/$FW" "$DST"
|
||||
break
|
||||
fi
|
||||
done
|
||||
modprobe -r moal mlan 2>/dev/null || true
|
||||
modprobe moal 2>/dev/null || true
|
||||
HELPER
|
||||
chmod +x /out/staging/usr/local/sbin/ps5-stage-firmware
|
||||
|
||||
cat > /out/staging/etc/systemd/system/ps5-stage-firmware.service <<'UNIT'
|
||||
[Unit]
|
||||
Description=Stage PS5 NXP IW620 wifi firmware from EFI partition
|
||||
After=local-fs.target
|
||||
Before=systemd-modules-load.service network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ps5-stage-firmware
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sysinit.target
|
||||
UNIT
|
||||
ln -sf ../ps5-stage-firmware.service \
|
||||
/out/staging/etc/systemd/system/sysinit.target.wants/ps5-stage-firmware.service
|
||||
|
||||
cat > /out/staging/usr/local/sbin/ps5-bt-quiet <<'HELPER'
|
||||
#!/bin/sh
|
||||
for i in $(seq 1 15); do
|
||||
[ -n "$(ls /sys/class/bluetooth/hci* 2>/dev/null)" ] && break
|
||||
sleep 1
|
||||
done
|
||||
for h in /sys/class/bluetooth/hci*; do
|
||||
[ -e "$h/address" ] || continue
|
||||
idx=$(basename "$h" | sed 's/hci//')
|
||||
addr=$(cat "$h/address")
|
||||
if [ "$addr" = "00:00:00:00:00:00" ]; then
|
||||
hciconfig hci$idx down 2>/dev/null || btmgmt -i $idx power off 2>/dev/null || true
|
||||
else
|
||||
bluetoothctl -- select "$addr" >/dev/null 2>&1 || true
|
||||
bluetoothctl -- power on >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
HELPER
|
||||
chmod +x /out/staging/usr/local/sbin/ps5-bt-quiet
|
||||
|
||||
cat > /out/staging/etc/systemd/system/ps5-bt-quiet.service <<'UNIT'
|
||||
[Unit]
|
||||
Description=PS5: silence broken hciN + power the working one
|
||||
After=bluetooth.service
|
||||
Wants=bluetooth.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/ps5-bt-quiet
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
ln -sf ../ps5-bt-quiet.service \
|
||||
/out/staging/etc/systemd/system/multi-user.target.wants/ps5-bt-quiet.service
|
||||
|
||||
echo "=== Build artifacts staged in /out/staging ==="
|
||||
|
||||
@@ -18,9 +18,20 @@ cp "$STAGING/System.map" "$PKG/boot/System.map-$KVER"
|
||||
cp "$STAGING/.config" "$PKG/boot/config-$KVER"
|
||||
cp -a "$STAGING/lib/modules/$KVER" "$PKG/lib/modules/"
|
||||
|
||||
# Userspace bits the kernel-builder staged at /out/staging/{etc,usr/local}:
|
||||
# ps5-stage-firmware service + script (bridges nxp blob from /boot/efi),
|
||||
# ps5-iw620 modprobe.d options. Without these the WLAN driver loads but
|
||||
# request_firmware() returns -2 and wlan_pcie probe fails.
|
||||
for d in etc usr/local; do
|
||||
[ -d "$STAGING/$d" ] && { mkdir -p "$PKG/$d"; cp -a "$STAGING/$d/." "$PKG/$d/"; }
|
||||
done
|
||||
|
||||
# Kernel headers (for out-of-tree module builds)
|
||||
if [ -d "$STAGING/headers" ]; then
|
||||
cp -a "$STAGING/headers/usr" "$PKG/usr"
|
||||
# $PKG/usr may already exist from other staged files, so merge contents
|
||||
# instead of creating a nested usr/usr/include/ directory.
|
||||
mkdir -p "$PKG/usr"
|
||||
cp -a "$STAGING/headers/usr/." "$PKG/usr/"
|
||||
# Point /lib/modules/$KVER/build at the installed headers
|
||||
HDR_DEST="/usr/lib/modules/$KVER/build"
|
||||
mkdir -p "$PKG/usr/lib/modules/$KVER"
|
||||
|
||||
Reference in New Issue
Block a user