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.
- py_pi_bake-0.0.4/.github/workflows/workflow.yml +73 -0
- py_pi_bake-0.0.4/.gitignore +12 -0
- py_pi_bake-0.0.4/LICENSE +21 -0
- py_pi_bake-0.0.4/PKG-INFO +177 -0
- py_pi_bake-0.0.4/README.md +151 -0
- py_pi_bake-0.0.4/ROADMAP.md +124 -0
- py_pi_bake-0.0.4/pyproject.toml +54 -0
- py_pi_bake-0.0.4/setup.cfg +4 -0
- py_pi_bake-0.0.4/src/pi_bake/__init__.py +49 -0
- py_pi_bake-0.0.4/src/pi_bake/alpine.py +375 -0
- py_pi_bake-0.0.4/src/pi_bake/bake.py +74 -0
- py_pi_bake-0.0.4/src/pi_bake/boards.py +77 -0
- py_pi_bake-0.0.4/src/pi_bake/cli.py +243 -0
- py_pi_bake-0.0.4/src/pi_bake/config.py +149 -0
- py_pi_bake-0.0.4/src/pi_bake/download.py +107 -0
- py_pi_bake-0.0.4/src/pi_bake/oses.py +167 -0
- py_pi_bake-0.0.4/src/pi_bake/raspbian.py +49 -0
- py_pi_bake-0.0.4/src/py_pi_bake.egg-info/PKG-INFO +177 -0
- py_pi_bake-0.0.4/src/py_pi_bake.egg-info/SOURCES.txt +23 -0
- py_pi_bake-0.0.4/src/py_pi_bake.egg-info/dependency_links.txt +1 -0
- py_pi_bake-0.0.4/src/py_pi_bake.egg-info/entry_points.txt +2 -0
- py_pi_bake-0.0.4/src/py_pi_bake.egg-info/top_level.txt +1 -0
- py_pi_bake-0.0.4/tests/test_apkovl.py +228 -0
- py_pi_bake-0.0.4/tests/test_catalogs.py +73 -0
- py_pi_bake-0.0.4/tests/test_config.py +82 -0
|
@@ -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.
|
py_pi_bake-0.0.4/LICENSE
ADDED
|
@@ -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,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
|
+
]
|