33"""
44BAUER GROUP XPD-RPIImage - Pages site + RPi Imager catalog renderer.
55
6+ Loads HTML templates from disk (site/*.tmpl) and substitutes {{placeholders}}.
7+ No Jinja or other heavy deps - keeps the Pages workflow dependency-free
8+ beyond what the build scripts already need.
9+
610Input:
7- --manifests DIR directory with one `*.manifest.json` per variant
8- (produced by build.yml's checksum step)
9- --tag vX.Y.Z tag to display on the landing page
10- --repo owner/name e.g. bauer-group/XPD-RPIImage
11- --out DIR destination directory for the rendered site
11+ --manifests DIR directory with one *.manifest.json per variant
12+ --tag vX.Y.Z release tag to display
13+ --repo owner/name
14+ --catalog-url URL public URL of the rpi-imager.json
15+ --templates DIR directory containing index.html.tmpl + card.html.tmpl
16+ --out DIR destination directory for the rendered site
1217
1318Output (in --out):
1419 rpi-imager.json catalog consumed by RPi Imager's Custom Repository
15- index.html human-facing landing page (rich info + direct flash links)
16- styles.css minimal, framework-free styling
17- .nojekyll disables Jekyll on GitHub Pages
20+ index.html human-facing landing page
21+ .nojekyll disables Jekyll on GitHub Pages (we ship pre-rendered HTML)
1822
19- CNAME is NOT rendered here - the workflow copies it from site/CNAME so the
20- custom domain lives in git instead of being re-generated on every run.
23+ `styles.css` and `CNAME` are copied by the workflow, not rendered here.
2124"""
2225from __future__ import annotations
2326
3033from typing import Any
3134
3235
33- # RPi Imager devices names - mapping our JSON `targets` enum to Imager's
34- # board identifiers. Entries left out here will be hidden on unknown boards.
36+ # RPi Imager device names - maps our JSON targets to Imager's identifiers.
3537TARGET_TO_IMAGER_DEVICE = {
3638 "rpi4" : "pi4-64bit" ,
3739 "rpi5" : "pi5" ,
4143
4244
4345def load_manifests (directory : Path ) -> list [dict [str , Any ]]:
44- """Read every `*.manifest.json` in `directory`, sorted by variant name."""
4546 paths = sorted (directory .glob ("*.manifest.json" ))
4647 if not paths :
4748 print (f"error: no *.manifest.json in { directory } " , file = sys .stderr )
@@ -53,8 +54,21 @@ def load_manifests(directory: Path) -> list[dict[str, Any]]:
5354 return out
5455
5556
57+ def substitute (template : str , values : dict [str , str ]) -> str :
58+ """Replace every `{{key}}` with `values[key]`.
59+
60+ Values that are missing are kept as-is so typos are visible in the
61+ rendered output rather than silently expanding to empty. No nested
62+ substitution, no conditionals - if the template grows logic beyond
63+ this, promote to Jinja2 with a proper test suite.
64+ """
65+ out = template
66+ for key , value in values .items ():
67+ out = out .replace ("{{" + key + "}}" , value )
68+ return out
69+
70+
5671def render_rpi_imager_json (manifests : list [dict [str , Any ]], tag : str ) -> dict [str , Any ]:
57- """Produce the RPi Imager Custom Repository JSON."""
5872 os_list : list [dict [str , Any ]] = []
5973 for m in manifests :
6074 variant = m ["variant" ]
@@ -80,163 +94,76 @@ def render_rpi_imager_json(manifests: list[dict[str, Any]], tag: str) -> dict[st
8094 }
8195
8296
83- def render_index_html (manifests : list [dict [str , Any ]], tag : str , repo : str , catalog_url : str ) -> str :
84- """Produce the standalone landing page."""
97+ def render_card (template : str , manifest : dict [str , Any ], tag : str ) -> str :
98+ img = manifest ["image" ]
99+ sha256_url = img ["url" ] + ".sha256"
100+ # bgrpiimage-<variant>-vX.Y.Z.img.xz → bgrpiimage-<variant>-vX.Y.Z.manifest.json
101+ manifest_url = img ["url" ].rsplit (".img.xz" , 1 )[0 ] + ".manifest.json"
102+ values = {
103+ "variant" : html .escape (manifest ["variant" ]),
104+ "tag" : html .escape (tag ),
105+ "description" : html .escape (manifest .get ("description" ) or "" ),
106+ "hostname" : html .escape (manifest .get ("hostname" ) or "-" ),
107+ "targets" : html .escape (", " .join (manifest ["targets" ])),
108+ "download_mib" : f"{ img ['download_size' ] / 1024 / 1024 :,.1f} " ,
109+ "extract_mib" : f"{ img ['extract_size' ] / 1024 / 1024 :,.1f} " ,
110+ "release_date" : html .escape (manifest .get ("release_date" ) or "-" ),
111+ "download_sha256" : html .escape (img ["download_sha256" ]),
112+ "extract_sha256" : html .escape (img ["extract_sha256" ]),
113+ "image_url" : html .escape (img ["url" ]),
114+ "sha256_url" : html .escape (sha256_url ),
115+ "manifest_url" : html .escape (manifest_url ),
116+ }
117+ return substitute (template , values )
118+
119+
120+ def render_index_html (
121+ index_template : str ,
122+ card_template : str ,
123+ manifests : list [dict [str , Any ]],
124+ tag : str ,
125+ repo : str ,
126+ catalog_url : str ,
127+ ) -> str :
128+ cards = "\n " .join (render_card (card_template , m , tag ) for m in manifests )
85129 generated = datetime .now (timezone .utc ).strftime ("%Y-%m-%d %H:%M UTC" )
86- cards = []
87- for m in manifests :
88- variant = m ["variant" ]
89- img = m ["image" ]
90- targets = ", " .join (m ["targets" ])
91- download_mb = img ["download_size" ] / 1024 / 1024
92- extract_mb = img ["extract_size" ] / 1024 / 1024
93- card = f"""
94- <article class="card">
95- <header>
96- <h2>{ html .escape (variant )} </h2>
97- <span class="tag">{ html .escape (tag )} </span>
98- </header>
99- <p class="description">{ html .escape (m .get ('description' ) or '' )} </p>
100- <dl class="meta">
101- <dt>Hostname</dt><dd><code>{ html .escape (m .get ('hostname' ) or '-' )} </code></dd>
102- <dt>Targets</dt><dd>{ html .escape (targets )} </dd>
103- <dt>Download size</dt><dd>{ download_mb :,.1f} MiB</dd>
104- <dt>Extracted size</dt><dd>{ extract_mb :,.1f} MiB</dd>
105- <dt>Release date</dt><dd>{ html .escape (m .get ('release_date' ) or '-' )} </dd>
106- </dl>
107- <div class="checksums">
108- <div><span class="label">SHA-256 (.img.xz)</span><code>{ html .escape (img ['download_sha256' ])} </code></div>
109- <div><span class="label">SHA-256 (.img)</span><code>{ html .escape (img ['extract_sha256' ])} </code></div>
110- </div>
111- <div class="actions">
112- <a class="btn primary" href="{ html .escape (img ['url' ])} ">⬇ Download .img.xz</a>
113- <a class="btn" href="{ html .escape (img ['url' ])} .sha256">.sha256</a>
114- <a class="btn" href="{ html .escape (img ['url' ]).rsplit ('.img.xz' , 1 )[0 ]} .manifest.json">manifest.json</a>
115- </div>
116- </article>
117- """
118- cards .append (card )
119-
120- cards_html = "\n " .join (cards )
121- catalog_copy_id = "catalog-url"
122- safe_repo = html .escape (repo )
123- safe_tag = html .escape (tag )
124- safe_catalog = html .escape (catalog_url )
125- safe_generated = html .escape (generated )
126-
127- return f"""<!DOCTYPE html>
128- <html lang="en">
129- <head>
130- <meta charset="utf-8" />
131- <meta name="viewport" content="width=device-width, initial-scale=1" />
132- <title>BAUER GROUP RPIImage – latest release { safe_tag } </title>
133- <meta name="description" content="Declarative, reproducible Raspberry Pi OS images by BAUER GROUP. RPi Imager catalog + direct downloads." />
134- <link rel="stylesheet" href="styles.css" />
135- <link rel="icon" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Crect%20width%3D%22100%22%20height%3D%22100%22%20rx%3D%2220%22%20fill%3D%22%23e80d2e%22%2F%3E%3Ctext%20x%3D%2250%22%20y%3D%2268%22%20font-family%3D%22sans-serif%22%20font-size%3D%2258%22%20font-weight%3D%22900%22%20fill%3D%22white%22%20text-anchor%3D%22middle%22%3EB%3C%2Ftext%3E%3C%2Fsvg%3E" />
136- </head>
137- <body>
138- <header class="hero">
139- <div class="inner">
140- <h1>📦 BAUER GROUP <span class="accent">RPIImage</span></h1>
141- <p class="subtitle">Declarative, reproducible, CI-built Raspberry Pi OS images for production and development.</p>
142- <p class="release">Current release: <a href="https://github.com/{ safe_repo } /releases/tag/{ safe_tag } "><strong>{ safe_tag } </strong></a></p>
143- </div>
144- </header>
145-
146- <main>
147- <section class="pitch">
148- <h2>🛠 Add this repo to Raspberry Pi Imager</h2>
149- <ol class="steps">
150- <li>Install <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> (v1.8.5 or later).</li>
151- <li>Open it and go to <strong>Settings</strong> (the ⚙ icon) → <strong>Custom repository</strong>.</li>
152- <li>Paste the catalog URL below and close the settings dialog:
153- <div class="copyrow">
154- <code id="{ catalog_copy_id } ">{ safe_catalog } </code>
155- <button type="button" onclick="navigator.clipboard.writeText(document.getElementById('{ catalog_copy_id } ').innerText)">Copy</button>
156- </div>
157- </li>
158- <li>Restart Imager. Our variants now appear under <strong>Operating System → BAUER GROUP</strong>.</li>
159- <li>Pick your target device → pick an image → flash.</li>
160- </ol>
161-
162- <h3>🧷 Flashing a Compute Module (CM4 / CM5 eMMC)</h3>
163- <ol>
164- <li>Put the carrier board in <strong>rpiboot mode</strong>:
165- <ul>
166- <li><strong>CM4 IO-Board</strong>: fit the jumper on <code>J2</code> (Disable eMMC Boot).</li>
167- <li><strong>CM5 IO-Board</strong>: bridge the <code>nRPIBOOT</code> test pad (or set the fit-jumper where provided).</li>
168- </ul>
169- </li>
170- <li>Connect the <strong>USB-C slave port</strong> (not the host USB) to your machine.</li>
171- <li>Power the board. RPi Imager (≥ 1.8.5) detects the CM as <em>"RPi"</em> mass storage via its built-in <code>rpiboot</code>.</li>
172- <li>Flash as usual.</li>
173- <li>Remove the jumper, re-power, boot.</li>
174- </ol>
175- </section>
176-
177- <section class="variants">
178- <h2>🧩 Variants in this release</h2>
179- <div class="cards">
180- { cards_html }
181- </div>
182- </section>
183-
184- <section class="security">
185- <h2>🔐 Default credentials</h2>
186- <p>These images ship with <strong>demo credentials</strong> - safe only on an isolated lab network:</p>
187- <ul>
188- <li><code>admin</code> password → <code>12345678</code></li>
189- <li>WiFi PSK for <code>IOT @ BAUER-GROUP</code> → <code>12345678</code></li>
190- </ul>
191- <p>Bake real values at build time via <code>.env</code>, or change them post-flash with:</p>
192- <pre><code>sudo bgrpiimage-setup password
193- sudo bgrpiimage-setup wifi "MyNet" "s3cret" DE
194- sudo bgrpiimage-setup ip eth0 static 10.0.0.5/24 10.0.0.1 1.1.1.1</code></pre>
195- </section>
196-
197- <section class="meta-links">
198- <h2>📚 More</h2>
199- <ul>
200- <li><a href="https://github.com/{ safe_repo } ">Repository on GitHub</a></li>
201- <li><a href="https://github.com/{ safe_repo } /releases">All releases</a></li>
202- <li><a href="https://github.com/{ safe_repo } /blob/main/docs/hardware.md">Hardware reference</a></li>
203- <li><a href="https://github.com/{ safe_repo } /blob/main/docs/flash.md">Flashing guide</a></li>
204- <li><a href="https://github.com/{ safe_repo } /blob/main/docs/configuration.md">JSON configuration reference</a></li>
205- </ul>
206- </section>
207- </main>
208-
209- <footer>
210- <p>© BAUER GROUP · <a href="https://github.com/{ safe_repo } /blob/main/LICENSE">MIT</a> · generated { safe_generated } </p>
211- </footer>
212- </body>
213- </html>
214- """
130+ return substitute (index_template , {
131+ "tag" : html .escape (tag ),
132+ "repo" : html .escape (repo ),
133+ "catalog_url" : html .escape (catalog_url ),
134+ "cards" : cards ,
135+ "generated" : html .escape (generated ),
136+ })
215137
216138
217139def main () -> int :
218140 p = argparse .ArgumentParser (description = __doc__ )
219- p .add_argument ("--manifests" , required = True , type = Path )
220- p .add_argument ("--tag" , required = True )
221- p .add_argument ("--repo" , required = True )
222- p .add_argument ("--catalog-url" , required = True ,
223- help = "Public URL where rpi-imager.json will be served." )
224- p .add_argument ("--out" , required = True , type = Path )
141+ p .add_argument ("--manifests" , required = True , type = Path )
142+ p .add_argument ("--tag" , required = True )
143+ p .add_argument ("--repo" , required = True )
144+ p .add_argument ("--catalog-url" , required = True )
145+ p .add_argument ("--templates" , required = True , type = Path ,
146+ help = "directory with index.html.tmpl + card.html.tmpl" )
147+ p .add_argument ("--out" , required = True , type = Path )
225148 args = p .parse_args ()
226149
227150 args .out .mkdir (parents = True , exist_ok = True )
228151 manifests = load_manifests (args .manifests )
229152
153+ # --- rpi-imager.json -----------------------------------------------------
230154 catalog = render_rpi_imager_json (manifests , args .tag )
231155 (args .out / "rpi-imager.json" ).write_text (
232156 json .dumps (catalog , indent = 2 ) + "\n " , encoding = "utf-8"
233157 )
234158
235- html_text = render_index_html (manifests , args .tag , args .repo , args .catalog_url )
159+ # --- index.html ----------------------------------------------------------
160+ index_tmpl = (args .templates / "index.html.tmpl" ).read_text (encoding = "utf-8" )
161+ card_tmpl = (args .templates / "card.html.tmpl" ).read_text (encoding = "utf-8" )
162+ html_text = render_index_html (index_tmpl , card_tmpl , manifests ,
163+ args .tag , args .repo , args .catalog_url )
236164 (args .out / "index.html" ).write_text (html_text , encoding = "utf-8" )
237165
238- # .nojekyll disables the default Jekyll build on GitHub Pages - we ship
239- # pre-rendered HTML and don't want Jekyll to mangle it.
166+ # --- .nojekyll -----------------------------------------------------------
240167 (args .out / ".nojekyll" ).write_text ("" , encoding = "utf-8" )
241168
242169 print (f"wrote { len (manifests )} variant(s) into { args .out } " , file = sys .stderr )
0 commit comments