Files
LithosAnanke/site/org/build.py
T
Claude 038556bb3e
Build / Image MILKV (push) Canceled after 0s
Build / Image PC (push) Canceled after 0s
Build / Image RASPI (push) Canceled after 0s
Build / v4 tests (32- and 64-bit cells) (push) Canceled after 0s
site: visitors see the design alone, not XWiki's chrome
Captain Bob, 2026-11-07: "Menu bar". For guests only, the .com pages and
the .org landing page hide XWiki's top bar, breadcrumb, title block, page
menu, panels, footer and tour, and the design runs the full width (a
velocity check on $isGuest; the stylesheet in a literal block). Logged in,
the wiki is all there. Checked on a phone and a desktop: the chrome is
hidden on Main, Inquiries and TryIt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GE3SDvQn5EoxdGufk84zz2
2026-11-07 13:51:15 +00:00

338 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
"""build.py -- the .org XWiki's three pages in the starshipos-f18.com look (the "StarshipOS.com"
design canvas: navy #0A1222, accent #F5891F, JetBrains Mono, Exo 2 headings), from one source.
site/org/build.py writes Main.WebHome.xwiki, Admin.WebHome.xwiki,
Contributors.WebHome.xwiki (each one {{html}} block)
site/org/build.py --canvas <dir>
also writes <dir>/project/Org*.dc.html, the same pages as
design-canvas artboards (status badges drawn as placeholders:
a canvas loads nothing from Gitea)
The {{html}} macro needs a page author with script rights. install.sh puts the pages on the wiki.
"""
import os
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
GITEA = "https://gitea.starshipos-f18.org/rajames/LithosAnanke"
COM = "https://starshipos-f18.com/"
ISSUES = GITEA + "/issues?state=open"
ACCENT = "#F5891F"
TEAL = "#4FD1B0"
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">')
H = "font-family: 'Exo 2', sans-serif"
CARD = "background: #111C30; border: 1px solid #22324D; border-radius: 6px; padding: 28px"
EYEBROW = ("margin: 0 0 12px; font-size: 13px; letter-spacing: 0.14em; text-transform: uppercase; "
"color: {accent}")
BTN = ("display: inline-block; padding: 14px 24px; background: {accent}; color: #0A1222; "
"font-weight: 700; text-decoration: none; border-radius: 4px")
BTN2 = ("display: inline-block; padding: 14px 24px; border: 1px solid #34486A; color: #EEF2F7; "
"text-decoration: none; border-radius: 4px")
WRAP = "max-width: 1120px; margin: 0 auto"
WORKFLOWS = { # file -> (branch for the badge, what it is)
"acceptance.yml": (None, "Acceptance -- the gate on every PR into master"),
"build.yml": ("StarForth-v4.0.0", "Build -- the quick check on StarForth-v4.0.0"),
"nightly.yml": ("master", "Nightly -- acceptance, images to Nexus, proofs (03:00 UTC)"),
"nightly-docs.yml": ("master", "Nightly documents (04:00 UTC)"),
"runner-image.yml": ("master", "Runner image"),
"release.yml": (None, "Release -- a v&lt;version&gt; tag"),
}
def badge(wf, canvas):
branch = WORKFLOWS[wf][0]
if canvas:
return ('<span style="display: inline-block; padding: 2px 10px; border: 1px dashed #34486A; '
'border-radius: 4px; font-size: 12px; color: #A3AEC2">[LIVE STATUS]</span>')
q = "?branch=" + branch if branch else ""
return ('<img src="%s/actions/workflows/%s/badge.svg%s" alt="%s status" '
'style="display: block; height: 20px">' % (GITEA, wf, q, wf))
def status_rows(wfs, canvas, runs=True):
rows = []
for wf in wfs:
link = (' <a href="%s/actions?workflow=%s" style="font-size: 13px">runs</a>' % (GITEA, wf)
if runs else "")
rows.append('<div style="display: flex; flex-wrap: wrap; align-items: center; '
'justify-content: space-between; gap: 12px; padding: 14px 0; '
'border-top: 1px solid #22324D">'
'<span style="color: #C8D1DE; font-size: 14px">%s</span>'
'<span style="display: flex; align-items: center; gap: 16px">%s%s</span></div>'
% (WORKFLOWS[wf][1], badge(wf, canvas), link))
return "\n".join(rows)
def nav(here, links):
items = []
for label, href, key in links:
cur = ' aria-current="page"' if key == here else ""
style = ("color: #EEF2F7; text-decoration: none; border-bottom: 2px solid {accent}"
if key == here else "color: #C8D1DE; text-decoration: none")
items.append('<a href="%s"%s style="%s">%s</a>' % (href, cur, style, label))
items.append('<a href="%s" style="color: {accent}; text-decoration: none">starshipos-f18.com &#8594;</a>'
% COM)
return ('<header style="border-bottom: 1px solid #22324D">\n'
'<nav aria-label="Main" style="%s; padding: 20px 24px; display: flex; flex-wrap: wrap; '
'align-items: center; justify-content: space-between; gap: 16px">\n'
'<a href="%s" style="%s; font-weight: 700; font-size: 22px; color: #EEF2F7; '
'text-decoration: none; letter-spacing: 0.02em">starship<span style="color: {accent}">OS</span>'
'<span style="font-size: 14px; color: #A3AEC2; margin-left: 6px">.org</span></a>\n'
'<div style="display: flex; flex-wrap: wrap; gap: 24px; font-size: 14px">%s</div>\n'
'</nav>\n</header>' % (WRAP, links[0][1], H, "".join(items)))
FOOTER = ('<footer style="border-top: 1px solid #22324D">\n'
'<div style="%s; padding: 32px 24px; display: flex; flex-wrap: wrap; gap: 16px 32px; '
'justify-content: space-between; font-size: 13px; color: #A3AEC2">\n'
'<span>&#169; 2026 Robert Allan James &#183; A Starship Foundation project (in formation)</span>\n'
'<span style="display: flex; flex-wrap: wrap; gap: 24px"><a href="%s" style="color: #C8D1DE">Source</a>'
'<a href="%s" style="color: #C8D1DE">starshipos-f18.com</a></span>\n</div>\n</footer>' % (WRAP, GITEA, COM))
def hero(eyebrow, title, text, buttons=""):
return ('<section style="%s; padding: 80px 24px 48px">\n'
'<p style="%s">%s</p>\n'
'<h1 style="margin: 0 0 20px; %s; font-weight: 700; font-size: clamp(36px, 5vw, 56px); '
'line-height: 1.08">%s</h1>\n'
'<p style="margin: 0 0 32px; max-width: 680px; font-size: 18px; color: #C8D1DE">%s</p>\n%s'
'</section>' % (WRAP, EYEBROW.replace("{accent}", TEAL), eyebrow, H, title, text,
('<div style="display: flex; flex-wrap: wrap; gap: 16px">%s</div>\n' % buttons)
if buttons else ""))
def section(eyebrow, title, body, pad="48px 24px"):
return ('<section style="%s; padding: %s">\n<p style="%s">%s</p>\n'
'<h2 style="margin: 0 0 32px; %s; font-size: 34px; line-height: 1.15; max-width: 760px">%s</h2>\n'
'%s\n</section>' % (WRAP, pad, EYEBROW, eyebrow, H, title, body))
def card(title, sub, body):
return ('<article style="%s">\n<h3 style="margin: 0 0 4px; %s; font-size: 22px">%s</h3>\n'
'<p style="margin: 0 0 14px; font-size: 13px; color: {accent}">%s</p>\n%s\n</article>'
% (CARD, H, title, sub, body))
def grid(items, min_w=280):
return ('<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(%dpx, 1fr)); '
'gap: 24px">\n%s\n</div>' % (min_w, "\n".join(items)))
def links(items):
return ('<ul style="margin: 0; padding-left: 20px; color: #C8D1DE; font-size: 15px; '
'display: flex; flex-direction: column; gap: 8px">%s</ul>'
% "".join("<li>%s</li>" % i for i in items))
def a(href, text):
return '<a href="%s">%s</a>' % (href, text)
def code(s):
return '<code style="color: #EEF2F7; background: #0A1222; padding: 1px 6px; border-radius: 3px">%s</code>' % s
def page_main(canvas, url):
links_ = [("Home", url("Main"), "Main"), ("Contributors", url("Contributors"), "Contributors")]
glance = [("v2.1.0", "LithosAnanke, the UEFI microkernel"), ("v4", "StarForth, the F18 engine"),
("3 ISAs", "amd64 &#183; aarch64 &#183; riscv64"), ("538", "POST checks on every boot")]
strip = ('<section aria-label="At a glance" style="border-top: 1px solid #22324D; border-bottom: 1px solid '
'#22324D; background: #0E182B">\n<div style="%s; padding: 32px 24px; display: grid; '
'grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 24px">%s</div>\n</section>'
% (WRAP, "".join('<div><div style="%s; font-size: 32px; font-weight: 700; color: {accent}">%s</div>'
'<div style="font-size: 14px; color: #A3AEC2">%s</div></div>' % (H, n, t)
for n, t in glance)))
project = section("The project", "One repository, one process, one line to master.", grid([
card("The source", "kernel, StarForth, proofs, documents",
'<p style="margin: 0 0 16px; color: #C8D1DE">Everything in one repository: LithosAnanke, '
'StarForth v3 and v4, the Isabelle/HOL proofs and the books.</p>' + a(GITEA, "Browse it &#8594;")),
card("Issues", "found, ruled, built, verified",
'<p style="margin: 0 0 16px; color: #C8D1DE">Every piece of work starts as an issue with its '
'evidence, and is ruled before it is built.</p>' + a(GITEA + "/issues", "Open issues &#8594;")),
card("Pull requests", "the only way into master",
'<p style="margin: 0 0 16px; color: #C8D1DE">master takes nothing but a pull request that '
'passed the acceptance protocol.</p>' + a(GITEA + "/pulls", "Pull requests &#8594;")),
]), pad="96px 24px 48px")
accept = section("How a change is accepted", "Booted on three architectures, every time.",
'<p style="margin: 0 0 24px; max-width: 760px; color: #C8D1DE">The kernel boots in QEMU '
'on amd64, aarch64 and riscv64 to its prompt, its power-on self-test passing every check; '
'the PC stick boots loaded the way a PC loads it; the 32-bit build passes its tests. A '
'developer runs it by hand, a pull request runs it by itself, and the nightly runs it '
'again before it publishes.</p>\n<div style="%s">%s</div>'
% (CARD, status_rows(["acceptance.yml", "nightly.yml", "nightly-docs.yml"], canvas, False)))
contrib = ('<section style="%s; padding: 48px 24px 96px">\n<div style="%s; padding: 40px; display: flex; '
'flex-wrap: wrap; gap: 32px; align-items: center">\n<div style="flex: 999 1 480px; min-width: 0">\n'
'<p style="%s">Contributors</p>\n<h2 style="margin: 0 0 12px; %s; font-size: 30px">'
'Working on StarshipOS?</h2>\n<p style="margin: 0; color: #C8D1DE">Contributors have a dashboard '
'of their own: the method, the acceptance commands, the open work. Log in to see it.</p>\n</div>\n'
'<div style="flex: 1 1 220px; display: flex; flex-direction: column; gap: 12px">\n'
'<a href="%s" style="%s; text-align: center">Log in</a>\n'
'<a href="%s" style="%s; text-align: center">Contributors\' dashboard</a>\n</div>\n</div>\n</section>'
% (WRAP, CARD, EYEBROW, H, url("login"), BTN, url("Contributors"), BTN2))
return "\n".join([
nav("Main", links_),
hero("ok&gt; the working home", "Where StarshipOS is built.",
"StarshipOS is an operating system with no other operating system underneath it. LithosAnanke, a "
"UEFI microkernel, boots straight from the firmware and runs StarForth, a FORTH virtual machine, "
"as its only runtime. This wiki holds its design, its record and its dashboards; the public face "
"is " + a(COM, "starshipos-f18.com") + ".",
'<a href="%s" style="%s">Browse the source</a><a href="%s" style="%s">Try it in a browser</a>'
% (GITEA, BTN, COM + "bin/view/TryIt/", BTN2)),
strip, project, accept, contrib, FOOTER])
def page_admin(canvas, url):
links_ = [("Home", url("Main"), "Main"), ("Contributors", url("Contributors"), "Contributors"),
("Admin", url("Admin"), "Admin")]
ci = section("CI", "Every workflow, live.", '<div style="%s">%s</div>' % (CARD, status_rows(list(WORKFLOWS), canvas)),
pad="0 24px 48px")
work = card("Work", "Gitea", links([
a(GITEA + "/pulls?state=open", "Open pull requests"),
a(ISSUES, "Open issues") + " &#8212; " + a(ISSUES + "&labels=23", "in progress") + ", "
+ a(ISSUES + "&labels=20", "blocked") + ", " + a(ISSUES + "&labels=21", "needs acceptance"),
a(GITEA + "/milestones", "Milestones") + " and " + a(GITEA + "/projects", "the project board"),
a(GITEA + "/settings/branches", "Branch protection") + " &#8212; master: no pushes, merges only after Acceptance",
a(GITEA + "/settings/actions/secrets", "Actions secrets") + " &#8212; " + code("NEXUS_CI_PASSWORD") + ", "
+ code("PACKAGES_TOKEN") + ", " + code("CAPSULE_SIGN_KEY"),
a(GITEA + "/settings/actions/runners", "Runners"),
a("https://gitea.starshipos-f18.org/user/settings/applications", "Access tokens")
+ " &#8212; revoke what a session no longer needs"]))
arts = card("Artifacts", "Nexus and packages", links([
a("https://nexus.starshipos-f18.org/#browse/browse:starshipos-snapshots:org%2Fstarship", "Snapshots")
+ " &#8212; " + code("kernelImage") + " (pc, raspi5, milkv, logs), " + code("documentation")
+ " (book, vol1&#8211;vol3, api)",
a("https://nexus.starshipos-f18.org/#browse/browse:starshipos-releases:org%2Fstarship", "Releases"),
a("https://gitea.starshipos-f18.org/rajames/-/packages", "Container images") + " &#8212; "
+ code("lithosananke-runner"), ]))
sites = card("Sites", "and the wiki itself", links([
a(COM, "starshipos-f18.com") + " and " + a(COM + "bin/view/TryIt/", "Try it"),
a(url("Contributors"), "Contributors' dashboard"),
a(url("admin"), "Wiki administration") + " &#8212; users, the group " + code("XWiki.StarshipContributors")
+ ", rights"]))
return "\n".join([
nav("Admin", links_),
hero("Administrator only", "Admin dashboard.",
"Seen by " + code("XWiki.rajames") + " alone: the Admin space allows view and edit to that "
"account and nobody else."),
ci, section("Run the project", "Work, artifacts, sites.", grid([work, arts, sites], 300), pad="0 24px 96px"),
FOOTER])
def page_contrib(canvas, url):
links_ = [("Home", url("Main"), "Main"), ("Contributors", url("Contributors"), "Contributors")]
steps = [("Found", "An issue says what is wrong or missing, with the evidence: a log, a line of code."),
("Ruled", "Captain Bob decides what is to be done. Nothing is built without a ruling, and no fix "
"nobody asked for."),
("Built", "On the working branch, " + code("StarForth-v4.0.0") + ". C99, no GNU extensions; compose "
"in FORTH before adding a C word."),
("Verified", "The acceptance protocol, below; its logs are committed under " + code("logs/") + "."),
("Merged", "A pull request into " + code("master") + ", which runs acceptance by itself. master takes "
"nothing else.")]
method = section("How we work", "The Captain Bob method.",
'<p style="margin: 0 0 32px; max-width: 720px; color: #C8D1DE">Neither agile nor waterfall: '
'one process from the developer\'s desk to master.</p>\n' + grid(
['<article style="%s"><div style="%s; font-size: 28px; font-weight: 700; color: {accent}">'
'%d</div><h3 style="margin: 4px 0 8px; %s; font-size: 22px">%s</h3>'
'<p style="margin: 0; color: #C8D1DE; font-size: 14px">%s</p></article>'
% (CARD, H, i + 1, H, t, d) for i, (t, d) in enumerate(steps)], 190), pad="0 24px 48px")
cmds = ("make -f kernel/Makefile ARCH=amd64 clean qemu\n"
"make -f kernel/Makefile ARCH=aarch64 clean qemu\n"
"make -f kernel/Makefile ARCH=riscv64 clean qemu\n"
"make -f kernel/Makefile ARCH=amd64 clean qemu-stick-high\n"
"make -C v4 test-32")
accept = section("The acceptance protocol", "In this order, one QEMU at a time, in the foreground.",
'<pre style="margin: 0 0 24px; background: #0E182B; border: 1px solid #22324D; '
'border-radius: 6px; padding: 24px; overflow-x: auto; font-size: 14px; color: #EEF2F7">'
'%s</pre>\n<p style="margin: 0 0 24px; max-width: 760px; color: #C8D1DE">Each boot reaches '
'%s with %s and every POST check passing, and the three architectures report the same '
'%s. %s installs everything this needs.</p>\n<div style="%s">%s</div>'
% (cmds, code("[zuse@Hera] ok&gt;"), code("POST: PASSED"), code("dict_hash"),
code("make ARCH=amd64,aarch64,riscv64 init"), CARD,
status_rows(["acceptance.yml", "build.yml", "nightly.yml"], canvas)))
where = card("Where things are", "start here", links([
a(GITEA, "The repository") + " &#8212; " + code("README.md") + ", " + code("docs/README.md")
+ "; the design record is " + code("docs/fabric/FABRIC-3.md"),
a(GITEA + "/issues/new/choose", "New issue") + " &#8212; the forms: bug, work, ruling, board",
a(ISSUES + "&labels=2", "Open features") + " and " + a(ISSUES + "&labels=1", "bugs"),
a(ISSUES + "&milestone=2", "Board bring-up") + " &#8212; " + code("boards/BRINGUP.md"),
"Before touching a capsule: read " + code("experiments/bare_metal/README.md")
+ " &#8212; the block ranges are shared by every capsule"]))
rules = card("Rules that bite", "learned the hard way", links([
"No new branch, no " + code("git stash") + ", without Captain Bob's say-so.",
"Never modify a registered, tested FORTH word to fix it: the problem is in the caller.",
"ACL policy lives in " + code("ACL.4th") + ", not in C.",
"Tags are sacred: each has the logs proving its state."]))
return "\n".join([
nav("Contributors", links_),
hero("Contributors", "Contributors' dashboard.",
"For the members of " + code("XWiki.StarshipContributors") + " and the administrator. "
"Ask Captain Bob to be added."),
method, accept, section("Reference", "Where things are, and what not to do.",
grid([where, rules], 360), pad="0 24px 96px"),
FOOTER])
PAGES = { # name -> (builder, title, canvas file, canvas height)
"Main": (page_main, "StarshipOS", "OrgHome.dc.html", 2300),
"Admin": (page_admin, "Admin Dashboard", "OrgAdmin.dc.html", 1900),
"Contributors": (page_contrib, "Contributors' Dashboard", "OrgContributors.dc.html", 2500),
}
def wiki_url(key):
return {"login": "/bin/login/XWiki/XWikiLogin?xredirect=/bin/view/Contributors/",
"admin": "/bin/admin/XWiki/XWikiPreferences"}.get(key, "/bin/view/%s/" % key)
def canvas_url(key):
files = {k: v[2] for k, v in PAGES.items()}
return files.get(key, "https://starshipos-f18.org" + wiki_url(key))
def body(name, canvas):
html = PAGES[name][0](canvas, canvas_url if canvas else wiki_url)
return ('<div style="background: #0A1222; color: #EEF2F7; font-family: \'JetBrains Mono\', ui-monospace, '
'monospace; font-size: 16px; line-height: 1.6">\n%s\n</div>' % html)
def write_wiki(name):
html = colorlock.lock(body(name, False).replace("{accent}", ACCENT), "<div style=\"background: #0A1222")
with open("%s.WebHome.xwiki" % name, "w") as f:
# the landing page is the public one: the design alone for visitors, as on the .com
chrome = colorlock.GUEST_CHROME if name == "Main" else ""
f.write(chrome + '{{html clean="false"}}\n%s\n%s\n{{/html}}\n' % (FONTS, html))
def write_canvas(name, root):
title, file, h = PAGES[name][1], PAGES[name][2], PAGES[name][3]
html = body(name, True).replace("{accent}", "{{accent}}")
doc = ('<!doctype html>\n<html lang="en">\n<head>\n<meta charset="utf-8">\n<title>StarshipOS.org &#8212; %s'
'</title>\n<script src="./support.js"></script>\n</head>\n<body>\n<x-dc>\n<helmet>\n%s\n<style>\n'
'body{margin:0;background:#0A1222}\na{color:#F5891F}a:hover{color:#FFB061}\n</style>\n</helmet>\n%s\n'
'</x-dc>\n<script type="text/x-dc" data-dc-script data-props=\'{"accent":{"editor":"color","default":'
'"#F5891F","options":["#F5891F","#5FD3C4","#8FB8FF"]},"$preview":{"width":1280,"height":%d}}\'>\n'
'class Component extends DCLogic {\n renderVals() {\n return { accent: this.props.accent ?? '
'\'#F5891F\' };\n }\n}\n</script>\n</body>\n</html>\n' % (title, FONTS, html, h))
os.makedirs(os.path.join(root, "project"), exist_ok=True)
with open(os.path.join(root, "project", file), "w") as f:
f.write(doc)
if __name__ == "__main__":
os.chdir(os.path.dirname(os.path.abspath(__file__)))
canvas_root = sys.argv[2] if len(sys.argv) > 2 and sys.argv[1] == "--canvas" else None
for n in PAGES:
write_wiki(n)
if canvas_root:
write_canvas(n, canvas_root)
print("wrote", ", ".join("%s.WebHome.xwiki" % n for n in PAGES),
"and the canvas artboards" if canvas_root else "")