Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa28b20df7 | |||
| e21a2dd0ec | |||
| e57149f496 | |||
| ed18cce7d0 | |||
| 34c687d160 | |||
| 27f923a817 | |||
| 60a29c2590 | |||
| 11eb0f1aa8 | |||
| 7bf226e73b | |||
| 2a4427919e | |||
| 155b427478 | |||
| 9575e5517d | |||
| 57517217e4 | |||
| 877fe92a4b | |||
| e7a1825b50 | |||
| 6d32d10a8a | |||
| 1cb5099add | |||
| 7757449452 | |||
| d1a6739405 | |||
|
|
6ecc1dc87b |
22 changed files with 713 additions and 162 deletions
|
|
@ -44,12 +44,14 @@ jobs:
|
||||||
path: dist/*
|
path: dist/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
# Ship every locally built apk (kernel, modemmanager, libqmi, imsd,
|
# Ship every locally built apk (kernel, modemmanager, libqmi,
|
||||||
# callaudioshim, audio files, ...) to the Forgejo Alpine registry, so
|
# callaudioshim, audio files, ...) to the Forgejo Alpine registry, so
|
||||||
# installed systems get updates via 'apk upgrade' instead of losing
|
# installed systems get updates via 'apk upgrade' instead of losing
|
||||||
# the FP6 patches to the next upstream version bump. Requires the
|
# the FP6 patches to the next upstream version bump. Requires the
|
||||||
# PACKAGE_TOKEN repo secret (catbot account, package:write scope);
|
# PACKAGE_TOKEN repo secret (catbot account, package:write scope);
|
||||||
# skips quietly until it exists. 409 = same version already published.
|
# skips quietly until it exists. 409 = same version already published.
|
||||||
|
# imsd and fingerprintd are skipped: build.sh 3b took them FROM the
|
||||||
|
# registry (re-signed for the chroot), so they are not ours to publish.
|
||||||
- name: Publish packages to the apk registry
|
- name: Publish packages to the apk registry
|
||||||
env:
|
env:
|
||||||
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
|
@ -62,6 +64,9 @@ jobs:
|
||||||
found=0
|
found=0
|
||||||
for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/*.apk; do
|
for f in /home/build/.local/var/pmbootstrap/packages/*/aarch64/*.apk; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
|
case "$(basename "$f")" in
|
||||||
|
imsd-*|fingerprintd-*) echo "registry-sourced, not republished: $(basename "$f")"; continue ;;
|
||||||
|
esac
|
||||||
found=1
|
found=1
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
--user "catbot:$PACKAGE_TOKEN" --upload-file "$f" \
|
--user "catbot:$PACKAGE_TOKEN" --upload-file "$f" \
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -14,10 +14,9 @@ Maintained by [Jorijn van der Graaf](https://catcrafts.net/about)
|
||||||
|
|
||||||
Everything on the `combined-stable` branch: display, touch, wifi, cellular
|
Everything on the `combined-stable` branch: display, touch, wifi, cellular
|
||||||
data, NFC (reader), speaker audio, microphone, IMU, magnetometer, barometer,
|
data, NFC (reader), speaker audio, microphone, IMU, magnetometer, barometer,
|
||||||
ambient light/proximity — plus VoLTE calls (both directions) through imsd.
|
ambient light/proximity, fingerprint unlock through
|
||||||
|
[fingerprintd](https://forgejo.catcrafts.net/Catcrafts/fingerprintd), plus
|
||||||
Verified on exactly one device / one carrier (KPN NL). Reports from other
|
VoLTE calls (both directions) through imsd.
|
||||||
carriers are very welcome.
|
|
||||||
|
|
||||||
## Flashing
|
## Flashing
|
||||||
|
|
||||||
|
|
@ -36,8 +35,8 @@ cd fp6-img
|
||||||
inside Linux where the bootloader's NV-wipe cannot see it, then reads the modem
|
inside Linux where the bootloader's NV-wipe cannot see it, then reads the modem
|
||||||
NV back and reports the result. The bootloader never writes `boot`.
|
NV back and reports the result. The bootloader never writes `boot`.
|
||||||
|
|
||||||
Default login: `user` / `147147` (same as official postmarketOS images —
|
Default login: `user` / `147147` (same as official postmarketOS images).
|
||||||
change it). **Never re-lock the bootloader** with a custom image installed.
|
**Never re-lock the bootloader** with a custom image installed.
|
||||||
|
|
||||||
## VoLTE configuration
|
## VoLTE configuration
|
||||||
|
|
||||||
|
|
@ -48,27 +47,8 @@ imsd is installed but needs your carrier's P-CSCF address:
|
||||||
PCSCF=<your carrier's P-CSCF IPv6 address>
|
PCSCF=<your carrier's P-CSCF IPv6 address>
|
||||||
```
|
```
|
||||||
|
|
||||||
Find it in a stock-firmware capture or your carrier's IMS documentation, then
|
Find it via android adb using `adb shell dumpsys telephony.registry`, then
|
||||||
`systemctl restart imsd` (the service is enabled at boot and waits for this
|
`systemctl restart imsd` (the service is enabled at boot and waits for this
|
||||||
file to exist). See the
|
file to exist). See the
|
||||||
[imsd README](https://forgejo.catcrafts.net/Catcrafts/imsd) for the full
|
[imsd README](https://forgejo.catcrafts.net/Catcrafts/imsd) for the full
|
||||||
variable reference and carrier assumptions.
|
variable reference and carrier assumptions.
|
||||||
|
|
||||||
## How it builds
|
|
||||||
|
|
||||||
`build.sh` drives [pmbootstrap](https://gitlab.postmarketos.org/postmarketOS/pmbootstrap)
|
|
||||||
with the `aports/` overlay in this repository:
|
|
||||||
|
|
||||||
- `device/linux-postmarketos-qcom-milos` — the pmaports kernel aport,
|
|
||||||
repointed at `combined-stable` with the exact kernel config the development FP6 runs.
|
|
||||||
- `modem/imsd` — imsd, packaged from source. Its `provides=81voltd` keeps the
|
|
||||||
conflicting modem-firmware IMS helper off the image (two IMS stacks cannot
|
|
||||||
share one PDN).
|
|
||||||
- `main/postmarketos-config-nftables` — adds the `-imsd` subpackage accepting
|
|
||||||
imsd's IPsec-protected SIP ports on the IMS PDN (auto-installed alongside
|
|
||||||
imsd).
|
|
||||||
- `device/catcrafts-fp6-repo` — subscribes the installed system to the
|
|
||||||
[Catcrafts apk registry](https://forgejo.catcrafts.net/Catcrafts/-/packages)
|
|
||||||
(signing key + `/etc/apk/repositories` entry). CI publishes every built
|
|
||||||
package there, so `apk upgrade` delivers kernel/imsd/GNSS updates between
|
|
||||||
image releases.
|
|
||||||
84
apk-resign.py
Executable file
84
apk-resign.py
Executable file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Re-sign an apk (v2 format) with another RSA key, in place.
|
||||||
|
|
||||||
|
apk-resign.py <pkg.apk> <private key .rsa> <public key file name>
|
||||||
|
|
||||||
|
An apk is three concatenated gzip streams: the signature tar, the control
|
||||||
|
tar (.PKGINFO) and the data tar. The signature covers the control stream
|
||||||
|
only, so replacing the first stream re-signs the package while its identity
|
||||||
|
checksum (over the control stream) and its contents stay byte-identical.
|
||||||
|
This is what abuild-sign does to the control segment when abuild packages.
|
||||||
|
"""
|
||||||
|
import gzip
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
DIGEST = {"RSA": "sha1", "RSA256": "sha256", "RSA512": "sha512"}
|
||||||
|
|
||||||
|
|
||||||
|
def gzip_members(data):
|
||||||
|
off = 0
|
||||||
|
while off < len(data):
|
||||||
|
d = zlib.decompressobj(31)
|
||||||
|
d.decompress(data[off:])
|
||||||
|
end = len(data) - len(d.unused_data)
|
||||||
|
if end <= off:
|
||||||
|
raise ValueError("gzip stream did not advance")
|
||||||
|
yield data[off:end]
|
||||||
|
off = end
|
||||||
|
|
||||||
|
|
||||||
|
def first_entry_name(tar):
|
||||||
|
"""Name of the first regular file in a tar stream, skipping pax headers."""
|
||||||
|
off = 0
|
||||||
|
while off + 512 <= len(tar):
|
||||||
|
hdr = tar[off:off + 512]
|
||||||
|
size = int(hdr[124:136].split(b"\0")[0].strip() or b"0", 8)
|
||||||
|
if hdr[156:157] not in (b"x", b"g"): # not a pax extended/global header
|
||||||
|
return hdr[:100].rstrip(b"\0").decode()
|
||||||
|
off += 512 + (size + 511) // 512 * 512
|
||||||
|
raise ValueError("no file entry in signature tar")
|
||||||
|
|
||||||
|
|
||||||
|
def main(path, privkey, pubname):
|
||||||
|
members = list(gzip_members(open(path, "rb").read()))
|
||||||
|
if len(members) != 3:
|
||||||
|
sys.exit(f"{path}: expected 3 gzip streams, found {len(members)}")
|
||||||
|
old_sig, control, payload = members
|
||||||
|
# keep the original digest type: the entry is .SIGN.<RSA|RSA256|RSA512>.<key>
|
||||||
|
name = first_entry_name(gzip.decompress(old_sig))
|
||||||
|
kind = name.split(".")[2] if name.startswith(".SIGN.") else ""
|
||||||
|
if kind not in DIGEST:
|
||||||
|
sys.exit(f"{path}: unexpected signature entry {name!r}")
|
||||||
|
if not os.access(privkey, os.R_OK):
|
||||||
|
sys.exit(f"{privkey}: not readable by uid {os.getuid()} (pmbootstrap's "
|
||||||
|
"abuild-keygen runs as the chroot user, uid 12345, and leaves "
|
||||||
|
"the key 0600 to it - sign from a readable copy)")
|
||||||
|
try:
|
||||||
|
sig = subprocess.run(
|
||||||
|
["openssl", "dgst", f"-{DIGEST[kind]}", "-sign", privkey],
|
||||||
|
input=control, stdout=subprocess.PIPE, check=True,
|
||||||
|
).stdout
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
sys.exit(f"openssl dgst -sign exited {e.returncode} signing {path}")
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with tarfile.open(fileobj=buf, mode="w", format=tarfile.USTAR_FORMAT) as tar:
|
||||||
|
info = tarfile.TarInfo(f".SIGN.{kind}.{pubname}")
|
||||||
|
info.size = len(sig)
|
||||||
|
info.mode = 0o644
|
||||||
|
tar.addfile(info, io.BytesIO(sig))
|
||||||
|
# like abuild-tar --cut: header + data blocks, no end-of-archive marker
|
||||||
|
cut = 512 + (len(sig) + 511) // 512 * 512
|
||||||
|
new_sig = gzip.compress(buf.getvalue()[:cut], mtime=0)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(new_sig + control + payload)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) != 4:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
main(*sys.argv[1:])
|
||||||
|
|
@ -10,21 +10,30 @@
|
||||||
# their post-install/post-upgrade should also run
|
# their post-install/post-upgrade should also run
|
||||||
# /usr/lib/fp6-vendor-blobs/extract --if-device so a package upgrade that
|
# /usr/lib/fp6-vendor-blobs/extract --if-device so a package upgrade that
|
||||||
# drops a previously-shipped blob restores the file immediately instead of
|
# drops a previously-shipped blob restores the file immediately instead of
|
||||||
# at the next boot. First consumer: soc-fairphone-fp6-audio (aw88261 acf).
|
# at the next boot (--refresh in addition for an mbn consumer: it re-derives
|
||||||
|
# the trustlet from the active slot even if a file is present). Consumers:
|
||||||
|
# soc-fairphone-fp6-audio (aw88261 acf), fingerprintd (focal64 trustlet).
|
||||||
|
#
|
||||||
|
# 1-r3 (2026-09-11): active slot first, an unpinned ('-') structurally
|
||||||
|
# verified mode for mbn, --refresh -- after two field units got no
|
||||||
|
# fingerprint sensor from a whole-image hash pin that can only match one
|
||||||
|
# Android build (fp6 repo journal/blobs/, journal/fingerprint/).
|
||||||
maintainer="Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"
|
maintainer="Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"
|
||||||
pkgname=fp6-vendor-blobs
|
pkgname=fp6-vendor-blobs
|
||||||
pkgver=1
|
pkgver=1
|
||||||
pkgrel=0
|
pkgrel=3
|
||||||
pkgdesc="On-device extraction of vendor blobs from the stock Android partitions"
|
pkgdesc="On-device extraction of vendor blobs from the stock Android partitions"
|
||||||
url="https://forgejo.catcrafts.net/Catcrafts/fp6-img"
|
url="https://forgejo.catcrafts.net/Catcrafts/fp6-img"
|
||||||
arch="noarch"
|
arch="noarch"
|
||||||
license="MIT"
|
license="MIT"
|
||||||
# fallback mapper for when the initramfs didn't map the dynamic partitions
|
# fallback mapper for when the initramfs didn't map the dynamic partitions
|
||||||
depends="make-dynpart-mappings"
|
depends="make-dynpart-mappings"
|
||||||
|
install="$pkgname.post-upgrade"
|
||||||
options="!check"
|
options="!check"
|
||||||
source="
|
source="
|
||||||
fp6-vendor-blobs-extract
|
fp6-vendor-blobs-extract
|
||||||
fp6-vendor-blobs.service
|
fp6-vendor-blobs.service
|
||||||
|
fp6-vendor-blobs.preset
|
||||||
"
|
"
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
|
|
@ -34,14 +43,26 @@ package() {
|
||||||
"$pkgdir"/usr/lib/systemd/system/fp6-vendor-blobs.service
|
"$pkgdir"/usr/lib/systemd/system/fp6-vendor-blobs.service
|
||||||
# enabled unconditionally: the unit is a fast no-op once every manifest
|
# enabled unconditionally: the unit is a fast no-op once every manifest
|
||||||
# dest exists, and blobs appearing on first boot must not depend on a
|
# dest exists, and blobs appearing on first boot must not depend on a
|
||||||
# manual systemctl enable
|
# manual systemctl enable. sysinit = the real (pre-coldplug) run;
|
||||||
mkdir -p "$pkgdir"/etc/systemd/system/multi-user.target.wants
|
# multi-user = the post-udev retry if the early run failed (see unit)
|
||||||
|
mkdir -p "$pkgdir"/etc/systemd/system/sysinit.target.wants \
|
||||||
|
"$pkgdir"/etc/systemd/system/multi-user.target.wants
|
||||||
|
ln -s /usr/lib/systemd/system/fp6-vendor-blobs.service \
|
||||||
|
"$pkgdir"/etc/systemd/system/sysinit.target.wants/fp6-vendor-blobs.service
|
||||||
ln -s /usr/lib/systemd/system/fp6-vendor-blobs.service \
|
ln -s /usr/lib/systemd/system/fp6-vendor-blobs.service \
|
||||||
"$pkgdir"/etc/systemd/system/multi-user.target.wants/fp6-vendor-blobs.service
|
"$pkgdir"/etc/systemd/system/multi-user.target.wants/fp6-vendor-blobs.service
|
||||||
|
# ...and a preset, because the symlink alone does NOT survive: image
|
||||||
|
# build runs `systemctl preset-all`, which removes .wants links for any
|
||||||
|
# unit not enabled by a preset. That is what shipped the 2026-08-24
|
||||||
|
# image with the service `disabled; preset: disabled` -- no blob
|
||||||
|
# extracted, no sound card. See journal/blobs/ 2026-08-28.
|
||||||
|
install -Dm644 "$srcdir"/fp6-vendor-blobs.preset \
|
||||||
|
"$pkgdir"/usr/lib/systemd/system-preset/50-fp6-vendor-blobs.preset
|
||||||
mkdir -p "$pkgdir"/usr/share/fp6-vendor-blobs/manifest.d
|
mkdir -p "$pkgdir"/usr/share/fp6-vendor-blobs/manifest.d
|
||||||
}
|
}
|
||||||
|
|
||||||
sha512sums="
|
sha512sums="
|
||||||
a71b2c86f980734d0aae6e135b26272fe52ae5603050bea52f55f7e74c8bd98c62b6194047711248d4fef40851792adc47d77c40016d99a0d68ecd2459894b80 fp6-vendor-blobs-extract
|
e9618b4a1ccbe0913a608b32a1a9e87337e3da79a3d7ccc7f49292ae9aaa9406d2ea9817d63b3cfff2c4346b80dd2476c10725f4f0f95c859acd17b22ec03a16 fp6-vendor-blobs-extract
|
||||||
e2c03e5016f1848c57e6160ce432530b181ff5d34a5aaf1822f0ffa5fe71d16691657ada4a5789c4d4209e14e83f43aab072d01f644b07b93648f91e2a7bb0ca fp6-vendor-blobs.service
|
b4c290095d9f39515378dfef08de720ce49324210342aa13c131dfce1103785e796e6f821f0c659671a4c44b46f466ce0e03f11f216fdcdee2a99db5e7970800 fp6-vendor-blobs.service
|
||||||
|
9e79dd0aed13f11a71282aa24b2a26331e85c105e25ab0c0fed6189b8c300769a5f4308b18b91d9855868d658ad3a57c03e26c9b11bd27fd5e03f9a5decbbd6a fp6-vendor-blobs.preset
|
||||||
"
|
"
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,15 @@
|
||||||
# processed in sorted order; '#' comments and blank lines ignored:
|
# processed in sorted order; '#' comments and blank lines ignored:
|
||||||
#
|
#
|
||||||
# file <partition[,partition...]> <path-in-partition> <dest> <sha256>
|
# file <partition[,partition...]> <path-in-partition> <dest> <sha256>
|
||||||
|
# mbn <partition[,partition...]> <dir-in-partition> <name> <dest> <sha256|->
|
||||||
# rebind <bus> <device>
|
# rebind <bus> <device>
|
||||||
#
|
#
|
||||||
|
# Partition lists are tried in the order written, except that on an A/B
|
||||||
|
# device the ACTIVE slot's partitions (androidboot.slot_suffix in
|
||||||
|
# /proc/cmdline) come first: the other slot may hold a different Android
|
||||||
|
# build, and for a signed image only the active slot's copy is guaranteed to
|
||||||
|
# match the TZ that is running.
|
||||||
|
#
|
||||||
# file: mount the first available listed partition READ-ONLY (ext4 also
|
# file: mount the first available listed partition READ-ONLY (ext4 also
|
||||||
# gets -o noload - never a byte written to the stock partitions, not
|
# gets -o noload - never a byte written to the stock partitions, not
|
||||||
# even a journal replay), copy <path-in-partition> to <dest>, verify the
|
# even a journal replay), copy <path-in-partition> to <dest>, verify the
|
||||||
|
|
@ -20,6 +27,26 @@
|
||||||
# unverified blob is never installed and a missing one never silently
|
# unverified blob is never installed and a missing one never silently
|
||||||
# skipped. Dests that already exist are left alone (no hashing: a
|
# skipped. Dests that already exist are left alone (no hashing: a
|
||||||
# deliberately replaced file stays).
|
# deliberately replaced file stays).
|
||||||
|
# mbn: the same, for a Qualcomm trustlet, which is not shipped as one file.
|
||||||
|
# QTEE images live in the modem partition's image/ as an ELF header+hashes
|
||||||
|
# file (<name>.mdt) plus one payload per program header (<name>.b00, .b01,
|
||||||
|
# ...), and the loader wants them written back at each segment's p_offset.
|
||||||
|
# Reassembly is therefore not a concatenation: segments are page aligned
|
||||||
|
# but not contiguous, gaps stay zero, and two segments may share an offset
|
||||||
|
# (focal64 has two such pairs), so they are written in index order and the
|
||||||
|
# later one wins. With a real sha256 the guarantees are file's: the hash is
|
||||||
|
# of the reassembled image, a mismatch tries the next partition, an
|
||||||
|
# unverified image is never installed. With '-' the image is verified
|
||||||
|
# STRUCTURALLY instead - ELF64 header, every segment present at the size
|
||||||
|
# its program header declares, page-aligned offsets, sane total - and not
|
||||||
|
# against a pinned hash. That is the right mode for an OEM-signed trustlet:
|
||||||
|
# the OEM re-signs it every Android release, so one whole-image hash matches
|
||||||
|
# exactly one build (six builds, six hashes, one trustlet: fp6 repo
|
||||||
|
# journal/fingerprint/ 2026-09-07..11, two field units failed on the pin),
|
||||||
|
# while the loader in TZ verifies the signature and the per-segment hashes
|
||||||
|
# itself and refuses a damaged or foreign image (one flipped byte ->
|
||||||
|
# ERROR_ELF_SIGNATURE_ERROR, measured 2026-09-03). The sha256 of what was
|
||||||
|
# installed is logged either way.
|
||||||
# rebind: if this fragment's run extracted at least one file, unbind and
|
# rebind: if this fragment's run extracted at least one file, unbind and
|
||||||
# re-probe <device> on <bus> so the consuming driver picks the file up
|
# re-probe <device> on <bus> so the consuming driver picks the file up
|
||||||
# in the same boot. Unconditional on purpose: a still-bound consumer may
|
# in the same boot. Unconditional on purpose: a still-bound consumer may
|
||||||
|
|
@ -37,6 +64,13 @@
|
||||||
# --if-device: exit 0 quietly when no stock super partition is visible
|
# --if-device: exit 0 quietly when no stock super partition is visible
|
||||||
# (apk post-install scripts run inside build/CI chroots too; on images
|
# (apk post-install scripts run inside build/CI chroots too; on images
|
||||||
# built there the first-boot service does the real extraction).
|
# built there the first-boot service does the real extraction).
|
||||||
|
# --refresh: re-derive every mbn dest even if it exists, replacing it only
|
||||||
|
# with an image that verifies (a failed refresh leaves the old file). For
|
||||||
|
# the consumer's post-install/post-upgrade: a fresh 'apk add' gets its
|
||||||
|
# trustlet without a reboot, and a trustlet that was hand-placed or pinned
|
||||||
|
# to another build is replaced by the active slot's on the next upgrade.
|
||||||
|
# file dests are still left alone: re-copying the acf would rebind the
|
||||||
|
# sound card on every upgrade for nothing.
|
||||||
|
|
||||||
MANIFEST_DIR=/usr/share/fp6-vendor-blobs/manifest.d
|
MANIFEST_DIR=/usr/share/fp6-vendor-blobs/manifest.d
|
||||||
SUPER=/dev/disk/by-partlabel/super
|
SUPER=/dev/disk/by-partlabel/super
|
||||||
|
|
@ -44,6 +78,10 @@ MNT=
|
||||||
MNT_PART=
|
MNT_PART=
|
||||||
CREATED=
|
CREATED=
|
||||||
TRIED_MAPPING=
|
TRIED_MAPPING=
|
||||||
|
IF_DEVICE=
|
||||||
|
REFRESH=
|
||||||
|
# "a" or "b" on an A/B device (androidboot.slot_suffix=_a), else empty
|
||||||
|
ACTIVE_SLOT=$(tr ' ' '\n' </proc/cmdline 2>/dev/null | sed -n 's/^androidboot\.slot_suffix=_\([ab]\)$/\1/p' | head -n1)
|
||||||
|
|
||||||
log() { echo "fp6-vendor-blobs: $*"; }
|
log() { echo "fp6-vendor-blobs: $*"; }
|
||||||
|
|
||||||
|
|
@ -103,9 +141,125 @@ mount_part() {
|
||||||
MNT_PART=$1
|
MNT_PART=$1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The listed partitions, space separated, the active slot's first.
|
||||||
|
order_parts() { # <partition,...>
|
||||||
|
first= rest=
|
||||||
|
for p in $(echo "$1" | tr ',' ' '); do
|
||||||
|
if [ -n "$ACTIVE_SLOT" ] && [ "${p%_$ACTIVE_SLOT}" != "$p" ]; then
|
||||||
|
first="$first $p"
|
||||||
|
else
|
||||||
|
rest="$rest $p"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "$first $rest"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Little-endian scalars out of an ELF header. aarch64 is little endian and so
|
||||||
|
# is the image, so od's host order is the right one.
|
||||||
|
u64() { od -An -tu8 -j "$2" -N 8 "$1" | tr -d ' '; }
|
||||||
|
u16() { od -An -tu2 -j "$2" -N 2 "$1" | tr -d ' '; }
|
||||||
|
u8() { od -An -tu1 -j "$2" -N 1 "$1" | tr -d ' '; }
|
||||||
|
hex4() { od -An -tx1 -N 4 "$1" | tr -d ' \n'; }
|
||||||
|
fsize() { stat -c %s "$1"; }
|
||||||
|
|
||||||
|
# Reassemble <dir>/<name>.mdt + .b0N into a flat image at <out>. Mirrors
|
||||||
|
# utilities/ta-analysis/reassemble.py in the fp6 bring-up repo, which is where
|
||||||
|
# the format was worked out and where the known-good hash comes from.
|
||||||
|
# POSIX sh has no locals, so these names are deliberately distinct from
|
||||||
|
# extract_mbn's: reassemble() taking rdir= would rewrite its CALLER's copy to
|
||||||
|
# the mount path, and the next partition in the retry loop would then be
|
||||||
|
# searched at $MNT/$MNT/...
|
||||||
|
reassemble() { # <dir> <name> <out>
|
||||||
|
mdir=$1 mname=$2 mout=$3
|
||||||
|
mdt="$mdir/$mname.mdt"
|
||||||
|
[ -f "$mdt" ] || return 1
|
||||||
|
# Structure first, before anything is written: an ELF64 header whose
|
||||||
|
# program header table fits in the .mdt, and for every segment with
|
||||||
|
# contents a .b0N file of exactly the declared size at a page-aligned
|
||||||
|
# offset. This is the whole verification when the manifest pins no hash;
|
||||||
|
# the loader's own signature check does the rest.
|
||||||
|
[ "$(hex4 "$mdt")" = 7f454c46 ] || { log "$mname.mdt: not an ELF image"; return 1; }
|
||||||
|
[ "$(u8 "$mdt" 4)" = 2 ] || { log "$mname.mdt: not ELF64"; return 1; }
|
||||||
|
phoff=$(u64 "$mdt" 32) phentsize=$(u16 "$mdt" 54) phnum=$(u16 "$mdt" 56)
|
||||||
|
[ -n "$phoff" ] && [ -n "$phentsize" ] && [ -n "$phnum" ] || return 1
|
||||||
|
[ "$phentsize" -eq 56 ] 2>/dev/null || { log "$mname.mdt: phentsize $phentsize"; return 1; }
|
||||||
|
[ "$phnum" -gt 0 ] 2>/dev/null && [ "$phnum" -le 64 ] || { log "$mname.mdt: phnum $phnum"; return 1; }
|
||||||
|
[ "$(fsize "$mdt")" -ge $((phoff + phnum * phentsize)) ] || { log "$mname.mdt: shorter than its program header table"; return 1; }
|
||||||
|
|
||||||
|
# The image is as long as the furthest segment reaches; everything no
|
||||||
|
# segment covers stays zero.
|
||||||
|
total=0 i=0
|
||||||
|
while [ "$i" -lt "$phnum" ]; do
|
||||||
|
o=$((phoff + i * phentsize))
|
||||||
|
pfsz=$(u64 "$mdt" $((o + 32)))
|
||||||
|
if [ "$pfsz" -gt 0 ]; then
|
||||||
|
poff=$(u64 "$mdt" $((o + 8)))
|
||||||
|
seg=$(printf '%s/%s.b%02d' "$mdir" "$mname" "$i")
|
||||||
|
[ -f "$seg" ] || { log "$mname: segment $i missing"; return 1; }
|
||||||
|
[ "$(fsize "$seg")" -eq "$pfsz" ] || { log "$mname: segment $i is $(fsize "$seg") bytes, header says $pfsz"; return 1; }
|
||||||
|
# dd seeks in whole blocks, which is only correct because
|
||||||
|
# every p_offset in these images is page aligned. Refuse
|
||||||
|
# rather than silently misplace a segment if that changes.
|
||||||
|
[ $((poff % 4096)) -eq 0 ] || { log "$mname: segment $i offset $poff is not page aligned"; return 1; }
|
||||||
|
[ $((poff + pfsz)) -gt "$total" ] && total=$((poff + pfsz))
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
[ "$total" -gt 0 ] || { log "$mname: no segment has contents"; return 1; }
|
||||||
|
# an order of magnitude above any TA; a garbage p_offset would otherwise
|
||||||
|
# make a sparse multi-GiB file that then gets hashed
|
||||||
|
[ "$total" -le $((64 * 1024 * 1024)) ] || { log "$mname: image would be $total bytes"; return 1; }
|
||||||
|
: > "$mout" || return 1
|
||||||
|
truncate -s "$total" "$mout" || return 1
|
||||||
|
|
||||||
|
i=0
|
||||||
|
while [ "$i" -lt "$phnum" ]; do
|
||||||
|
o=$((phoff + i * phentsize))
|
||||||
|
pfsz=$(u64 "$mdt" $((o + 32)))
|
||||||
|
if [ "$pfsz" -gt 0 ]; then
|
||||||
|
poff=$(u64 "$mdt" $((o + 8)))
|
||||||
|
seg=$(printf '%s/%s.b%02d' "$mdir" "$mname" "$i")
|
||||||
|
dd if="$seg" of="$mout" bs=4096 seek=$((poff / 4096)) \
|
||||||
|
conv=notrunc 2>/dev/null || return 1
|
||||||
|
fi
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_mbn() { # <partition,...> <dir-in-partition> <name> <dest> <sha256|->
|
||||||
|
parts=$1 rdir=$2 rname=$3 dest=$4 want=$5
|
||||||
|
for part in $(order_parts "$parts"); do
|
||||||
|
mount_part "$part" || { log "$part: not mountable, trying next"; continue; }
|
||||||
|
[ -f "$MNT/$rdir/$rname.mdt" ] || { log "$part: no $rdir/$rname.mdt, trying next"; continue; }
|
||||||
|
tmp="$dest.fp6-extract.$$"
|
||||||
|
mkdir -p "${dest%/*}" || fail "cannot create ${dest%/*}"
|
||||||
|
if ! reassemble "$MNT/$rdir" "$rname" "$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
log "$part: reassembling $rname failed, trying next"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
got=$(sha256sum "$tmp" | awk '{print $1}')
|
||||||
|
if [ "$want" != - ] && [ "$got" != "$want" ]; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
log "$part:$rdir/$rname sha256 $got != expected, trying next"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
chmod 644 "$tmp" && mv "$tmp" "$dest" || { rm -f "$tmp"; fail "installing $dest failed"; }
|
||||||
|
log "reassembled $part:$rdir/$rname.{mdt,b0N} -> $dest (sha256 $got)"
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
kept=
|
||||||
|
[ -e "$dest" ] && kept=" (the existing file is left in place)"
|
||||||
|
if [ "$want" = - ]; then
|
||||||
|
fail "no listed partition ($parts) yields a well-formed $rname - $dest NOT installed$kept"
|
||||||
|
fi
|
||||||
|
fail "no listed partition ($parts) yields $rname with sha256 $want - $dest NOT installed$kept"
|
||||||
|
}
|
||||||
|
|
||||||
extract() { # <partition,...> <path-in-partition> <dest> <sha256>
|
extract() { # <partition,...> <path-in-partition> <dest> <sha256>
|
||||||
parts=$1 src=$2 dest=$3 want=$4
|
parts=$1 src=$2 dest=$3 want=$4
|
||||||
for part in $(echo "$parts" | tr ',' ' '); do
|
for part in $(order_parts "$parts"); do
|
||||||
mount_part "$part" || { log "$part: not mountable, trying next"; continue; }
|
mount_part "$part" || { log "$part: not mountable, trying next"; continue; }
|
||||||
[ -f "$MNT/$src" ] || { log "$part: no $src, trying next"; continue; }
|
[ -f "$MNT/$src" ] || { log "$part: no $src, trying next"; continue; }
|
||||||
tmp="$dest.fp6-extract.$$"
|
tmp="$dest.fp6-extract.$$"
|
||||||
|
|
@ -144,7 +298,14 @@ rebind_all() { # <bus/device ...>
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "${1:-}" = --if-device ] && [ ! -b "$SUPER" ]; then
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--if-device) IF_DEVICE=1 ;;
|
||||||
|
--refresh) REFRESH=1 ;;
|
||||||
|
*) fail "unknown option '$arg'" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [ -n "$IF_DEVICE" ] && [ ! -b "$SUPER" ]; then
|
||||||
log "no stock super partition visible (build chroot?), nothing to do"
|
log "no stock super partition visible (build chroot?), nothing to do"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -155,17 +316,21 @@ fi
|
||||||
missing=
|
missing=
|
||||||
for f in "$MANIFEST_DIR"/*.manifest; do
|
for f in "$MANIFEST_DIR"/*.manifest; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
while read -r kind _ _ dest _; do
|
while read -r kind a b c d e; do
|
||||||
[ "$kind" = file ] && [ ! -e "$dest" ] && missing=1
|
case "$kind" in
|
||||||
|
file) [ -e "$c" ] || missing=1 ;;
|
||||||
|
mbn) [ -e "$d" ] && [ -z "$REFRESH" ] || missing=1 ;;
|
||||||
|
esac
|
||||||
done < "$f"
|
done < "$f"
|
||||||
done
|
done
|
||||||
[ -z "$missing" ] && exit 0
|
[ -z "$missing" ] && exit 0
|
||||||
|
[ -n "$ACTIVE_SLOT" ] && log "active slot $ACTIVE_SLOT"
|
||||||
|
|
||||||
for f in "$MANIFEST_DIR"/*.manifest; do
|
for f in "$MANIFEST_DIR"/*.manifest; do
|
||||||
[ -e "$f" ] || continue
|
[ -e "$f" ] || continue
|
||||||
extracted=
|
extracted=
|
||||||
rebinds=
|
rebinds=
|
||||||
while read -r kind a b c d; do
|
while read -r kind a b c d e; do
|
||||||
case "$kind" in
|
case "$kind" in
|
||||||
''|'#'*) ;;
|
''|'#'*) ;;
|
||||||
file)
|
file)
|
||||||
|
|
@ -174,6 +339,12 @@ for f in "$MANIFEST_DIR"/*.manifest; do
|
||||||
extract "$a" "$b" "$c" "$d" </dev/null
|
extract "$a" "$b" "$c" "$d" </dev/null
|
||||||
extracted=1
|
extracted=1
|
||||||
;;
|
;;
|
||||||
|
mbn)
|
||||||
|
[ -n "$e" ] || fail "$f: malformed mbn line"
|
||||||
|
[ -e "$d" ] && [ -z "$REFRESH" ] && continue
|
||||||
|
extract_mbn "$a" "$b" "$c" "$d" "$e" </dev/null
|
||||||
|
extracted=1
|
||||||
|
;;
|
||||||
rebind)
|
rebind)
|
||||||
[ -n "$b" ] || fail "$f: malformed rebind line"
|
[ -n "$b" ] || fail "$f: malformed rebind line"
|
||||||
rebinds="$rebinds $a/$b"
|
rebinds="$rebinds $a/$b"
|
||||||
|
|
|
||||||
10
aports/device/fp6-vendor-blobs/fp6-vendor-blobs.post-upgrade
Normal file
10
aports/device/fp6-vendor-blobs/fp6-vendor-blobs.post-upgrade
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Re-apply the preset: images built before the preset file existed (the
|
||||||
|
# 2026-08-24 image) shipped the unit `disabled; preset: disabled`, so an
|
||||||
|
# upgrade must enable it or those installs never extract anything. Then
|
||||||
|
# extract immediately rather than at the next boot (--if-device makes that
|
||||||
|
# a quiet no-op inside build/CI chroots). A card that already hard-failed
|
||||||
|
# this boot recovers playback via the manifest's rebind lines; capture
|
||||||
|
# needs the next boot (journal/blobs/ 2026-08-29).
|
||||||
|
systemctl preset fp6-vendor-blobs.service 2>/dev/null || :
|
||||||
|
exec /usr/lib/fp6-vendor-blobs/extract --if-device
|
||||||
7
aports/device/fp6-vendor-blobs/fp6-vendor-blobs.preset
Normal file
7
aports/device/fp6-vendor-blobs/fp6-vendor-blobs.preset
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# The package also ships the multi-user.target.wants symlink, but a
|
||||||
|
# `systemctl preset-all` (pmbootstrap runs one while building the image)
|
||||||
|
# strips manually-installed .wants links for any unit whose preset does not
|
||||||
|
# enable it -- which left the service `disabled; preset: disabled` in the
|
||||||
|
# 2026-08-24 image and so no aw88261_acf.bin and no sound card on a fresh
|
||||||
|
# install. This preset is what makes the enable survive that.
|
||||||
|
enable fp6-vendor-blobs.service
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Extract vendor blobs from the stock Android partitions
|
Description=Extract vendor blobs from the stock Android partitions
|
||||||
# The dynamic-partition mappings come from the initramfs and the dests live
|
# Must run BEFORE udev coldplug loads the consuming drivers: a sound card
|
||||||
# on the rootfs, so local-fs ordering is enough. Deliberately not ordered
|
# that probes without its firmware hard-fails (-2, never retried), and while
|
||||||
# against driver/audio bringup: the extractor re-probes consumers itself
|
# a later re-probe of the machine driver re-forms the card, that late-formed
|
||||||
# (manifest rebind lines), so it never has to win a race.
|
# card has a dead capture path (wcd9378 TX sequencer refuses PS0 - fp6 repo
|
||||||
After=local-fs.target
|
# journal/blobs/ 2026-08-29). Only extraction-before-probe yields a fully
|
||||||
|
# working card, so this runs pre-coldplug; the manifest rebind lines remain
|
||||||
|
# as the fallback for consumers that probed anyway (playback recovers same
|
||||||
|
# boot, capture needs the next boot) and for on-device re-extraction after a
|
||||||
|
# package upgrade.
|
||||||
|
# The dynamic-partition mappings come from the initramfs (device nodes exist
|
||||||
|
# pre-udev) and the dests live on the rootfs, so remount-fs ordering is
|
||||||
|
# enough. multi-user is a deliberate second Wants: if the early run failed
|
||||||
|
# (e.g. no mappings pre-udev), the multi-user start job retries it after
|
||||||
|
# udev, where the by-partlabel/make-dynpart-mappings fallbacks work.
|
||||||
|
DefaultDependencies=no
|
||||||
|
After=systemd-remount-fs.service
|
||||||
|
Before=systemd-udev-trigger.service sysinit.target shutdown.target
|
||||||
|
Conflicts=shutdown.target
|
||||||
|
RequiresMountsFor=/usr/lib/firmware
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
|
@ -12,4 +26,4 @@ RemainAfterExit=yes
|
||||||
ExecStart=/usr/lib/fp6-vendor-blobs/extract
|
ExecStart=/usr/lib/fp6-vendor-blobs/extract
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=sysinit.target multi-user.target
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ _flavor="postmarketos-qcom-milos"
|
||||||
pkgname=linux-$_flavor
|
pkgname=linux-$_flavor
|
||||||
pkgver=7.2.0
|
pkgver=7.2.0
|
||||||
# always sorts above the upstream aport (r0..r99)
|
# always sorts above the upstream aport (r0..r99)
|
||||||
pkgrel=100
|
pkgrel=102
|
||||||
pkgdesc="Milos mainline kernel + Catcrafts FP6 bring-up carries (combined-stable)"
|
pkgdesc="Milos mainline kernel + Catcrafts FP6 bring-up carries (combined-stable)"
|
||||||
arch="aarch64"
|
arch="aarch64"
|
||||||
_carch="arm64"
|
_carch="arm64"
|
||||||
|
|
@ -57,7 +57,14 @@ prepare() {
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
unset LDFLAGS
|
unset LDFLAGS
|
||||||
|
# Build for the cores this image runs on: 4x Cortex-A520 + 4x Cortex-A720.
|
||||||
|
# The stock arm64 build gives the compiler no arch flag so one image boots
|
||||||
|
# any Armv8 board; this one boots the FP6. armv8.6-a is the highest level
|
||||||
|
# whose mandatory set the phone exposes (8.7 would assume WFxT, 9.x SVE2;
|
||||||
|
# neither is in its hwcaps); +fp16fml+aes+sha3+sm4 are the optional
|
||||||
|
# extensions it has, the same set `gcc -march=native` derives on the phone.
|
||||||
make ARCH="$_carch" LLVM=1 \
|
make ARCH="$_carch" LLVM=1 \
|
||||||
|
KCFLAGS="-march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720" \
|
||||||
KBUILD_BUILD_VERSION="$((pkgrel + 1 ))-$_flavor"
|
KBUILD_BUILD_VERSION="$((pkgrel + 1 ))-$_flavor"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7224,7 +7224,7 @@ CONFIG_TEE_DMABUF_HEAPS=y
|
||||||
CONFIG_OPTEE=y
|
CONFIG_OPTEE=y
|
||||||
# CONFIG_OPTEE_INSECURE_LOAD_IMAGE is not set
|
# CONFIG_OPTEE_INSECURE_LOAD_IMAGE is not set
|
||||||
CONFIG_OPTEE_STATIC_PROTMEM_POOL=y
|
CONFIG_OPTEE_STATIC_PROTMEM_POOL=y
|
||||||
# CONFIG_QCOMTEE is not set
|
CONFIG_QCOMTEE=m
|
||||||
# CONFIG_MUX_CORE is not set
|
# CONFIG_MUX_CORE is not set
|
||||||
CONFIG_PM_OPP=y
|
CONFIG_PM_OPP=y
|
||||||
# CONFIG_SIOX is not set
|
# CONFIG_SIOX is not set
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
maintainer="Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"
|
maintainer="Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"
|
||||||
pkgname=soc-fairphone-fp6-audio
|
pkgname=soc-fairphone-fp6-audio
|
||||||
pkgver=4
|
pkgver=4
|
||||||
pkgrel=0
|
pkgrel=1
|
||||||
pkgdesc="Fairphone 6 audio topology and UCM profile"
|
pkgdesc="Fairphone 6 audio topology and UCM profile"
|
||||||
url="https://forgejo.catcrafts.net/Catcrafts/audioreach-topology"
|
url="https://forgejo.catcrafts.net/Catcrafts/audioreach-topology"
|
||||||
arch="noarch"
|
arch="noarch"
|
||||||
|
|
@ -47,7 +47,7 @@ package() {
|
||||||
|
|
||||||
sha512sums="
|
sha512sums="
|
||||||
51071c574b9f5899c78ba9fe1b6f3bbda2195e6d274014a05869ae56ed0ff18e15678bd7d78cd68846900d3c579556328e229842152cffd943186d43344ed33c fp6-tplg.bin
|
51071c574b9f5899c78ba9fe1b6f3bbda2195e6d274014a05869ae56ed0ff18e15678bd7d78cd68846900d3c579556328e229842152cffd943186d43344ed33c fp6-tplg.bin
|
||||||
bdfe413494b6815cf96843d83c6c9e8deb757b083d7577c93eab460de49d1cd00b8615c115c25d71f6488de4e65f04d5f02bca816b2c31747880a22613e547a3 aw88261-acf.manifest
|
0240a36e6fae76f6df576c5ffc93241f6d59e03db429811d61c746561743ddb4690cb925028546a6c3d69dcc867aead5ddf1c85b12a0065195ea9c6621fa847f aw88261-acf.manifest
|
||||||
3bb268f9fcb8d1c0b42741b67da42b6361bab259e7211d9930919995ebe7dcf67826e6d4f00ff5e4fe5b52067fe90d00212158c3b4879d1bee481e87f0623206 ucm-milos-fp6.conf
|
3bb268f9fcb8d1c0b42741b67da42b6361bab259e7211d9930919995ebe7dcf67826e6d4f00ff5e4fe5b52067fe90d00212158c3b4879d1bee481e87f0623206 ucm-milos-fp6.conf
|
||||||
2a189c3141fedc43f8eb19f4df1e43487223d524e27da3f948556d224cca1f493f6815ada191953fb79a19d5c5abadb21de7b3ece7b74b0929338c216e21e84a HiFi.conf
|
2a189c3141fedc43f8eb19f4df1e43487223d524e27da3f948556d224cca1f493f6815ada191953fb79a19d5c5abadb21de7b3ece7b74b0929338c216e21e84a HiFi.conf
|
||||||
ec63fa5363738a43c9eee4b1be70201e2d2076ec817031fda087e61d781e81dc67351be44faf8b056a2939875a139cd909f7c311115bb1150e17ad4493846606 50-echo-cancel.conf
|
ec63fa5363738a43c9eee4b1be70201e2d2076ec817031fda087e61d781e81dc67351be44faf8b056a2939875a139cd909f7c311115bb1150e17ad4493846606 50-echo-cancel.conf
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,14 @@
|
||||||
# journal/blobs/, 2026-08-24). Slot A first, slot B as fallback; the copy
|
# journal/blobs/, 2026-08-24). Slot A first, slot B as fallback; the copy
|
||||||
# must hash-match the blob the audio stack was tested with.
|
# must hash-match the blob the audio stack was tested with.
|
||||||
file vendor_a,vendor_b firmware/aw882xx_acf.bin /usr/lib/firmware/qcom/milos/fairphone/fp6/aw88261_acf.bin af723973655ba5901948d4a22212323e39ce53ff0156021185720af4af6ab0b3
|
file vendor_a,vendor_b firmware/aw882xx_acf.bin /usr/lib/firmware/qcom/milos/fairphone/fp6/aw88261_acf.bin af723973655ba5901948d4a22212323e39ce53ff0156021185720af4af6ab0b3
|
||||||
# the amps bind on i2c probe but request the ACF only at card init, which
|
# Rebinds are the FALLBACK for a card that probed before extraction (the
|
||||||
# races the extractor on first boot and is never retried on failure - a
|
# service normally runs pre-coldplug, so they no-op). The amps bind on i2c
|
||||||
# forced fresh probe of both amps makes audio work that same boot either way
|
# probe but request the ACF only at card init, never retried on failure; and
|
||||||
|
# when that failed card init already took the machine driver down (-2, not a
|
||||||
|
# defer), re-probing the amps alone cannot re-form the card - the platform
|
||||||
|
# device needs a fresh probe too. Note the recovered card plays back fine
|
||||||
|
# but its capture path is dead until the next boot (wcd9378 TX sequencer
|
||||||
|
# refuses PS0 on a late-formed card - journal/blobs/ 2026-08-29).
|
||||||
rebind i2c 6-0034
|
rebind i2c 6-0034
|
||||||
rebind i2c 6-0035
|
rebind i2c 6-0035
|
||||||
|
rebind platform sound
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
enable imsd.service
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
maintainer="Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>"
|
|
||||||
pkgname=imsd
|
|
||||||
pkgver=0.3.0
|
|
||||||
pkgrel=0
|
|
||||||
pkgdesc="Userspace IMS/VoLTE daemon for mainline Linux phones"
|
|
||||||
url="https://forgejo.catcrafts.net/Catcrafts/imsd"
|
|
||||||
# other arches: nothing wrong known, just never run there
|
|
||||||
arch="aarch64 x86_64"
|
|
||||||
license="GPL-3.0-only"
|
|
||||||
# the media leg dlopen's the AMR-WB codecs; pw-record/pw-play drive PipeWire —
|
|
||||||
# none of which abuild's .so auto-scan can see
|
|
||||||
depends="modemmanager opencore-amr vo-amrwbenc pipewire-tools"
|
|
||||||
# clang/libc++ C++26-modules build (upstream Makefile); llvm-runtimes ships
|
|
||||||
# the libc++ std module sources the build precompiles
|
|
||||||
makedepends="clang lld libc++-dev llvm-libunwind-dev llvm-runtimes glib-dev pkgconf"
|
|
||||||
# the versioned provides both satisfies soc-qcom-modem's 81voltd dependency
|
|
||||||
# and excludes the real package: 81voltd serves the modem firmware's own
|
|
||||||
# ims-PDN requests, which races imsd for the PDN and flaps it (a new prefix
|
|
||||||
# every ~2.5 min) — two IMS stacks cannot share one PDN. Installing imsd is
|
|
||||||
# an explicit choice to hand the IMS PDN to userspace.
|
|
||||||
provides="81voltd=$pkgver-r$pkgrel"
|
|
||||||
# no OpenRC service yet: the unit's PDN-bring-up/env-file sequencing is only
|
|
||||||
# tested under systemd; an initd is welcome once someone can verify one
|
|
||||||
subpackages="$pkgname-systemd"
|
|
||||||
# Pinned by CI (see build.sh at the repository root): the Forgejo instance
|
|
||||||
# serves no source archives, so the tarball is generated with git-archive
|
|
||||||
# (prefix imsd/) and placed next to this APKBUILD. The skel override hides
|
|
||||||
# kde-telephony's modem daemon autostart for the account created at install —
|
|
||||||
# imsd-dialerd owns those session D-Bus names instead.
|
|
||||||
_commit="REPLACED_BY_CI"
|
|
||||||
source="
|
|
||||||
imsd-$_commit.tar.gz
|
|
||||||
org.kde.modem.daemon.desktop
|
|
||||||
80-imsd.preset
|
|
||||||
"
|
|
||||||
builddir="$srcdir/$pkgname"
|
|
||||||
|
|
||||||
build() {
|
|
||||||
make
|
|
||||||
}
|
|
||||||
|
|
||||||
check() {
|
|
||||||
make check
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
make install DESTDIR="$pkgdir"
|
|
||||||
install -Dm644 "$srcdir"/org.kde.modem.daemon.desktop \
|
|
||||||
"$pkgdir"/etc/skel/.config/autostart/org.kde.modem.daemon.desktop
|
|
||||||
# enabled by preset: the unit is a no-op until /etc/imsd.env exists, and
|
|
||||||
# VoLTE surviving reboots must not depend on a manual systemctl enable
|
|
||||||
install -Dm644 "$srcdir"/80-imsd.preset \
|
|
||||||
"$pkgdir"/usr/lib/systemd/system-preset/80-imsd.preset
|
|
||||||
mkdir -p "$pkgdir"/etc/systemd/system/multi-user.target.wants
|
|
||||||
ln -s /usr/lib/systemd/system/imsd.service \
|
|
||||||
"$pkgdir"/etc/systemd/system/multi-user.target.wants/imsd.service
|
|
||||||
# ...but only actually start once the carrier config exists, so
|
|
||||||
# unconfigured systems don't boot into a failing unit
|
|
||||||
mkdir -p "$pkgdir"/usr/lib/systemd/system/imsd.service.d
|
|
||||||
printf '[Unit]\nConditionPathExists=/etc/imsd.env\n' \
|
|
||||||
> "$pkgdir"/usr/lib/systemd/system/imsd.service.d/10-require-config.conf
|
|
||||||
}
|
|
||||||
|
|
||||||
systemd() {
|
|
||||||
install_if="$pkgname=$pkgver-r$pkgrel systemd"
|
|
||||||
|
|
||||||
amove usr/lib/systemd/system
|
|
||||||
}
|
|
||||||
|
|
||||||
sha512sums="
|
|
||||||
REPLACED_BY_CI imsd-REPLACED_BY_CI.tar.gz
|
|
||||||
REPLACED_BY_CI org.kde.modem.daemon.desktop
|
|
||||||
REPLACED_BY_CI 80-imsd.preset
|
|
||||||
"
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[Desktop Entry]
|
|
||||||
Hidden=true
|
|
||||||
|
|
@ -3,7 +3,7 @@ maintainer="Robert Mader <robert.mader@collabora.com>"
|
||||||
pkgname=libcamera
|
pkgname=libcamera
|
||||||
_pkgver=0.7.2
|
_pkgver=0.7.2
|
||||||
pkgver=9999$_pkgver
|
pkgver=9999$_pkgver
|
||||||
pkgrel=9
|
pkgrel=10
|
||||||
pkgdesc="Linux camera framework"
|
pkgdesc="Linux camera framework"
|
||||||
url="https://libcamera.org/"
|
url="https://libcamera.org/"
|
||||||
arch="all"
|
arch="all"
|
||||||
|
|
@ -83,6 +83,14 @@ builddir="$srcdir/$pkgname-v$_pkgver"
|
||||||
# gstreamer tests fail
|
# gstreamer tests fail
|
||||||
# manual strip because ipa .sign files depend on the file contents- have to re-sign after strip
|
# manual strip because ipa .sign files depend on the file contents- have to re-sign after strip
|
||||||
options="!strip !check"
|
options="!strip !check"
|
||||||
|
# fp6-img: this package only ever runs on the FP6 (4x Cortex-A520 + 4x
|
||||||
|
# Cortex-A720), so build for it. armv8.6-a is the highest level whose
|
||||||
|
# mandatory set the phone exposes (8.7 would assume WFxT, 9.x SVE2; neither
|
||||||
|
# is in its hwcaps); +fp16fml+aes+sha3+sm4 are the optional extensions it
|
||||||
|
# has, the same set `gcc -march=native` derives on the phone. One spelling
|
||||||
|
# for gcc and clang, and the same string the kernel aport uses.
|
||||||
|
export CFLAGS="$CFLAGS -march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720"
|
||||||
|
export CXXFLAGS="$CXXFLAGS -march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720"
|
||||||
|
|
||||||
case "$CARCH" in
|
case "$CARCH" in
|
||||||
arm*|aarch64)
|
arm*|aarch64)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ maintainer="Achill Gilgenast <achill@achill.org>"
|
||||||
pkgname=libqmi
|
pkgname=libqmi
|
||||||
pkgver=1.39.1_git20260808
|
pkgver=1.39.1_git20260808
|
||||||
_commit=30f3e998e6cbda364ac1bc73223de20561e6d555
|
_commit=30f3e998e6cbda364ac1bc73223de20561e6d555
|
||||||
pkgrel=100
|
pkgrel=101
|
||||||
pkgdesc="QMI modem protocol helper library"
|
pkgdesc="QMI modem protocol helper library"
|
||||||
url="https://www.freedesktop.org/wiki/Software/libqmi"
|
url="https://www.freedesktop.org/wiki/Software/libqmi"
|
||||||
arch="all"
|
arch="all"
|
||||||
|
|
@ -27,6 +27,13 @@ makedepends="
|
||||||
# fp6-img: crossdirect breaks meson compiler introspection (cc1 spawn
|
# fp6-img: crossdirect breaks meson compiler introspection (cc1 spawn
|
||||||
# failure), same as modemmanager; build under plain qemu.
|
# failure), same as modemmanager; build under plain qemu.
|
||||||
options="!pmb:crossdirect"
|
options="!pmb:crossdirect"
|
||||||
|
# fp6-img: this package only ever runs on the FP6 (4x Cortex-A520 + 4x
|
||||||
|
# Cortex-A720), so build for it. armv8.6-a is the highest level whose
|
||||||
|
# mandatory set the phone exposes (8.7 would assume WFxT, 9.x SVE2; neither
|
||||||
|
# is in its hwcaps); +fp16fml+aes+sha3+sm4 are the optional extensions it
|
||||||
|
# has, the same set `gcc -march=native` derives on the phone. One spelling
|
||||||
|
# for gcc and clang, and the same string the kernel aport uses.
|
||||||
|
export CFLAGS="$CFLAGS -march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720"
|
||||||
subpackages="
|
subpackages="
|
||||||
$pkgname-dev
|
$pkgname-dev
|
||||||
$pkgname-doc
|
$pkgname-doc
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
maintainer="Achill Gilgenast <achill@achill.org>"
|
maintainer="Achill Gilgenast <achill@achill.org>"
|
||||||
pkgname=modemmanager
|
pkgname=modemmanager
|
||||||
pkgver=1.25.95_git20260709
|
pkgver=1.25.95_git20260709
|
||||||
pkgrel=100
|
pkgrel=101
|
||||||
_commit=d776ea38d29ca472a12323c1d45002ee19a66f57
|
_commit=d776ea38d29ca472a12323c1d45002ee19a66f57
|
||||||
pkgdesc="ModemManager library"
|
pkgdesc="ModemManager library"
|
||||||
url="https://www.freedesktop.org/wiki/Software/ModemManager"
|
url="https://www.freedesktop.org/wiki/Software/ModemManager"
|
||||||
|
|
@ -42,6 +42,13 @@ checkdepends="glib-dev py3-gobject3 py3-dbus"
|
||||||
# native builders, and none exercise the GNSS code our patches change) -
|
# native builders, and none exercise the GNSS code our patches change) -
|
||||||
# on-device validation is the real test.
|
# on-device validation is the real test.
|
||||||
options="!pmb:crossdirect !check"
|
options="!pmb:crossdirect !check"
|
||||||
|
# fp6-img: this package only ever runs on the FP6 (4x Cortex-A520 + 4x
|
||||||
|
# Cortex-A720), so build for it. armv8.6-a is the highest level whose
|
||||||
|
# mandatory set the phone exposes (8.7 would assume WFxT, 9.x SVE2; neither
|
||||||
|
# is in its hwcaps); +fp16fml+aes+sha3+sm4 are the optional extensions it
|
||||||
|
# has, the same set `gcc -march=native` derives on the phone. One spelling
|
||||||
|
# for gcc and clang, and the same string the kernel aport uses.
|
||||||
|
export CFLAGS="$CFLAGS -march=armv8.6-a+fp16fml+aes+sha3+sm4 -mtune=cortex-a720"
|
||||||
subpackages="
|
subpackages="
|
||||||
$pkgname-lang
|
$pkgname-lang
|
||||||
$pkgname-doc
|
$pkgname-doc
|
||||||
|
|
|
||||||
108
build.sh
108
build.sh
|
|
@ -1,7 +1,8 @@
|
||||||
#!/bin/sh -eu
|
#!/bin/sh -eu
|
||||||
# fp6-img pipeline: build a flashable postmarketOS image for the Fairphone 6
|
# fp6-img pipeline: build a flashable postmarketOS image for the Fairphone 6
|
||||||
# with the Catcrafts kernel (milos-linux combined-stable) and, once its tag
|
# with the Catcrafts kernel (milos-linux combined-stable), imsd (VoLTE) and
|
||||||
# is published, imsd (VoLTE).
|
# fingerprintd (fingerprint unlock)
|
||||||
|
# installed from the Catcrafts apk registry.
|
||||||
#
|
#
|
||||||
# Runs in CI inside an Alpine container on the privileged "pmos" runner
|
# Runs in CI inside an Alpine container on the privileged "pmos" runner
|
||||||
# (pmbootstrap needs loop devices; the aarch64 chroots need the qemu-user
|
# (pmbootstrap needs loop devices; the aarch64 chroots need the qemu-user
|
||||||
|
|
@ -17,13 +18,26 @@ set -eu
|
||||||
|
|
||||||
KERNEL_REPO=https://forgejo.catcrafts.net/Catcrafts/milos-linux.git
|
KERNEL_REPO=https://forgejo.catcrafts.net/Catcrafts/milos-linux.git
|
||||||
KERNEL_BRANCH=combined-stable
|
KERNEL_BRANCH=combined-stable
|
||||||
IMSD_REPO=https://forgejo.catcrafts.net/Catcrafts/imsd.git
|
# imsd and fingerprintd are not built here: each repo's package CI publishes
|
||||||
# imsd 0.3.0: what the dev phone runs (0.2.7) + emergency-calling stage 1 +
|
# its apk to the registry, and the image installs the NEWEST published
|
||||||
# the make build path the aport needs. Bump deliberately, not via tip-chasing.
|
# version (section 3b), so a fresh install carries the same binary every
|
||||||
IMSD_COMMIT=1037958
|
# installed phone gets from 'apk upgrade', and a release of either needs no
|
||||||
|
# commit here. Integrity comes from the same place as on the phones: the
|
||||||
|
# registry index is verified against the key catcrafts-fp6-repo ships
|
||||||
|
# (aports/device/, the phones' /etc/apk/keys) and each apk against that
|
||||||
|
# index, by registry-fetch.py. The resolved versions and sha256s are in the
|
||||||
|
# build summary. Until 2026-09-11 both were pinned here by version and
|
||||||
|
# sha256; that gated fresh installs only, never upgrades, and cost a commit
|
||||||
|
# and an image run per release.
|
||||||
|
REGISTRY=https://forgejo.catcrafts.net/api/packages/Catcrafts/alpine/edge/fp6
|
||||||
PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git
|
PMAPORTS_REPO=https://gitlab.postmarketos.org/postmarketOS/pmaports.git
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
# Fail in seconds, not at minute 57: a stale sha512sum in one of our own
|
||||||
|
# aports (run 52, fp6-vendor-blobs 1-r2) only surfaces when abuild reaches that
|
||||||
|
# aport, an hour into the run. This checks every aport's local source files
|
||||||
|
# against the committed sums before pmbootstrap does anything.
|
||||||
|
./check-aports.sh
|
||||||
|
|
||||||
# pmbootstrap refuses to run as root: install deps, then re-exec as a build
|
# pmbootstrap refuses to run as root: install deps, then re-exec as a build
|
||||||
# user with passwordless sudo (pmbootstrap escalates itself where needed).
|
# user with passwordless sudo (pmbootstrap escalates itself where needed).
|
||||||
|
|
@ -35,7 +49,7 @@ if [ "$(id -u)" = 0 ]; then
|
||||||
# multipath-tools: kpartx; util-linux: losetup with --json support
|
# multipath-tools: kpartx; util-linux: losetup with --json support
|
||||||
# (pmbootstrap's host-tool checks + image mounting need both)
|
# (pmbootstrap's host-tool checks + image mounting need both)
|
||||||
apk add -q git sudo openssl python3 py3-pip multipath-tools util-linux \
|
apk add -q git sudo openssl python3 py3-pip multipath-tools util-linux \
|
||||||
tar xz
|
tar xz curl
|
||||||
# the pmOS gitlab hiccups under crawler load and truncates clones
|
# the pmOS gitlab hiccups under crawler load and truncates clones
|
||||||
# ("early EOF"); that should cost a retry, not the run — same reasoning
|
# ("early EOF"); that should cost a retry, not the run — same reasoning
|
||||||
# as clone_retry below, which isn't defined yet in this root branch
|
# as clone_retry below, which isn't defined yet in this root branch
|
||||||
|
|
@ -135,7 +149,7 @@ cp -r aports/device/fp6-device-tweaks "$WORK/pmaports/device/"
|
||||||
cp -r aports/device/fp6-charging-mode "$WORK/pmaports/device/"
|
cp -r aports/device/fp6-charging-mode "$WORK/pmaports/device/"
|
||||||
cp -r aports/device/catcrafts-fp6-repo "$WORK/pmaports/device/"
|
cp -r aports/device/catcrafts-fp6-repo "$WORK/pmaports/device/"
|
||||||
cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/"
|
cp -r aports/main/postmarketos-config-nftables "$WORK/pmaports/main/"
|
||||||
cp -r aports/modem/imsd "$WORK/pmaports/modem/"
|
# imsd has no aport here at all: section 3b installs the published apk.
|
||||||
# Alpine forks carrying the GNSS patches (libqmi !470 unreleased; MM !1463
|
# Alpine forks carrying the GNSS patches (libqmi !470 unreleased; MM !1463
|
||||||
# draft) - deps of modemmanager/imsd, built from aports because r100 > repo.
|
# draft) - deps of modemmanager/imsd, built from aports because r100 > repo.
|
||||||
cp -r aports/temp/libqmi "$WORK/pmaports/temp/"
|
cp -r aports/temp/libqmi "$WORK/pmaports/temp/"
|
||||||
|
|
@ -161,16 +175,7 @@ sed -i "s/^_commit=.*/_commit=\"$COMMIT\"/" "$KAPORT/APKBUILD"
|
||||||
KDATE=$(git -C "$WORK/milos-src" log -1 --format=%cd --date=format:%Y%m%d)
|
KDATE=$(git -C "$WORK/milos-src" log -1 --format=%cd --date=format:%Y%m%d)
|
||||||
sed -i "s/^pkgver=\([0-9.]*\)\$/pkgver=\1_git$KDATE/" "$KAPORT/APKBUILD"
|
sed -i "s/^pkgver=\([0-9.]*\)\$/pkgver=\1_git$KDATE/" "$KAPORT/APKBUILD"
|
||||||
|
|
||||||
# Same dance for imsd, pinned to a reviewed commit rather than branch tip.
|
# Same dance for the gitlab.freedesktop.org packages (libqmi, modemmanager,
|
||||||
|
|
||||||
clone_retry "$WORK/imsd-src" -q "$IMSD_REPO"
|
|
||||||
git -C "$WORK/imsd-src" checkout -q "$IMSD_COMMIT"
|
|
||||||
IAPORT="$WORK/pmaports/modem/imsd"
|
|
||||||
git -C "$WORK/imsd-src" archive --prefix=imsd/ \
|
|
||||||
-o "$IAPORT/imsd-$IMSD_COMMIT.tar.gz" HEAD
|
|
||||||
sed -i "s/^_commit=.*/_commit=\"$IMSD_COMMIT\"/" "$IAPORT/APKBUILD"
|
|
||||||
|
|
||||||
# Same again for the gitlab.freedesktop.org packages (libqmi, modemmanager,
|
|
||||||
# libcamera): their pinned tarballs came from fd.o's on-demand archive
|
# libcamera): their pinned tarballs came from fd.o's on-demand archive
|
||||||
# endpoint, which 503/504s for hours at a stretch — runs #25, #27, #30 and
|
# endpoint, which 503/504s for hours at a stretch — runs #25, #27, #30 and
|
||||||
# #31 all died there, outlasting any in-run retry. git clone is served from
|
# #31 all died there, outlasting any in-run retry. git clone is served from
|
||||||
|
|
@ -215,15 +220,14 @@ aports = $WORK/pmaports
|
||||||
device = fairphone-fp6
|
device = fairphone-fp6
|
||||||
ui = plasma-mobile
|
ui = plasma-mobile
|
||||||
systemd = always
|
systemd = always
|
||||||
extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel
|
extra_packages = soc-fairphone-fp6-audio,callaudioshim,imsd,fingerprintd,fingerprintd-systemd,fingerprintd-agent,fp6-device-tweaks,fp6-charging-mode,catcrafts-fp6-repo,postmarketos-base-ui-audio-backend-pipewire,pipewire-pulse,pipewire-echo-cancel
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# All five source tarballs are generated locally above, so every checksum
|
# All four source tarballs are generated locally above, so every checksum
|
||||||
# step is offline. libcamera needs a checksum step now too: its committed
|
# step is offline. libcamera needs a checksum step now too: its committed
|
||||||
# sums were for the fd.o-served tarball, and git-archive output is not
|
# sums were for the fd.o-served tarball, and git-archive output is not
|
||||||
# byte-identical to it (verified: sha512 differs).
|
# byte-identical to it (verified: sha512 differs).
|
||||||
pmbootstrap checksum linux-postmarketos-qcom-milos
|
pmbootstrap checksum linux-postmarketos-qcom-milos
|
||||||
pmbootstrap checksum imsd
|
|
||||||
pmbootstrap checksum libqmi
|
pmbootstrap checksum libqmi
|
||||||
pmbootstrap checksum modemmanager
|
pmbootstrap checksum modemmanager
|
||||||
pmbootstrap checksum libcamera
|
pmbootstrap checksum libcamera
|
||||||
|
|
@ -245,12 +249,60 @@ retry "build modemmanager" pmbootstrap $NOCROSS build --arch aarch64 modemmanage
|
||||||
# patched -r2 exists for the publish step even if the install set resolves
|
# patched -r2 exists for the publish step even if the install set resolves
|
||||||
# it before the overlay is considered.
|
# it before the overlay is considered.
|
||||||
retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera
|
retry "build libcamera" pmbootstrap $NOCROSS build --arch aarch64 libcamera
|
||||||
# imsd is the one crossdirect build left to the install phase, which has no
|
# --- 3b. imsd + fingerprintd: the published apks, not local builds -----------
|
||||||
# per-package flag — pre-build it here in no-crossdirect mode so install
|
# Each repo's package CI is the only producer of its apk; the image installs
|
||||||
# finds it current.
|
# the newest registry package, the one users get via 'apk upgrade'.
|
||||||
if [ -n "$NOCROSS" ]; then
|
# pmbootstrap has no knob for a third-party repository, and after the main
|
||||||
retry "build imsd" pmbootstrap $NOCROSS build --arch aarch64 imsd
|
# 'apk add' it re-adds every package found in its local packages dir BY FILE
|
||||||
|
# PATH — which makes apk verify the package's own signature, and registry
|
||||||
|
# packages are signed with per-run keys nobody keeps (phones trust the
|
||||||
|
# registry-signed index instead). So: fetch the newest of each, verified the
|
||||||
|
# way a phone verifies them (registry-fetch.py: index signature against the
|
||||||
|
# shipped key, control checksum and data hash against the index), re-sign
|
||||||
|
# the envelope with this run's abuild key (control and data streams stay
|
||||||
|
# byte-identical, so the identity checksum equals the registry's), drop into
|
||||||
|
# the local packages dir, re-index. The abuild key exists because the builds
|
||||||
|
# above initialized the buildroot. Each group's first name decides the
|
||||||
|
# version; its subpackages must exist at that same version.
|
||||||
|
REG_DL="$WORK/registry-apks"
|
||||||
|
rm -rf "$REG_DL"
|
||||||
|
mkdir -p "$REG_DL"
|
||||||
|
REGISTRY_KEY=$(echo aports/device/catcrafts-fp6-repo/*.rsa.pub)
|
||||||
|
if [ ! -f "$REGISTRY_KEY" ]; then
|
||||||
|
echo "expected exactly one registry key in aports/device/catcrafts-fp6-repo" >&2
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
retry "fetch registry packages" python3 ./registry-fetch.py "$REGISTRY" "$REGISTRY_KEY" "$REG_DL" \
|
||||||
|
imsd,imsd-systemd fingerprintd,fingerprintd-systemd,fingerprintd-agent
|
||||||
|
ABUILD_KEY=$(echo "$WORKDIR"/config_abuild/*.rsa)
|
||||||
|
if [ ! -f "$ABUILD_KEY" ]; then
|
||||||
|
echo "expected exactly one abuild key in $WORKDIR/config_abuild" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# abuild-keygen ran inside the chroot as pmbootstrap's user (uid 12345), so
|
||||||
|
# the key is 0600 to that uid and unreadable here (run 49 died on exactly
|
||||||
|
# this); sign from a private copy taken via sudo, then drop it.
|
||||||
|
KEYCOPY="$REG_DL/abuild-key.rsa"
|
||||||
|
sudo install -m 0600 -o "$(id -un)" "$ABUILD_KEY" "$KEYCOPY"
|
||||||
|
for _f in "$REG_DL"/*.apk; do
|
||||||
|
python3 ./apk-resign.py "$_f" "$KEYCOPY" "$(basename "$ABUILD_KEY").pub"
|
||||||
|
done
|
||||||
|
rm -f "$KEYCOPY"
|
||||||
|
# The packages dir belongs to pmbootstrap's chroot user as well (abuild wrote
|
||||||
|
# it from inside the chroot), so the build user cannot write there either
|
||||||
|
# (run 50): install the files with the directory's own owner, as abuild would
|
||||||
|
# have left them. The dir exists because the builds above populated it.
|
||||||
|
PKGDIR="$WORKDIR/packages/edge/aarch64"
|
||||||
|
if [ ! -d "$PKGDIR" ]; then
|
||||||
|
echo "$PKGDIR missing - the package builds above should have created it" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for _f in "$REG_DL"/*.apk; do
|
||||||
|
sudo install -m 0644 -o "$(stat -c %u "$PKGDIR")" -g "$(stat -c %g "$PKGDIR")" \
|
||||||
|
"$_f" "$PKGDIR/$(basename "$_f")"
|
||||||
|
done
|
||||||
|
rm -f "$REG_DL"/*.apk
|
||||||
|
pmbootstrap index
|
||||||
|
|
||||||
# --- 4. build the image -------------------------------------------------------
|
# --- 4. build the image -------------------------------------------------------
|
||||||
# Same default credentials as the official postmarketOS images.
|
# Same default credentials as the official postmarketOS images.
|
||||||
|
|
@ -281,7 +333,9 @@ cp README.md install.sh "$STAGE/fp6-img/"
|
||||||
echo "kernel: $KERNEL_REPO $KERNEL_BRANCH @ $COMMIT"
|
echo "kernel: $KERNEL_REPO $KERNEL_BRANCH @ $COMMIT"
|
||||||
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
echo "built: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
echo "default login: user / 147147 (same as official postmarketOS images)"
|
echo "default login: user / 147147 (same as official postmarketOS images)"
|
||||||
echo "imsd: $IMSD_REPO @ $IMSD_COMMIT (0.3.0)"
|
while read -r _n _v _s; do
|
||||||
|
printf '%-14s %s %s-%s (newest registry package, sha256 %s)\n' "$_n:" "$REGISTRY" "$_n" "$_v" "$_s"
|
||||||
|
done < "$REG_DL/manifest"
|
||||||
} > "$STAGE/fp6-img/build-info.txt"
|
} > "$STAGE/fp6-img/build-info.txt"
|
||||||
# sums of the extracted contents
|
# sums of the extracted contents
|
||||||
(cd "$STAGE/fp6-img" && sha256sum -- * > sha256sums.txt)
|
(cd "$STAGE/fp6-img" && sha256sum -- * > sha256sums.txt)
|
||||||
|
|
|
||||||
55
check-aports.sh
Executable file
55
check-aports.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
||||||
|
#!/bin/sh -eu
|
||||||
|
# check-aports.sh - verify the committed sha512sums of every aport's LOCAL
|
||||||
|
# source files (scripts, units, configs, patches) against the files actually
|
||||||
|
# in the aport directory.
|
||||||
|
#
|
||||||
|
# Why this exists: CI run 52 (2026-09-05) died after 57 minutes, at the point
|
||||||
|
# abuild reached fp6-vendor-blobs, because the extractor had been changed and
|
||||||
|
# its sha512sum had not. abuild finds that only when it gets to that aport;
|
||||||
|
# this finds it in under a second, before pmbootstrap does anything. build.sh
|
||||||
|
# runs it first; run it by hand before pushing too.
|
||||||
|
#
|
||||||
|
# Out of scope, by design: sources fetched from a URL (abuild verifies those
|
||||||
|
# against the same sums after fetching), and the aports whose sums build.sh
|
||||||
|
# regenerates at build time with 'pmbootstrap checksum' (their tarballs are
|
||||||
|
# generated there and do not exist here) - that list is read from build.sh so
|
||||||
|
# the two cannot drift apart.
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
regen=$(sed -n 's/^pmbootstrap checksum \([a-z0-9-]*\)$/\1/p' build.sh | tr '\n' ' ')
|
||||||
|
rc=0
|
||||||
|
for d in aports/*/*/; do
|
||||||
|
[ -f "$d/APKBUILD" ] || continue
|
||||||
|
d=${d%/}
|
||||||
|
case " $regen " in *" ${d##*/} "*) continue ;; esac
|
||||||
|
info=$(cd "$d" && sh -c '. ./APKBUILD; printf "%s\n" $source; printf "==\n"; printf "%s\n" "$sha512sums"' 2>/dev/null) || {
|
||||||
|
echo "$d: APKBUILD does not source cleanly" >&2; rc=1; continue
|
||||||
|
}
|
||||||
|
srcs=$(printf '%s\n' "$info" | sed '/^==$/,$d')
|
||||||
|
sums=$(printf '%s\n' "$info" | sed '1,/^==$/d')
|
||||||
|
for s in $srcs; do
|
||||||
|
case "$s" in
|
||||||
|
*://*) continue ;; # remote: abuild fetches and verifies
|
||||||
|
*::*) f=${s%%::*} ;;
|
||||||
|
*) f=${s##*/} ;;
|
||||||
|
esac
|
||||||
|
# a local source may sit in a subdirectory (rules/00_log_all.nft);
|
||||||
|
# the sums entry is keyed by its basename either way
|
||||||
|
path="$d/$s"; [ -f "$path" ] || path="$d/$f"
|
||||||
|
want=$(printf '%s\n' "$sums" | awk -v f="$f" '$2==f{print $1}')
|
||||||
|
[ "$want" = REPLACED_BY_CI ] && continue
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
echo "$d: local source '$s' is missing" >&2; rc=1; continue
|
||||||
|
fi
|
||||||
|
if [ -z "$want" ]; then
|
||||||
|
echo "$d: '$f' has no sha512sums entry" >&2; rc=1; continue
|
||||||
|
fi
|
||||||
|
have=$(sha512sum "$path" | awk '{print $1}')
|
||||||
|
if [ "$want" != "$have" ]; then
|
||||||
|
echo "$d: sha512 MISMATCH for '$f' (APKBUILD has ${want%"${want#????????????????}"}..., file is ${have%"${have#????????????????}"}...)" >&2
|
||||||
|
echo "$d: fix: update the sha512sums entry to: $have $f" >&2
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
[ "$rc" = 0 ] && echo "check-aports: all local source checksums match"
|
||||||
|
exit $rc
|
||||||
15
install.sh
15
install.sh
|
|
@ -76,10 +76,15 @@ say "waiting for the phone to come up on the USB network ($PHONE)"
|
||||||
# machine's side of the USB link: the gadget shows up as a new network
|
# machine's side of the USB link: the gadget shows up as a new network
|
||||||
# interface here, and either nothing configures an address on it, or a
|
# interface here, and either nothing configures an address on it, or a
|
||||||
# network manager grabs it with the wrong profile (we have seen
|
# network manager grabs it with the wrong profile (we have seen
|
||||||
# NetworkManager hand it an unrelated LAN address). The phone runs a DHCP
|
# NetworkManager hand it an unrelated LAN address). Whether the host gets
|
||||||
# server on the link, but host managers don't always ask.
|
# an address automatically depends on its network manager - don't count
|
||||||
|
# on it.
|
||||||
|
# The probe must be ssh-keyscan, not bash's /dev/tcp: this script runs
|
||||||
|
# under /bin/sh, which is dash on Debian, where /dev/tcp is a literal
|
||||||
|
# nonexistent path and the loop never succeeds (field report, 2026-08-28).
|
||||||
|
# ssh-keyscan ships with the ssh client we already require.
|
||||||
i=0
|
i=0
|
||||||
until (exec 3<>"/dev/tcp/$PHONE/22") 2>/dev/null; do
|
until ssh-keyscan -T 5 "$PHONE" >/dev/null 2>&1; do
|
||||||
i=$((i+1))
|
i=$((i+1))
|
||||||
if [ $i -gt 40 ]; then
|
if [ $i -gt 40 ]; then
|
||||||
cat >&2 <<'EOF'
|
cat >&2 <<'EOF'
|
||||||
|
|
@ -98,12 +103,14 @@ its side of the USB network link. Fix it by hand:
|
||||||
|
|
||||||
If a network manager keeps reclaiming the interface, tell it to ignore it,
|
If a network manager keeps reclaiming the interface, tell it to ignore it,
|
||||||
e.g.: sudo nmcli device set <iface> managed no
|
e.g.: sudo nmcli device set <iface> managed no
|
||||||
|
|
||||||
|
If ping 172.16.42.1 already works, this machine's side is fine - the phone's
|
||||||
|
ssh service just isn't answering yet. Wait a minute and re-run.
|
||||||
EOF
|
EOF
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
exec 3<&- 3>&- 2>/dev/null || true
|
|
||||||
sleep 8
|
sleep 8
|
||||||
|
|
||||||
# --- 4: write boot from Linux ---------------------------------------------------
|
# --- 4: write boot from Linux ---------------------------------------------------
|
||||||
|
|
|
||||||
185
registry-fetch.py
Executable file
185
registry-fetch.py
Executable file
|
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Fetch the newest published versions of our registry packages, verified.
|
||||||
|
|
||||||
|
registry-fetch.py <registry-url> <trusted-key.rsa.pub> <dest-dir> <group>...
|
||||||
|
|
||||||
|
<registry-url> is the Alpine repository root the phones carry in
|
||||||
|
/etc/apk/repositories (.../alpine/edge/fp6); <trusted-key.rsa.pub> is the key
|
||||||
|
they carry in /etc/apk/keys (aports/device/catcrafts-fp6-repo/); a <group> is
|
||||||
|
a comma-separated list of package names whose FIRST member decides the
|
||||||
|
version: "imsd,imsd-systemd" fetches the newest imsd and the imsd-systemd of
|
||||||
|
that same version, and fails if the registry lacks it.
|
||||||
|
|
||||||
|
Verification mirrors apk's own, so the image trusts exactly what an installed
|
||||||
|
phone trusts: the index signature (.SIGN.RSA*.<key>, over the index's
|
||||||
|
compressed tar) against the trusted key, and the signing key's NAME against
|
||||||
|
the trusted key's; each package's control segment against the index's C:
|
||||||
|
checksum ("Q1" + base64 sha1); each data segment against the control
|
||||||
|
segment's datahash (sha256). A package that fails any step is not written.
|
||||||
|
Prints one "name version sha256" line per apk and writes the same lines to
|
||||||
|
<dest-dir>/manifest.
|
||||||
|
|
||||||
|
Version order: apk's rules for the shapes our own packages use
|
||||||
|
(X.Y.Z[-rN], numeric components); a suffix like _git is compared as text.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import gzip
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
DIGEST = {"RSA": "sha1", "RSA256": "sha256", "RSA512": "sha512"}
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg):
|
||||||
|
sys.exit(f"registry-fetch: {msg}")
|
||||||
|
|
||||||
|
|
||||||
|
def gzip_members(data):
|
||||||
|
off = 0
|
||||||
|
while off < len(data):
|
||||||
|
d = zlib.decompressobj(31)
|
||||||
|
d.decompress(data[off:])
|
||||||
|
end = len(data) - len(d.unused_data)
|
||||||
|
if end <= off:
|
||||||
|
raise ValueError("gzip stream did not advance")
|
||||||
|
yield data[off:end]
|
||||||
|
off = end
|
||||||
|
|
||||||
|
|
||||||
|
def tar_files(tar):
|
||||||
|
"""(name, bytes) for each regular file in a tar image; pax headers skipped."""
|
||||||
|
off = 0
|
||||||
|
while off + 512 <= len(tar):
|
||||||
|
hdr = tar[off:off + 512]
|
||||||
|
if hdr == b"\0" * 512:
|
||||||
|
return
|
||||||
|
size = int(hdr[124:136].split(b"\0")[0].strip() or b"0", 8)
|
||||||
|
name = hdr[:100].rstrip(b"\0").decode()
|
||||||
|
if hdr[156:157] not in (b"x", b"g"):
|
||||||
|
yield name, tar[off + 512:off + 512 + size]
|
||||||
|
off += 512 + (size + 511) // 512 * 512
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url):
|
||||||
|
last = None
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=120) as r:
|
||||||
|
return r.read()
|
||||||
|
except Exception as e: # noqa: BLE001 - any transport failure retries
|
||||||
|
last = e
|
||||||
|
time.sleep(10)
|
||||||
|
die(f"cannot fetch {url}: {last}")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_index(index_tgz, keyfile):
|
||||||
|
"""Returns the APKINDEX text after checking the signature against keyfile."""
|
||||||
|
try:
|
||||||
|
members = list(gzip_members(index_tgz))
|
||||||
|
except (zlib.error, ValueError) as e:
|
||||||
|
die(f"index: corrupt gzip stream ({e})")
|
||||||
|
if len(members) != 2:
|
||||||
|
die(f"index: expected 2 gzip streams, found {len(members)}")
|
||||||
|
sig_entries = list(tar_files(gzip.decompress(members[0])))
|
||||||
|
if not sig_entries:
|
||||||
|
die("index: no signature entry")
|
||||||
|
name, sig = sig_entries[0]
|
||||||
|
m = re.fullmatch(r"\.SIGN\.(RSA\d*)\.(.+)", name)
|
||||||
|
if not m or m.group(1) not in DIGEST:
|
||||||
|
die(f"index: unexpected signature entry {name!r}")
|
||||||
|
kind, signer = m.groups()
|
||||||
|
if signer != os.path.basename(keyfile):
|
||||||
|
die(f"index: signed by {signer!r}, phones trust {os.path.basename(keyfile)!r}")
|
||||||
|
with tempfile.TemporaryDirectory() as t:
|
||||||
|
sigf, dataf = os.path.join(t, "sig"), os.path.join(t, "data")
|
||||||
|
open(sigf, "wb").write(sig)
|
||||||
|
open(dataf, "wb").write(members[1])
|
||||||
|
r = subprocess.run(["openssl", "dgst", f"-{DIGEST[kind]}", "-verify", keyfile,
|
||||||
|
"-signature", sigf, dataf], capture_output=True, text=True)
|
||||||
|
if r.returncode != 0 or "Verified OK" not in r.stdout:
|
||||||
|
die(f"index: signature does NOT verify against {keyfile}: {r.stdout.strip()} {r.stderr.strip()}")
|
||||||
|
files = dict(tar_files(gzip.decompress(members[1])))
|
||||||
|
if "APKINDEX" not in files:
|
||||||
|
die("index: no APKINDEX entry")
|
||||||
|
return files["APKINDEX"].decode()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_index(text):
|
||||||
|
"""{name: {version: fields}} for aarch64 entries."""
|
||||||
|
out = {}
|
||||||
|
for block in text.split("\n\n"):
|
||||||
|
f = dict(line.split(":", 1) for line in block.splitlines() if ":" in line)
|
||||||
|
if f.get("A", "aarch64") != "aarch64" or "P" not in f or "V" not in f:
|
||||||
|
continue
|
||||||
|
out.setdefault(f["P"], {})[f["V"]] = f
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def version_key(v):
|
||||||
|
ver, _, rel = v.partition("-r")
|
||||||
|
parts = tuple((0, int(t)) if t.isdigit() else (1, t) for t in re.split(r"[._]", ver))
|
||||||
|
return parts, int(rel) if rel.isdigit() else 0
|
||||||
|
|
||||||
|
|
||||||
|
def verify_apk(blob, fields, name):
|
||||||
|
try:
|
||||||
|
members = list(gzip_members(blob))
|
||||||
|
except (zlib.error, ValueError) as e:
|
||||||
|
die(f"{name}: corrupt gzip stream ({e})")
|
||||||
|
if len(members) != 3:
|
||||||
|
die(f"{name}: expected 3 gzip streams, found {len(members)}")
|
||||||
|
want = fields.get("C", "")
|
||||||
|
if not want.startswith("Q1"):
|
||||||
|
die(f"{name}: index has no Q1 checksum")
|
||||||
|
got = "Q1" + base64.b64encode(hashlib.sha1(members[1]).digest()).decode()
|
||||||
|
if got != want:
|
||||||
|
die(f"{name}: control checksum {got} != index {want}")
|
||||||
|
pkginfo = dict(tar_files(gzip.decompress(members[1]))).get(".PKGINFO", b"").decode()
|
||||||
|
datahash = next((l.split("=", 1)[1].strip() for l in pkginfo.splitlines()
|
||||||
|
if l.startswith("datahash")), None)
|
||||||
|
if not datahash:
|
||||||
|
die(f"{name}: .PKGINFO has no datahash")
|
||||||
|
if hashlib.sha256(members[2]).hexdigest() != datahash:
|
||||||
|
die(f"{name}: data segment does not match its datahash")
|
||||||
|
if "S" in fields and int(fields["S"]) != len(blob):
|
||||||
|
die(f"{name}: size {len(blob)} != index {fields['S']}")
|
||||||
|
|
||||||
|
|
||||||
|
def main(registry, keyfile, dest, groups):
|
||||||
|
registry = registry.rstrip("/")
|
||||||
|
if not os.path.isfile(keyfile):
|
||||||
|
die(f"trusted key {keyfile} not found")
|
||||||
|
os.makedirs(dest, exist_ok=True)
|
||||||
|
index = parse_index(verify_index(fetch(f"{registry}/aarch64/APKINDEX.tar.gz"), keyfile))
|
||||||
|
lines = []
|
||||||
|
for group in groups:
|
||||||
|
names = group.split(",")
|
||||||
|
anchor = names[0]
|
||||||
|
if anchor not in index:
|
||||||
|
die(f"{anchor}: not in the registry index")
|
||||||
|
version = max(index[anchor], key=version_key)
|
||||||
|
for n in names:
|
||||||
|
fields = index.get(n, {}).get(version)
|
||||||
|
if fields is None:
|
||||||
|
die(f"{n}-{version}: not in the registry (newest {anchor} is {version})")
|
||||||
|
fname = f"{n}-{version}.apk"
|
||||||
|
blob = fetch(f"{registry}/aarch64/{fname}")
|
||||||
|
verify_apk(blob, fields, fname)
|
||||||
|
open(os.path.join(dest, fname), "wb").write(blob)
|
||||||
|
lines.append(f"{n} {version} {hashlib.sha256(blob).hexdigest()}")
|
||||||
|
with open(os.path.join(dest, "manifest"), "w") as f:
|
||||||
|
f.write("\n".join(lines) + "\n")
|
||||||
|
print("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 5:
|
||||||
|
sys.exit(__doc__)
|
||||||
|
main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4:])
|
||||||
Loading…
Reference in a new issue