Files
LithosAnanke/site/com/build.py
T
Claude be2c204d56 site(com): Downloads and Documents, built from a release; the nav links; the release's SHA256SUMS covers every file (#126, #157) [skip ci]
The design canvas (v17) gains the Documents artboard -- the Book of Fabric and
the Dictionary of v4 first, then the book, the three volumes and the API
reference, each with its SHA-256, and the SSRN paper -- and Downloads and
Documents in every .com nav; Downloads' reading list becomes a pointer to
Documents. site/com/canvas/ copied from it.

site/com/build.py --release TAG --sums SHA256SUMS builds Downloads and
Documents from the release ([TAG], [RELEASE]/<file>, [SHA:<file>]; a file the
release's SHA256SUMS lacks fails the build). Without a release it builds Home,
Try it and Inquiries exactly as before -- byte for byte -- with no link to the
two pages, so they go live only with alpha.4. install.sh installs them when
built.

scripts/nightly.sh gitea-release: the release's SHA256SUMS now covers every
file it attaches -- the images' sums, then the PDFs and the Root CA's
certificate -- so `sha256sum -c SHA256SUMS` checks all of it and Documents
finds each sum. The Nexus artifacts keep their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2
2026-11-10 11:38:50 +00:00

145 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: LicenseRef-Starship-1.0
# Copyright (c) 2023-2026 Robert A. James
"""build.py -- the starshipos-f18.com XWiki pages, made from the "StarshipOS.com" design canvas.
site/com/build.py [--release TAG --sums SHA256SUMS]
canvas/{Main,Inquiries,TryIt}.dc.html -> {Main,Inquiries,TryIt}.WebHome.xwiki,
and with a release, canvas/{Downloads,Documents}.dc.html too
The canvas is the source: its artboards are copied into canvas/ as they are, and this turns each into
one {{html}} block (which needs a page author with script rights). What changes on the way:
- the accent hole becomes its default colour, and the canvas's links become the wiki's pages
(Main.dc.html -> /bin/view/Main/, ...) and the real domains (starshipos.org -> starshipos-f18.org);
- the two images become attachments of Main.WebHome (badge.jpg, captain-bob.jpg -- web-size copies
of the canvas's uploads);
- Try it: the drawn console becomes the real one, an iframe of try.starshipos-f18.com;
- the inquiry form posts to the .org's PublicInquiry, which files it under Inquiries there
(admin-only, site/org/inquiries); the announcements form, which has no server, sends by e-mail;
- Downloads and Documents (#126, #157) are built from a release: --release TAG and the release's
SHA256SUMS fill in [TAG], [RELEASE]/<file> (the file on the Gitea release) and [SHA:<file>].
Without a release they are not built, and the other pages' links to them are taken out, so
nothing on the site points at a page that is not there (they go live with alpha.4);
install.sh puts the pages and the images on the wiki.
"""
import os
import re
import sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import colorlock # noqa: E402 the design's colours over the wiki's high-contrast theme
ACCENT = "#F5891F"
FONTS = ('<link rel="preconnect" href="https://fonts.googleapis.com">\n'
'<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700'
'&amp;family=Exo+2:wght@600;700&amp;display=swap" rel="stylesheet">')
ORG = "https://starshipos-f18.org/"
PAGES = ["Main", "Inquiries", "TryIt"]
RELEASE_PAGES = ["Downloads", "Documents"]
RELEASES = "https://gitea.starshipos-f18.org/rajames/LithosAnanke/releases/download/"
IMAGES = {"/_blob/5b564d54d98e02ad600c913a9a1888f2": "badge.jpg",
"/_blob/5483451f292782a8b366e437181c4bcf": "captain-bob.jpg"}
CONSOLE = '''<div style="background: #050A14; border: 1px solid #34486A; border-radius: 8px; overflow: hidden">
<div style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 20px; border-bottom: 1px solid #22324D; background: #0E182B; font-size: 13px; color: #A3AEC2">
<span style="display: flex; align-items: center; gap: 10px"><span aria-hidden="true" style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #4FD1B0"></span>StarshipOS &#183; amd64 &#183; your session</span>
<span>try.starshipos-f18.com</span>
</div>
<iframe src="https://try.starshipos-f18.com/" title="StarshipOS console" style="display: block; width: 100%; height: 30em; border: 0; background: #050A14"></iframe>
</div>
<p style="margin: 12px 0 0; font-size: 13px; color: #A3AEC2">If the console does not connect, every machine is in use: try again in a few minutes.</p>'''
def body(src, pages):
m = re.search(r"<x-dc>(.*)</x-dc>", src, re.S)
html = re.sub(r"<helmet>.*?</helmet>", "", m.group(1), flags=re.S).strip()
html = html.replace("{{accent}}", ACCENT)
# a page not built this time is not linked to: its <a> taken out (the nav, the footers)
for p in RELEASE_PAGES:
if p not in pages:
html = re.sub(r'<a href="%s\.dc\.html"[^>]*>[^<]*</a>\n?' % p, "", html)
for p in pages:
html = html.replace('href="%s.dc.html"' % p, 'href="/bin/view/%s/"' % p)
html = html.replace('href="https://starshipos.org"', 'href="%s"' % ORG)
html = html.replace(">starshipos.org</a>", ">starshipos-f18.org</a>")
html = html.replace("on starshipos.org.", "on starshipos-f18.org.")
for blob, name in IMAGES.items():
html = html.replace('src="%s"' % blob, 'src="/bin/download/Main/WebHome/%s"' % name)
html = html.replace("min-height: 100vh", "min-height: 0") # inside the wiki's page, not a screen
return html
def try_it(html):
start = html.index('<div style="background: #050A14')
end = html.index("</p>", html.index("[LIVE CONSOLE]")) + len("</p>")
return html[:start] + CONSOLE + html[end:]
def forms(html, page):
if page == "Main":
html = html.replace('<form style="flex: 1 1 420px; display: flex; flex-wrap: wrap; gap: 12px" aria-label="Subscribe to announcements">',
'<form action="mailto:info@starshipos.com?subject=Subscribe%20to%20launch%20announcements" method="post" enctype="text/plain" style="flex: 1 1 420px; display: flex; flex-wrap: wrap; gap: 12px" aria-label="Subscribe to announcements">')
html = html.replace('<input id="sub-email" type="email"', '<input id="sub-email" name="email" type="email" required')
html = html.replace('<button type="button" style="min-height: 48px; padding: 0 24px',
'<button type="submit" style="min-height: 48px; padding: 0 24px')
if page == "Inquiries":
html = html.replace('<form aria-label="Inquiry form"',
'<form action="https://starshipos-f18.org/bin/view/PublicInquiry/" method="post" aria-label="Inquiry form"')
# a field people cannot see; a robot fills it, and PublicInquiry keeps nothing it sends
html = html.replace('<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px">',
'<div aria-hidden="true" style="position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden">'
'<label for="q-website">Leave this empty</label><input id="q-website" name="website" type="text" tabindex="-1" autocomplete="off"></div>\n'
'<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px">', 1)
for i, n in (("q-name", "name"), ("q-email", "email"), ("q-org", "organization"), ("q-topic", "topic"),
("q-msg", "message"), ("q-list", "announcements")):
html = html.replace('id="%s"' % i, 'id="%s" name="%s"' % (i, n), 1)
html = html.replace('<button type="button" style="min-height: 48px; padding: 0 28px',
'<button type="submit" style="min-height: 48px; padding: 0 28px')
html = html.replace("Replies usually within a few days.",
"Replies usually within a few days.")
return html
def release(html, tag, sums):
"""[TAG], [RELEASE]/<file> and [SHA:<file>] from the release; a file it has not is an error."""
html = html.replace("[TAG]", tag)
html = html.replace("[RELEASE]/", RELEASES + tag + "/")
def sha(m):
if m.group(1) not in sums:
sys.exit("build: %s is not in the release's SHA256SUMS" % m.group(1))
return sums[m.group(1)]
return re.sub(r"\[SHA:([^\]]+)\]", sha, html)
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--release", help="the release's tag, e.g. v4.0.0-alpha.4")
ap.add_argument("--sums", help="the release's SHA256SUMS")
a = ap.parse_args()
if bool(a.release) != bool(a.sums):
sys.exit("build: --release and --sums go together")
sums = {}
if a.sums:
for l in open(a.sums).read().split("\n"):
f = l.split()
if len(f) == 2:
sums[f[1].lstrip("*").lstrip("./")] = f[0]
os.chdir(os.path.dirname(os.path.abspath(__file__)))
pages = PAGES + (RELEASE_PAGES if a.release else [])
for p in pages:
html = forms(body(open("canvas/%s.dc.html" % p).read(), pages), p)
if p in RELEASE_PAGES:
html = release(html, a.release, sums)
if p == "TryIt":
html = try_it(html)
html = colorlock.lock(html, '<div style="background: #0A1222')
assert "{{" not in html.replace(colorlock.STYLE, "") and "[SSRN" not in html and ".dc.html" not in html and "_blob" not in html, p
assert "[TAG]" not in html and "[RELEASE]" not in html and "[SHA:" not in html, p
with open("%s.WebHome.xwiki" % p, "w") as f:
f.write(colorlock.GUEST_CHROME + '{{html clean="false"}}\n%s\n%s\n{{/html}}\n' % (FONTS, html))
print("wrote", ", ".join("%s.WebHome.xwiki" % p for p in pages))