Skip to content
This repository was archived by the owner on Nov 8, 2026. It is now read-only.

Commit 2d5d85e

Browse files
techmoreclaude
andauthored
Kernel split step 7: pin the Kernel-repo release, add opt-in fetch-kernel (#631)
Additive. mseries/kernel-pin.json pins release kernel-2026.10.27.1 of techmore/ElementaryOS_MSeries-Kernel by tag and sha256. mseries/fetch-kernel.py downloads and verifies it (and checks the fairydust mkosi profile versions against the pin). daily-9.0.yml gets an opt-in fetch_kernel input and a fetch-kernel job; the in-repo build-kernel job stays the default. Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 73797e9 commit 2d5d85e

5 files changed

Lines changed: 216 additions & 3 deletions

File tree

‎.github/workflows/daily-9.0.yml‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ on:
1414
type: boolean
1515
required: false
1616
default: false
17+
fetch_kernel:
18+
description: With fairydust, use the pinned Kernel-repo release (mseries/kernel-pin.json) instead of building the kernel here
19+
type: boolean
20+
required: false
21+
default: false
1722

1823
permissions:
1924
contents: read
@@ -23,8 +28,23 @@ concurrency:
2328
cancel-in-progress: false
2429

2530
jobs:
31+
fetch-kernel:
32+
if: inputs.fairydust && inputs.fetch_kernel
33+
runs-on: ubuntu-26.04-arm
34+
timeout-minutes: 20
35+
steps:
36+
- uses: actions/checkout@v7
37+
- name: Fetch and verify the pinned kernel release
38+
run: python3 mseries/fetch-kernel.py --check-profiles --out mkosi.packages
39+
- name: Preserve pinned kernel packages
40+
uses: actions/upload-artifact@v7
41+
with:
42+
name: fairydust-kernel-packages-${{ github.run_id }}
43+
path: mkosi.packages/
44+
retention-days: 2
45+
2646
build-kernel:
27-
if: inputs.fairydust
47+
if: inputs.fairydust && !inputs.fetch_kernel
2848
runs-on: ubuntu-26.04-arm
2949
timeout-minutes: 180
3050
steps:
@@ -87,8 +107,8 @@ jobs:
87107
retention-days: 2
88108

89109
build:
90-
needs: build-kernel
91-
if: always() && (!inputs.fairydust || needs.build-kernel.result == 'success')
110+
needs: [build-kernel, fetch-kernel]
111+
if: always() && (!inputs.fairydust || needs.build-kernel.result == 'success' || needs.fetch-kernel.result == 'success')
92112
runs-on: ubuntu-26.04-arm
93113
timeout-minutes: 180
94114
steps:

‎mseries/fetch-kernel.py‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
"""Fetch the pinned kernel packages from a Kernel-repo release and verify every checksum.
3+
4+
The OS image consumes kernel output by immutable release tag and sha256 (mseries/kernel-pin.json),
5+
never by branch. `--check-profiles` only compares the pin with the Fairydust mkosi profile versions.
6+
"""
7+
import argparse
8+
import hashlib
9+
import json
10+
import re
11+
import sys
12+
import urllib.request
13+
from pathlib import Path
14+
15+
ROOT = Path(__file__).resolve().parent.parent
16+
PIN = ROOT / 'mseries/kernel-pin.json'
17+
PROFILE = ROOT / 'mkosi.images/base/mkosi.profiles/fairydust/mkosi.conf'
18+
SHA = re.compile(r'[0-9a-f]{64}')
19+
BASE = 'https://github.com/{repo}/releases/download/{tag}/{asset}'
20+
21+
22+
def load_pin(path=PIN):
23+
pin = json.loads(Path(path).read_text())
24+
tag = pin.get('release_tag', '')
25+
if pin.get('schema') != 1 or not tag.startswith('kernel-') or tag in ('latest', 'main', 'dev'):
26+
raise SystemExit('pin needs schema 1 and an immutable kernel-* release_tag')
27+
items = pin['packages'] + [pin['boot']['m1n1_bootbin']]
28+
for item in items:
29+
if not SHA.fullmatch(item.get('sha256', '')) or not item.get('asset') or not item.get('file'):
30+
raise SystemExit(f'pin entry is missing sha256, asset or file: {item}')
31+
return pin, items
32+
33+
34+
def sha256(data):
35+
return hashlib.sha256(data).hexdigest()
36+
37+
38+
def profile_versions(path=PROFILE):
39+
out = {}
40+
for line in Path(path).read_text().splitlines():
41+
m = re.fullmatch(r'\s+([A-Za-z0-9.+-]+)=(\S+)', line)
42+
if m:
43+
out[m[1]] = m[2]
44+
return out
45+
46+
47+
def check_profiles(pin, path=PROFILE):
48+
have = profile_versions(path)
49+
bad = [f"{p['name']}: pin {p['version']} profile {have.get(p['name'])}"
50+
for p in pin['packages'] if have.get(p['name']) != p['version']]
51+
if bad:
52+
raise SystemExit('fairydust profile does not match kernel-pin.json:\n ' + '\n '.join(bad))
53+
54+
55+
def fetch(pin, items, out, base):
56+
out.mkdir(parents=True, exist_ok=True)
57+
for item in items:
58+
url = base.format(repo=pin['kernel_repo'], tag=pin['release_tag'], asset=item['asset'])
59+
with urllib.request.urlopen(url) as r:
60+
data = r.read()
61+
if sha256(data) != item['sha256']:
62+
raise SystemExit(f"checksum mismatch for {item['file']}: got {sha256(data)}, pinned {item['sha256']}")
63+
(out / item['file']).write_bytes(data)
64+
print(f"ok {item['file']}")
65+
debs = [p for p in pin['packages']]
66+
upstream = [p for p in debs if not p['name'].startswith('linux-image-')]
67+
boot = pin['boot']['m1n1_bootbin']
68+
line = lambda i: f"{i['sha256']} {i['file']}\n"
69+
(out / 'SHA256SUMS').write_text(''.join(line(p) for p in debs))
70+
(out / 'UPSTREAM-SHA256SUMS').write_text(''.join(line(p) for p in upstream))
71+
(out / 'FAIRYDUST-BOOT-SHA256SUMS').write_text(line(boot))
72+
73+
74+
def main():
75+
ap = argparse.ArgumentParser()
76+
ap.add_argument('--pin', default=str(PIN))
77+
ap.add_argument('--out', type=Path)
78+
ap.add_argument('--base-url', default=BASE, help='template with {repo} {tag} {asset}; file:// works for tests')
79+
ap.add_argument('--check-profiles', action='store_true')
80+
a = ap.parse_args()
81+
pin, items = load_pin(a.pin)
82+
if a.check_profiles:
83+
check_profiles(pin)
84+
print('pin matches fairydust profile versions')
85+
if a.out:
86+
fetch(pin, items, a.out, a.base_url)
87+
return 0
88+
89+
90+
if __name__ == '__main__':
91+
sys.exit(main())

‎mseries/kernel-pin.json‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"schema": 1,
3+
"kernel_repo": "techmore/ElementaryOS_MSeries-Kernel",
4+
"release_tag": "kernel-2026.10.27.1",
5+
"kernel_repo_commit": "c98d92c16807f8212e93818e11706ee32170d1db",
6+
"channel": "fairydust-usb4",
7+
"kernel_release": "7.1.13-elementary-fairydust-usb4",
8+
"toolchain_note": "Built by Kernel-repo CI with rustc 1.99.0; the earlier local build used rustc 1.98.1 (compiled Rust differs, source and config options identical). Accepted as canonical by the owner on 2026-10-27, to be proven by the j314s boot.",
9+
"packages": [
10+
{
11+
"name": "linux-image-7.1.13-elementary-fairydust-usb4",
12+
"version": "7.1.13+elementary.usb4.4",
13+
"file": "linux-image-7.1.13-elementary-fairydust-usb4_7.1.13+elementary.usb4.4_arm64.deb",
14+
"asset": "linux-image-7.1.13-elementary-fairydust-usb4_7.1.13+elementary.usb4.4_arm64.deb",
15+
"sha256": "deae4866097b2cc46494ca2020e86913c92035439423e1292ccb49145be25430"
16+
},
17+
{
18+
"name": "m1n1",
19+
"version": "1.5.2-2ubuntu0.1~asahi1",
20+
"file": "m1n1_1.5.2-2ubuntu0.1~asahi1_arm64.deb",
21+
"asset": "m1n1_1.5.2-2ubuntu0.1.asahi1_arm64.deb",
22+
"sha256": "6c78ead41b46b7cfca145e01743ed4874e58e8b37f0127d46d3b7c186cdb2088"
23+
},
24+
{
25+
"name": "u-boot-asahi",
26+
"version": "2025.10-2-1",
27+
"file": "u-boot-asahi_2025.10-2-1_arm64.deb",
28+
"asset": "u-boot-asahi_2025.10-2-1_arm64.deb",
29+
"sha256": "e0cc3d96dc4d57fb491979280254bc095204588c66984bc46948b35529690b51"
30+
}
31+
],
32+
"boot": {
33+
"m1n1_bootbin": {
34+
"file": "fairydust-m1n1-boot.bin",
35+
"asset": "fairydust-m1n1-boot.bin",
36+
"sha256": "049c5bf29e5e4f379e44e8ef8fffce3a018ede03ee19676c98cc6ee6e8608f8d"
37+
}
38+
}
39+
}

‎mseries/test-kernel-pin.py‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
"""Offline tests for fetch-kernel.py and kernel-pin.json (no network)."""
3+
import importlib.util
4+
import json
5+
import tempfile
6+
import unittest
7+
from pathlib import Path
8+
9+
here = Path(__file__).resolve().parent
10+
spec = importlib.util.spec_from_file_location('fetch_kernel', here / 'fetch-kernel.py')
11+
fk = importlib.util.module_from_spec(spec)
12+
spec.loader.exec_module(fk)
13+
14+
15+
class PinTests(unittest.TestCase):
16+
def test_pin_is_immutable_and_complete(self):
17+
pin, items = fk.load_pin()
18+
self.assertTrue(pin['release_tag'].startswith('kernel-'))
19+
self.assertEqual(len(items), 4)
20+
21+
def test_pin_matches_fairydust_profile(self):
22+
pin, _ = fk.load_pin()
23+
fk.check_profiles(pin)
24+
25+
def test_mutable_tag_is_refused(self):
26+
pin = json.loads(fk.PIN.read_text())
27+
pin['release_tag'] = 'main'
28+
with tempfile.TemporaryDirectory() as d:
29+
p = Path(d) / 'pin.json'
30+
p.write_text(json.dumps(pin))
31+
with self.assertRaises(SystemExit):
32+
fk.load_pin(p)
33+
34+
def _serve(self, d, pin, items, corrupt=None):
35+
rel = Path(d) / 'rel'
36+
rel.mkdir()
37+
for it in items:
38+
data = b'bad' if it['asset'] == corrupt else it['asset'].encode()
39+
(rel / it['asset']).write_bytes(data)
40+
it['sha256'] = fk.sha256(it['asset'].encode())
41+
return 'file://' + str(rel) + '/{asset}'
42+
43+
def test_fetch_verifies_and_writes_sums(self):
44+
pin, items = fk.load_pin()
45+
with tempfile.TemporaryDirectory() as d:
46+
base = self._serve(d, pin, items)
47+
out = Path(d) / 'out'
48+
fk.fetch(pin, items, out, base)
49+
sums = (out / 'SHA256SUMS').read_text().splitlines()
50+
self.assertEqual(len(sums), 3)
51+
self.assertIn('m1n1_1.5.2-2ubuntu0.1~asahi1_arm64.deb', (out / 'UPSTREAM-SHA256SUMS').read_text())
52+
53+
def test_checksum_mismatch_fails(self):
54+
pin, items = fk.load_pin()
55+
with tempfile.TemporaryDirectory() as d:
56+
base = self._serve(d, pin, items, corrupt=items[1]['asset'])
57+
with self.assertRaises(SystemExit):
58+
fk.fetch(pin, items, Path(d) / 'out', base)
59+
60+
61+
if __name__ == '__main__':
62+
unittest.main()

‎mseries/validate.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ check kernel 'fairydust/usb4/test-*.py' 'for f in mseries/fairydust/usb4/test-*.
223223
check kernel 'fairydust/usb4/tools/test-staging-tools.py' 'python3 mseries/fairydust/usb4/tools/test-staging-tools.py'
224224
check kernel 'boot/test-prepare-uki-kernels.py' 'python3 mseries/boot/test-prepare-uki-kernels.py'
225225
check kernel 'test-kernel-config.py' 'python3 mseries/test-kernel-config.py'
226+
check kernel 'test-kernel-pin.py' 'python3 mseries/test-kernel-pin.py'
226227
check kernel 'aurora unit tests' "python3 -m unittest discover -s mseries/aurora -p 'test_*.py'"
227228
check kernel 'aurora-sep/test_touchid_core.py' 'python3 mseries/aurora-sep/test_touchid_core.py'
228229
check kernel 'touchid-settings/tests/test-pam-helper.sh' 'bash mseries/touchid-settings/tests/test-pam-helper.sh'

0 commit comments

Comments
 (0)