py-pi-bake 0.0.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ # Build + publish py-pi-bake to PyPI on every `v*` tag.
2
+ #
3
+ # Auth: PyPI Trusted Publishing (OIDC) — no secrets stored in the
4
+ # repo. Before the first publish, configure PyPI ONCE:
5
+ #
6
+ # https://pypi.org/manage/project/py-pi-bake/settings/publishing/
7
+ #
8
+ # PyPI Project Name: py-pi-bake
9
+ # Owner: kurt-cb
10
+ # Repository: pi-bake
11
+ # Workflow: publish.yml
12
+ # Environment: pypi
13
+ #
14
+ # (For the very first release before the PyPI project exists, use
15
+ # https://pypi.org/manage/account/publishing/ — "Add a pending
16
+ # publisher" — with the same values.)
17
+ #
18
+ # Version comes from the git tag via setuptools_scm. Tagging
19
+ # `v0.0.4` produces a sdist + wheel for version `0.0.4`.
20
+
21
+ name: publish
22
+
23
+ on:
24
+ push:
25
+ tags:
26
+ - "v*"
27
+
28
+ jobs:
29
+ build:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ # setuptools_scm needs the tag history to derive the version.
35
+ fetch-depth: 0
36
+
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.12"
40
+
41
+ - name: Install build tooling
42
+ run: python -m pip install --upgrade build
43
+
44
+ - name: Build sdist + wheel
45
+ run: python -m build
46
+
47
+ - name: Show version that will be published
48
+ run: |
49
+ ls -la dist/
50
+ python -c "import importlib.metadata as m; print(m.version('py-pi-bake'))" \
51
+ || true
52
+
53
+ - uses: actions/upload-artifact@v4
54
+ with:
55
+ name: dist
56
+ path: dist/
57
+
58
+ publish:
59
+ needs: build
60
+ runs-on: ubuntu-latest
61
+ environment: pypi
62
+ permissions:
63
+ # Required for Trusted Publishing (OIDC token minting).
64
+ id-token: write
65
+ steps:
66
+ - uses: actions/download-artifact@v4
67
+ with:
68
+ name: dist
69
+ path: dist/
70
+
71
+ - uses: pypa/gh-action-pypi-publish@release/v1
72
+ # No `with: password:` — Trusted Publishing handles auth via
73
+ # the OIDC token issued for this workflow + environment.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .pytest_cache/
8
+ .coverage
9
+ .cache/
10
+ *.img
11
+ *.img.gz
12
+ *.apkovl.tar.gz
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kurt Godwin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-pi-bake
3
+ Version: 0.0.4
4
+ Summary: Generate flashable, headless Raspberry Pi images (no setup-alpine, no rpi-imager-pre-fill required)
5
+ Author-email: Kurt Godwin <kurtgo@hotmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/kurt-cb/pi-bake
8
+ Project-URL: Issues, https://github.com/kurt-cb/pi-bake/issues
9
+ Keywords: raspberry-pi,alpine,raspbian,headless,image,sd-card
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Topic :: System :: Boot
21
+ Classifier: Topic :: System :: Installation/Setup
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # pi-bake
28
+
29
+ Generate flashable, headless Raspberry Pi images. Flash one
30
+ `.img.gz` per Pi, boot, SSH in. No `setup-alpine` interactive
31
+ walk, no `rpi-imager` GUI clicking through pre-fill, no console
32
+ on the Pi.
33
+
34
+ ## What gets baked
35
+
36
+ Per node, from CLI flags or the Python API:
37
+
38
+ - **Hostname** → `/etc/hostname`
39
+ - **SSH pubkey** → `/root/.ssh/authorized_keys` (mode 0600) +
40
+ sshd `PasswordAuthentication no`
41
+ - **WiFi creds** (optional) → `/etc/wpa_supplicant/wpa_supplicant.conf`
42
+ so the Pi auto-joins on first boot. Omit for wired-only.
43
+ - **Timezone, regulatory country** (sensible defaults)
44
+ - **First-boot script** that `apk add`s the small set of packages
45
+ (openssh-server, iproute2, etc.), enables services, then
46
+ self-disables.
47
+
48
+ That's it. No role-specific code, no totaldns, no platform lock-in.
49
+ Once the Pi is on the network, whatever orchestrator you use
50
+ (pyinfra, Ansible, plain SSH) takes over.
51
+
52
+ ## Install
53
+
54
+ ```
55
+ pip install pi-bake
56
+ ```
57
+
58
+ System tools (one-time per dev machine):
59
+
60
+ ```
61
+ # Fedora
62
+ sudo dnf install mtools dosfstools
63
+ # Debian / Ubuntu
64
+ sudo apt install mtools dosfstools
65
+ # Alpine
66
+ apk add mtools dosfstools
67
+ ```
68
+
69
+ The Alpine baker uses **`mtools`** (no root). The Raspbian baker
70
+ (v0.2) will additionally need `sudo` for `losetup`.
71
+
72
+ ## Quick start
73
+
74
+ ```
75
+ # What can we bake for what?
76
+ pi-bake list-boards
77
+ pi-bake list-os --board pi-zero-2-w
78
+
79
+ # Bake an Alpine image for a Pi Zero 2 W with WiFi creds.
80
+ pi-bake build \
81
+ --board pi-zero-2-w \
82
+ --os alpine \
83
+ --hostname pi-radio-1 \
84
+ --ssh-pubkey ~/.ssh/id_ed25519.pub \
85
+ --wifi-ssid totaldns-lab \
86
+ --wifi-psk secret \
87
+ --out ~/sdcards/pi-radio-1.img.gz
88
+
89
+ # Flash. Replace mmcblk0 with your SD card's actual device.
90
+ zcat ~/sdcards/pi-radio-1.img.gz | sudo dd of=/dev/mmcblk0 bs=4M status=progress
91
+
92
+ # Boot the Pi. Wait ~30s. Then:
93
+ ssh root@pi-radio-1.lan uptime
94
+ ```
95
+
96
+ For wired-only nodes (eth0), omit the WiFi flags:
97
+
98
+ ```
99
+ pi-bake build \
100
+ --board pi-5 --os alpine --hostname boat \
101
+ --ssh-pubkey ~/.ssh/id_ed25519.pub \
102
+ --out ~/sdcards/boat.img.gz
103
+ ```
104
+
105
+ ## Supported boards × OSes
106
+
107
+ | Board | Alpine | Raspbian | Debian |
108
+ |----------------|--------|----------|--------|
109
+ | Pi Zero W | ✓ (armhf) | ✗ (32-bit ARMv6 not packaged) | ✗ |
110
+ | Pi Zero 2 W | ✓ | ✓ (32-bit ARMv7 / arm64) | ✗ |
111
+ | Pi 3 | ✓ | ✓ | ✗ |
112
+ | Pi 4 | ✓ | ✓ | ✓ |
113
+ | Pi 5 | ✓ (3.21+) | ✓ | ✓ |
114
+
115
+ Run `pi-bake list-os --board <b>` for the current matrix.
116
+
117
+ ## Status
118
+
119
+ **v0.1**: Alpine baker is fully working (no-root, mtools). Raspbian
120
+ + Debian backends are stubbed with a clear error pointing at the
121
+ v0.2 roadmap. Most Pi 4 / Pi 5 deployments bootstrap with
122
+ `rpi-imager`'s pre-fill flow today — `pi-bake` will take that
123
+ over in v0.2.
124
+
125
+ ## Python API
126
+
127
+ The CLI is a thin wrapper around `pi_bake.build()`:
128
+
129
+ ```python
130
+ from pi_bake import build, NodeConfig
131
+
132
+ build(
133
+ board="pi-zero-2-w",
134
+ os_name="alpine",
135
+ version=None, # latest known-good
136
+ node=NodeConfig(
137
+ hostname="pi-radio-1",
138
+ ssh_pubkey=open(".../id_ed25519.pub").read(),
139
+ wifi_ssid="totaldns-lab",
140
+ wifi_psk="secret",
141
+ ),
142
+ out_path="~/sdcards/pi-radio-1.img.gz",
143
+ )
144
+ ```
145
+
146
+ ## Roadmap
147
+
148
+ - **v0.2 — Raspbian + Debian backends.** `losetup -P` + `firstrun.sh`
149
+ injection. Needs `sudo`; CLI prompts honestly.
150
+ - **v0.2 — dynamic OS version discovery.** Pull
151
+ `https://dl-cdn.alpinelinux.org/alpine/` and the rpi downloads
152
+ index instead of the hardcoded `versions` tuples in the catalog.
153
+ - **v0.3 — multi-image batch.** `pi-bake build-many topology.json`
154
+ emits an `.img.gz` per node entry in one go.
155
+ - **v0.3 — static IP option.** Today we DHCP from first reachable
156
+ iface; static-IP-only deployments need a `--static-v4` flag.
157
+ - **v0.3 — encrypted overlay** for the rootfs (LUKS).
158
+
159
+ ## Why does this exist
160
+
161
+ Three reasons to bake images instead of using
162
+ `rpi-imager` pre-fill or hand-running `setup-alpine`:
163
+
164
+ 1. **Per-node config in a script.** Topology files (any shape —
165
+ JSON, YAML, a hand-written shell script) drive `pi-bake build`
166
+ in a loop. Lab grows from 1 Pi to 20 without 20 separate
167
+ keyboard sessions.
168
+ 2. **Reproducible.** Same inputs → same `.img.gz`. Re-flash a
169
+ replacement SD card identically; clone a node by changing the
170
+ hostname.
171
+ 3. **Alpine RPi has no `rpi-imager` pre-fill equivalent.** This is
172
+ the only convenient headless flow for the Pi Zero W / 2 W
173
+ family running Alpine.
174
+
175
+ ## License
176
+
177
+ MIT — see `LICENSE`.
@@ -0,0 +1,151 @@
1
+ # pi-bake
2
+
3
+ Generate flashable, headless Raspberry Pi images. Flash one
4
+ `.img.gz` per Pi, boot, SSH in. No `setup-alpine` interactive
5
+ walk, no `rpi-imager` GUI clicking through pre-fill, no console
6
+ on the Pi.
7
+
8
+ ## What gets baked
9
+
10
+ Per node, from CLI flags or the Python API:
11
+
12
+ - **Hostname** → `/etc/hostname`
13
+ - **SSH pubkey** → `/root/.ssh/authorized_keys` (mode 0600) +
14
+ sshd `PasswordAuthentication no`
15
+ - **WiFi creds** (optional) → `/etc/wpa_supplicant/wpa_supplicant.conf`
16
+ so the Pi auto-joins on first boot. Omit for wired-only.
17
+ - **Timezone, regulatory country** (sensible defaults)
18
+ - **First-boot script** that `apk add`s the small set of packages
19
+ (openssh-server, iproute2, etc.), enables services, then
20
+ self-disables.
21
+
22
+ That's it. No role-specific code, no totaldns, no platform lock-in.
23
+ Once the Pi is on the network, whatever orchestrator you use
24
+ (pyinfra, Ansible, plain SSH) takes over.
25
+
26
+ ## Install
27
+
28
+ ```
29
+ pip install pi-bake
30
+ ```
31
+
32
+ System tools (one-time per dev machine):
33
+
34
+ ```
35
+ # Fedora
36
+ sudo dnf install mtools dosfstools
37
+ # Debian / Ubuntu
38
+ sudo apt install mtools dosfstools
39
+ # Alpine
40
+ apk add mtools dosfstools
41
+ ```
42
+
43
+ The Alpine baker uses **`mtools`** (no root). The Raspbian baker
44
+ (v0.2) will additionally need `sudo` for `losetup`.
45
+
46
+ ## Quick start
47
+
48
+ ```
49
+ # What can we bake for what?
50
+ pi-bake list-boards
51
+ pi-bake list-os --board pi-zero-2-w
52
+
53
+ # Bake an Alpine image for a Pi Zero 2 W with WiFi creds.
54
+ pi-bake build \
55
+ --board pi-zero-2-w \
56
+ --os alpine \
57
+ --hostname pi-radio-1 \
58
+ --ssh-pubkey ~/.ssh/id_ed25519.pub \
59
+ --wifi-ssid totaldns-lab \
60
+ --wifi-psk secret \
61
+ --out ~/sdcards/pi-radio-1.img.gz
62
+
63
+ # Flash. Replace mmcblk0 with your SD card's actual device.
64
+ zcat ~/sdcards/pi-radio-1.img.gz | sudo dd of=/dev/mmcblk0 bs=4M status=progress
65
+
66
+ # Boot the Pi. Wait ~30s. Then:
67
+ ssh root@pi-radio-1.lan uptime
68
+ ```
69
+
70
+ For wired-only nodes (eth0), omit the WiFi flags:
71
+
72
+ ```
73
+ pi-bake build \
74
+ --board pi-5 --os alpine --hostname boat \
75
+ --ssh-pubkey ~/.ssh/id_ed25519.pub \
76
+ --out ~/sdcards/boat.img.gz
77
+ ```
78
+
79
+ ## Supported boards × OSes
80
+
81
+ | Board | Alpine | Raspbian | Debian |
82
+ |----------------|--------|----------|--------|
83
+ | Pi Zero W | ✓ (armhf) | ✗ (32-bit ARMv6 not packaged) | ✗ |
84
+ | Pi Zero 2 W | ✓ | ✓ (32-bit ARMv7 / arm64) | ✗ |
85
+ | Pi 3 | ✓ | ✓ | ✗ |
86
+ | Pi 4 | ✓ | ✓ | ✓ |
87
+ | Pi 5 | ✓ (3.21+) | ✓ | ✓ |
88
+
89
+ Run `pi-bake list-os --board <b>` for the current matrix.
90
+
91
+ ## Status
92
+
93
+ **v0.1**: Alpine baker is fully working (no-root, mtools). Raspbian
94
+ + Debian backends are stubbed with a clear error pointing at the
95
+ v0.2 roadmap. Most Pi 4 / Pi 5 deployments bootstrap with
96
+ `rpi-imager`'s pre-fill flow today — `pi-bake` will take that
97
+ over in v0.2.
98
+
99
+ ## Python API
100
+
101
+ The CLI is a thin wrapper around `pi_bake.build()`:
102
+
103
+ ```python
104
+ from pi_bake import build, NodeConfig
105
+
106
+ build(
107
+ board="pi-zero-2-w",
108
+ os_name="alpine",
109
+ version=None, # latest known-good
110
+ node=NodeConfig(
111
+ hostname="pi-radio-1",
112
+ ssh_pubkey=open(".../id_ed25519.pub").read(),
113
+ wifi_ssid="totaldns-lab",
114
+ wifi_psk="secret",
115
+ ),
116
+ out_path="~/sdcards/pi-radio-1.img.gz",
117
+ )
118
+ ```
119
+
120
+ ## Roadmap
121
+
122
+ - **v0.2 — Raspbian + Debian backends.** `losetup -P` + `firstrun.sh`
123
+ injection. Needs `sudo`; CLI prompts honestly.
124
+ - **v0.2 — dynamic OS version discovery.** Pull
125
+ `https://dl-cdn.alpinelinux.org/alpine/` and the rpi downloads
126
+ index instead of the hardcoded `versions` tuples in the catalog.
127
+ - **v0.3 — multi-image batch.** `pi-bake build-many topology.json`
128
+ emits an `.img.gz` per node entry in one go.
129
+ - **v0.3 — static IP option.** Today we DHCP from first reachable
130
+ iface; static-IP-only deployments need a `--static-v4` flag.
131
+ - **v0.3 — encrypted overlay** for the rootfs (LUKS).
132
+
133
+ ## Why does this exist
134
+
135
+ Three reasons to bake images instead of using
136
+ `rpi-imager` pre-fill or hand-running `setup-alpine`:
137
+
138
+ 1. **Per-node config in a script.** Topology files (any shape —
139
+ JSON, YAML, a hand-written shell script) drive `pi-bake build`
140
+ in a loop. Lab grows from 1 Pi to 20 without 20 separate
141
+ keyboard sessions.
142
+ 2. **Reproducible.** Same inputs → same `.img.gz`. Re-flash a
143
+ replacement SD card identically; clone a node by changing the
144
+ hostname.
145
+ 3. **Alpine RPi has no `rpi-imager` pre-fill equivalent.** This is
146
+ the only convenient headless flow for the Pi Zero W / 2 W
147
+ family running Alpine.
148
+
149
+ ## License
150
+
151
+ MIT — see `LICENSE`.
@@ -0,0 +1,124 @@
1
+ # pi-bake roadmap
2
+
3
+ ## v0.2 — top of list
4
+
5
+ ### Pre-baked SSH host keys (no host-key-change warnings)
6
+ Every reflash regenerates `/etc/ssh/ssh_host_*_key{,.pub}` so the
7
+ operator's `~/.ssh/known_hosts` flags the rebuilt Pi as "REMOTE HOST
8
+ IDENTIFICATION HAS CHANGED". Annoying; also breaks pyinfra runs
9
+ without `-o StrictHostKeyChecking=no`.
10
+
11
+ macmpi/alpine-linux-headless-bootstrap solves this by placing
12
+ `ssh_host_*_key{,.pub}` files alongside the apkovl on the FAT
13
+ partition; on first boot the apkovl restore copies them into
14
+ `/etc/ssh/` with the right perms (600/644). pi-bake should
15
+ either:
16
+
17
+ 1. Generate a per-hostname keypair at bake time and bake it
18
+ into the apkovl (`/etc/ssh/ssh_host_ed25519_key{,.pub}`),
19
+ OR
20
+ 2. Accept a `--ssh-host-key PATH` CLI flag pointing at an
21
+ existing keypair (so reflashes can reuse the same identity).
22
+
23
+ Probably (2) — operator-controlled, easy to back up, no surprises.
24
+
25
+ ### Bake-time package fetch (avahi + firmware)
26
+ Alpine RPi's stock /apks cache ships sshd, dhcpcd, chrony,
27
+ wpa_supplicant — enough for "real sshd + an IP". But .local
28
+ discovery via avahi-daemon, dbus, and WiFi firmware blobs
29
+ (linux-firmware-brcm for Pi-built-in BCM43455, linux-firmware-intel
30
+ for BE200) are NOT in the stock cache.
31
+
32
+ Plan: at bake time, after extracting the upstream tarball, do a
33
+ local `apk fetch` against the upstream Alpine mirror to drop
34
+ extra .apk files into `apks/aarch64/` and regenerate the
35
+ APKINDEX.tar.gz. Then add those packages to /etc/apk/world so
36
+ the diskless init's `apk add --no-network` picks them up from the
37
+ now-enriched local cache on first boot. No podman, no chroot —
38
+ just `apk fetch --no-cache --recursive --output ...` on the bake
39
+ host, which works as a regular user with the apk-tools package.
40
+
41
+ The 936f233 baseline (sshd + dhcpcd + chrony) is sufficient for
42
+ pyinfra take-over. Avahi is a discoverability nice-to-have, not a
43
+ blocker.
44
+
45
+ ### Interactive mode + YAML recipes (UX)
46
+ The `pi-bake build` CLI is now flag-heavy enough that nobody will
47
+ remember it. Add an interactive walk-through:
48
+
49
+ ```
50
+ pi-bake build --interactive
51
+ ? Which board? (pi-5, pi-zero-2-w, …) > pi-5
52
+ ? Which OS? (alpine 3.21.4, raspbian, …) > alpine 3.21.4
53
+ ? Hostname? > td-pi5-1
54
+ ? SSH pubkey path? (Tab to browse) > ~/.ssh/totaldns-adhoc.pub
55
+ ? WiFi? [y/N] > N
56
+ ? Static IP? [y/N] > y
57
+ ? CIDR? > 192.168.4.111/24
58
+ ? Gateway? > 192.168.4.1
59
+ ? HATs / expansion? (multi-select)
60
+ [x] Intel BE200 M.2 WiFi 7 (PCIe HAT)
61
+ [ ] PoE+ HAT (Pi 5)
62
+ [ ] Sense HAT
63
+ [ ] Adafruit 2.8" PiTFT 320x240
64
+ ? Save recipe to YAML? [td-pi5-1.yaml]
65
+ ? Bake now? [Y/n]
66
+ ```
67
+
68
+ The HAT picker drives config.txt edits (dtoverlay=, dtparam=).
69
+
70
+ ### YAML recipe in/out
71
+ - `pi-bake build --from-yaml pi-1.yaml` — bake from a saved recipe.
72
+ - `pi-bake build --to-yaml pi-1.yaml ...` — write the recipe without baking.
73
+ - `pi-bake build --interactive --to-yaml ...` — walk-through saves.
74
+
75
+ Means batch deployments are: edit pi-1.yaml, pi-2.yaml, pi-3.yaml,
76
+ then `for y in *.yaml; do pi-bake build --from-yaml $y; done`.
77
+
78
+ ### HAT catalog + config.txt overlays (FAT writes)
79
+ Currently we only write apkovl. To support HATs (PCIe BE200, Sense
80
+ HAT, displays, etc.) we need to write `/uboot/usercfg.txt` (the
81
+ Pi-canonical override file) or append to `/config.txt`. Each HAT
82
+ catalog entry knows its required dtoverlay= line(s).
83
+
84
+ ### Raspbian backend
85
+ Documented v0.2 target — losetup-based, sudo prompted. Pi 4 / Pi 5
86
+ Raspbian deployments still go via rpi-imager pre-fill until this lands.
87
+
88
+ ### Dynamic version discovery
89
+ Pull the live Alpine + Raspbian version indexes instead of the hard-
90
+ coded `versions` tuples. CLI flag `--refresh-versions` to update.
91
+
92
+ ## v0.1 retrospective (what we learned during real deployment)
93
+
94
+ - udhcpc on Pi 5 + busybox 1.37 + Alpine 3.21 hangs. Static IP
95
+ baked-in path now standard (commit 3a2efbd).
96
+ - Pi has no RTC. Without time, apk TLS verify rejects everything.
97
+ HTTP-Date header hack + chrony in firstboot (commit 3a2efbd).
98
+ - WiFi firmware needs explicit linux-firmware-{brcm,intel}, not a
99
+ meta `wifi-firmware` package (doesn't exist on Alpine).
100
+ - /etc/apk/repositories must be set to upstream main + community in
101
+ the apkovl — local FAT cache only covers the bootstrap set.
102
+ - FAT partition mounts read-only by default. lbu commit handles the
103
+ remount cycle; external apkovl pushes need explicit `mount -o
104
+ remount,rw /media/mmcblk0` first.
105
+ - Backup the apkovl before overwriting:
106
+ `cp .../td-pi5-1.apkovl.tar.gz{,.bak}` enables console rollback
107
+ without re-flashing.
108
+
109
+ ### DHCP client choice — RESOLVED (commit 936f233)
110
+ We picked dhcpcd over both busybox udhcpc and ISC dhclient. dhcpcd
111
+ ships pre-built in the stock Alpine RPi /apks cache (no bake-time
112
+ fetch needed), runs as a daemon watching all interfaces, sends DHCP
113
+ option 12 from /etc/hostname automatically, and doesn't share
114
+ udhcpc's Pi 5 + macb-driver hang. dhclient would have worked too
115
+ but isn't in the stock cache so it'd need a fetch dance.
116
+
117
+ ### Backup convention — APKOVL FILENAME LESSON
118
+ The Alpine RPi bootloader globs `*.apkovl.tar.gz` on the FAT root.
119
+ A backup at `td-pi5-1.apkovl.tar.gz.bak` was still glob-matched
120
+ (or close enough to confuse the loader) and bricked boot until
121
+ the user renamed it to `.failed`. Going forward: NEVER leave a
122
+ file matching the apkovl pattern on the FAT root. Either keep
123
+ backups OUTSIDE the FAT or use a name that has no `.apkovl.tar.gz`
124
+ anywhere in it.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "setuptools_scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ # PyPI distribution name is `py-pi-bake` (`pi-bake` was unavailable
7
+ # on PyPI). The import name + console_script stay `pi_bake` /
8
+ # `pi-bake`, so users `pip install py-pi-bake` then run
9
+ # `pi-bake build ...`.
10
+ name = "py-pi-bake"
11
+ # Version is derived from the latest git tag via setuptools_scm.
12
+ # `git tag v0.0.5 && git push --tags` releases 0.0.5; no version
13
+ # string lives in source. CI needs fetch-depth: 0 so tags reach
14
+ # the build environment.
15
+ dynamic = ["version"]
16
+ description = "Generate flashable, headless Raspberry Pi images (no setup-alpine, no rpi-imager-pre-fill required)"
17
+ readme = "README.md"
18
+ license = { text = "MIT" }
19
+ authors = [{ name = "Kurt Godwin", email = "kurtgo@hotmail.com" }]
20
+ requires-python = ">=3.9"
21
+ keywords = ["raspberry-pi", "alpine", "raspbian", "headless", "image", "sd-card"]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Programming Language :: Python :: 3.9",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Operating System :: POSIX :: Linux",
33
+ "Topic :: System :: Boot",
34
+ "Topic :: System :: Installation/Setup",
35
+ ]
36
+ # Pure stdlib at the Python level. Runtime depends on `mtools` +
37
+ # `dosfstools` on PATH (Alpine baker) and `losetup` (raspbian
38
+ # baker, when implemented in v0.2). Operator installs those via
39
+ # their distro package manager.
40
+ dependencies = []
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/kurt-cb/pi-bake"
44
+ Issues = "https://github.com/kurt-cb/pi-bake/issues"
45
+
46
+ [project.scripts]
47
+ pi-bake = "pi_bake.cli:main"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.setuptools_scm]
53
+ # Tags like `v0.0.4` map to version `0.0.4`. Untagged commits get
54
+ # a dev suffix (0.0.5.dev3+g<sha>), which pip + PyPI both accept.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,49 @@
1
+ """pi-bake — generate flashable, headless Raspberry Pi images.
2
+
3
+ Bake `(board, os, version, hostname, ssh_pubkey, [wifi])` into a
4
+ single `.img.gz` operator dd's to an SD card. Boot the Pi → it
5
+ joins the network → operator can SSH in. No keyboard, no monitor,
6
+ no `setup-alpine`.
7
+
8
+ Public API (also surfaced via the `pi-bake` CLI):
9
+
10
+ from pi_bake import NodeConfig, build, list_boards, list_oses
11
+ out = build(
12
+ board="pi-zero-2-w",
13
+ os_name="alpine",
14
+ version="3.21",
15
+ node=NodeConfig(
16
+ hostname="pi-radio-1",
17
+ ssh_pubkey="ssh-ed25519 AAAA...",
18
+ wifi_ssid="totaldns-lab",
19
+ wifi_psk="secret",
20
+ ),
21
+ out_path="~/sdcards/pi-radio-1.img.gz",
22
+ )
23
+
24
+ Designed to be agnostic of any specific downstream — totaldns,
25
+ home-server projects, anything that wants a flash-and-boot Pi.
26
+ """
27
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
28
+
29
+ from pi_bake.boards import Board, BOARDS, list_boards
30
+ from pi_bake.config import NodeConfig
31
+ from pi_bake.oses import OSImage, OSES, list_oses, resolve_image
32
+ from pi_bake.bake import build, supports
33
+
34
+ try:
35
+ # Distribution name on PyPI is `py-pi-bake`; importlib.metadata
36
+ # reads it from the installed dist-info, which setuptools_scm
37
+ # populated from the git tag at build time.
38
+ __version__ = _pkg_version("py-pi-bake")
39
+ except PackageNotFoundError:
40
+ # Running from a checkout without `pip install -e .` — fine for
41
+ # one-off tests, just don't claim a real version.
42
+ __version__ = "0.0.0+unknown"
43
+
44
+ __all__ = [
45
+ "Board", "BOARDS", "list_boards",
46
+ "OSImage", "OSES", "list_oses", "resolve_image",
47
+ "NodeConfig",
48
+ "build", "supports",
49
+ ]