Vibecoded CI/CD
53
.gitea/workflows/deploy-lean.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Build and Deploy Lean
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
# This pipeline owns the lean editor image: heavy (it builds lean4web
|
||||||
|
# with Mathlib), so it runs only when lean/** actually changes
|
||||||
|
paths:
|
||||||
|
- "lean/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: docker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install SSH client
|
||||||
|
run: |
|
||||||
|
apt-get update -y && apt-get install -y openssh-client
|
||||||
|
|
||||||
|
- name: Setup SSH inside container
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
# Add the container host to known_hosts
|
||||||
|
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
- name: Log in to registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
|
||||||
|
# Multi-stage: compilers/dev deps stay in the builder, the pushed image
|
||||||
|
# is the slim runtime (toolchain + client + Mathlib cache). Still a
|
||||||
|
# heavy build and a multi-GB push — expect it to take a while.
|
||||||
|
# Pin upstream with: docker build --build-arg LEAN4WEB_REF=v0.2.2 ...
|
||||||
|
- name: Build lean editor server
|
||||||
|
run: |
|
||||||
|
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-lean:latest ./lean
|
||||||
|
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-lean:latest
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.aranroig.com -u "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||||
|
cd /var/www/app/
|
||||||
|
docker compose pull lean
|
||||||
|
docker compose up -d lean
|
||||||
|
EOF
|
||||||
@@ -3,6 +3,10 @@ name: Build and Deploy Nuxt
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
# The Lean editor carries Mathlib and moves on its own schedule — its
|
||||||
|
# image is built and pushed by deploy-lean.yml, only when lean/** changes
|
||||||
|
paths-ignore:
|
||||||
|
- "lean/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -39,16 +43,13 @@ jobs:
|
|||||||
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-backend:latest ./backend
|
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-backend:latest ./backend
|
||||||
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-backend:latest
|
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-backend:latest
|
||||||
|
|
||||||
- name: Build lean backend
|
|
||||||
run: |
|
|
||||||
docker build -t git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-lean:latest ./lean
|
|
||||||
docker push git.aranroig.com/${{ secrets.REGISTRY_USER }}/mathew-lean:latest
|
|
||||||
|
|
||||||
- name: Copy files
|
- name: Copy files
|
||||||
run: |
|
run: |
|
||||||
scp docker-compose.yml deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/
|
scp docker-compose.yml deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/
|
||||||
scp nginx.conf deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/nginx.conf
|
scp nginx.conf deploy@${{ secrets.DEPLOY_HOST }}:/var/www/app/nginx.conf
|
||||||
|
|
||||||
|
# `compose pull` also pulls the lean image from the registry —
|
||||||
|
# deploy-lean.yml is what keeps that tag current
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: |
|
run: |
|
||||||
ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
|
ssh deploy@${{ secrets.DEPLOY_HOST }} << 'EOF'
|
||||||
@@ -57,4 +58,3 @@ jobs:
|
|||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d --remove-orphans
|
docker compose up -d --remove-orphans
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
36
AGENTS.md
@@ -63,10 +63,17 @@ its theme (live, same page), and the Goals/Messages pane.
|
|||||||
|
|
||||||
`lean/Dockerfile` still builds lean4web (upstream's real Lean + Mathlib per session;
|
`lean/Dockerfile` still builds lean4web (upstream's real Lean + Mathlib per session;
|
||||||
`--build-arg LEAN4WEB_REF=<tag|branch>` pins it) and runs its websocket server on port 8080;
|
`--build-arg LEAN4WEB_REF=<tag|branch>` pins it) and runs its websocket server on port 8080;
|
||||||
the compose `lean` service runs it. Upstream asks for a dedicated machine, and the image is
|
the compose `lean` service runs it (image `git.aranroig.com/syndria98/mathew-lean:latest` —
|
||||||
heavy (Mathlib). It runs `NO_BWRAP=true` — bubblewrap cannot nest in a container, so Docker
|
CI builds and pushes it, and deploys lean, only when `lean/**` changes: `deploy-lean.yml`;
|
||||||
itself is the sandbox; whoever reaches the port can run Lean. Build/run, TLS, security and
|
the regular deploy just pulls the tag). Upstream asks for a dedicated machine, so the build
|
||||||
projects: `lean/README.md`.
|
is **multi-stage**: dev tooling stays in the builder, and the pushed image is a slim runtime
|
||||||
|
of node + elan toolchain + built client + Mathlib's compiled cache (~11GB — the cache is
|
||||||
|
the bulk; upstream's second demo project `Stable` is dropped, its extra ~3GB toolchain was
|
||||||
|
unused). `--build-arg BAKE_MATHLIB=false` ships Mathlib *out* of the image (~4GB) into a
|
||||||
|
`.lake` volume, filled once at first start by `lean/entrypoint.sh` — see lean/README.md.
|
||||||
|
It runs `NO_BWRAP=true` — bubblewrap cannot nest in a container, so Docker itself is the
|
||||||
|
sandbox; whoever reaches the port can run Lean. Build/run, TLS, security and projects:
|
||||||
|
`lean/README.md`.
|
||||||
|
|
||||||
- `frontend/app/utils/lean/lsp.ts` — a minimal LSP JSON-RPC client over a websocket
|
- `frontend/app/utils/lean/lsp.ts` — a minimal LSP JSON-RPC client over a websocket
|
||||||
(request/response pairing, notification dispatch, and auto-ack of the server→client
|
(request/response pairing, notification dispatch, and auto-ack of the server→client
|
||||||
@@ -206,6 +213,24 @@ carry `<code>`/`<span>` markup are drawn with `v-html` through `th()`
|
|||||||
(`useAppLocale`), which escapes interpolated values. Adding a language = add a catalog
|
(`useAppLocale`), which escapes interpolated values. Adding a language = add a catalog
|
||||||
copy + an `APP_LANGUAGES` entry + the code in the backend's `SUPPORTED_LOCALES`.
|
copy + an `APP_LANGUAGES` entry + the code in the backend's `SUPPORTED_LOCALES`.
|
||||||
|
|
||||||
|
- **The same bundle also ships as a desktop app** (Tauri 2, `src-tauri/`):
|
||||||
|
`npm run desktop:dev` / `desktop:build` / `desktop:generate` / `desktop:icons`
|
||||||
|
in `frontend/` (needs Rust and the WebKitGTK stack; the shell, the scripts and
|
||||||
|
the build prerequisites are `src-tauri/README.md`). `nuxt.config.ts` changes
|
||||||
|
shape on one flag — `MATHEW_DESKTOP=1`, implied too by the `TAURI_ENV_*` the
|
||||||
|
Tauri CLI passes to its hooks: `ssr: false`, no `/api` proxy route, the
|
||||||
|
pre-paint script from `public/preboot.js` instead of inline — and writes the
|
||||||
|
files to `dist-desktop/public` (never over the web `.output/`), which is what
|
||||||
|
`frontendDist` reads. Nothing in `app/` is desktop code: `utils/desktop.ts`
|
||||||
|
asks at run time whether it is in a window (`__TAURI_INTERNALS__`) and only
|
||||||
|
then does the window's thing — outward links leave through the `opener`
|
||||||
|
plugin (`plugins/desktop.ts`), and the session token and interface language
|
||||||
|
mirror into localStorage (`keepTokenRemembered`, since a webview is not owed
|
||||||
|
a cookie jar for its own `tauri://localhost`). A desktop build bakes the API
|
||||||
|
base to `https://mathew.aranroig.com` (`NUXT_PUBLIC_API_BASE` overrides, e.g.
|
||||||
|
`http://localhost:5000` to test against this machine) and leaves `leanUrl`
|
||||||
|
empty — a window on a random laptop has no `localhost:8080` to find.
|
||||||
|
|
||||||
- `app/pages/` — `/` (search), `/wiki/[lang]/[slug]` (one language version of an
|
- `app/pages/` — `/` (search), `/wiki/[lang]/[slug]` (one language version of an
|
||||||
article — the address every version has, English included — drawn as a page of
|
article — the address every version has, English included — drawn as a page of
|
||||||
the open book), `/wiki/[slug]` (an address naming no language: the middleware
|
the open book), `/wiki/[slug]` (an address naming no language: the middleware
|
||||||
@@ -425,5 +450,6 @@ copy + an `APP_LANGUAGES` entry + the code in the backend's `SUPPORTED_LOCALES`.
|
|||||||
A tool page like this is wired in step: `ToolPage`/`TOOL_PAGE_PATHS`
|
A tool page like this is wired in step: `ToolPage`/`TOOL_PAGE_PATHS`
|
||||||
(`utils/routes.ts`), the page file, `TOOL_LABEL_KEYS`, the `ToolPane` branch and spine
|
(`utils/routes.ts`), the page file, `TOOL_LABEL_KEYS`, the `ToolPane` branch and spine
|
||||||
glyph, the sidebar link, and the `PAGE_TOOLS` entry.
|
glyph, the sidebar link, and the `PAGE_TOOLS` entry.
|
||||||
- Frontend dev server may fall back to port 3001 if 3000 is taken.
|
- Frontend dev server may fall back to port 3001 if 3000 is taken — the desktop
|
||||||
|
shell's `devUrl` is fixed to 3000, so free the port before `npm run desktop:dev`.
|
||||||
- `frontend/types/markdown-it-texmath.d.ts` provides types for the untyped texmath package.
|
- `frontend/types/markdown-it-texmath.d.ts` provides types for the untyped texmath package.
|
||||||
|
|||||||
@@ -23,12 +23,22 @@ services:
|
|||||||
|
|
||||||
# The Lean editor (lean4web) — its own server, since it runs a real Lean
|
# The Lean editor (lean4web) — its own server, since it runs a real Lean
|
||||||
# process per session and asks for a dedicated machine (see lean/).
|
# process per session and asks for a dedicated machine (see lean/).
|
||||||
|
# CI builds and pushes this image only when lean/** changes.
|
||||||
lean:
|
lean:
|
||||||
build: ./lean
|
build:
|
||||||
image: mathew-lean:latest
|
context: ./lean
|
||||||
|
# To ship a ~4GB image that keeps Mathlib's cache in the volume below
|
||||||
|
# instead of inside itself (filled once at first start — see the
|
||||||
|
# "Keeping Mathlib out of the image" section of lean/README.md):
|
||||||
|
# args:
|
||||||
|
# BAKE_MATHLIB: "false"
|
||||||
|
image: git.aranroig.com/syndria98/mathew-lean:latest
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
restart: always
|
restart: always
|
||||||
|
# Only with BAKE_MATHLIB: "false" above:
|
||||||
|
# volumes:
|
||||||
|
# - lean-mathlib:/opt/lean4web/Projects/MathlibDemo/.lake
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
image: git.aranroig.com/syndria98/mathew-backend:latest
|
image: git.aranroig.com/syndria98/mathew-backend:latest
|
||||||
@@ -44,3 +54,8 @@ services:
|
|||||||
# anyone could then forge sessions against the public site.
|
# anyone could then forge sessions against the public site.
|
||||||
JWT_SECRET: ${JWT_SECRET:-}
|
JWT_SECRET: ${JWT_SECRET:-}
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
|
# Only with the lean volume mode (uncomment together with the lean build arg
|
||||||
|
# and the lean service's volumes entry):
|
||||||
|
# volumes:
|
||||||
|
# lean-mathlib:
|
||||||
|
|||||||
3
frontend/.gitignore
vendored
@@ -6,6 +6,9 @@
|
|||||||
.cache
|
.cache
|
||||||
dist
|
dist
|
||||||
|
|
||||||
|
# The desktop bundle (nuxt generate in its desktop shape, read by src-tauri/)
|
||||||
|
dist-desktop
|
||||||
|
|
||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
A session is a bearer token the API handed us, kept in a cookie rather than
|
A session is a bearer token the API handed us, kept in a cookie rather than
|
||||||
localStorage so the server can see it too: the sidebar then knows who is
|
localStorage so the server can see it too: the sidebar then knows who is
|
||||||
signed in on the very first draw, before any script runs. The token itself is
|
signed in on the very first draw, before any script runs. The token itself is
|
||||||
never decoded here — the API is asked who it belongs to (see restoreSession). */
|
never decoded here — the API is asked who it belongs to (see restoreSession).
|
||||||
|
|
||||||
|
In the desktop build the token is kept twice — see keepTokenRemembered, at
|
||||||
|
the foot of this file. */
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
id: string
|
id: string
|
||||||
@@ -34,6 +37,11 @@ export function useAuth() {
|
|||||||
maxAge: WEEK_IN_SECONDS,
|
maxAge: WEEK_IN_SECONDS,
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
})
|
})
|
||||||
|
/* A window is not owed a cookie jar: its own address is one a webview is not
|
||||||
|
everywhere obliged to store cookies against, and a session that could not
|
||||||
|
outlive the window would ask the person to sign in every time they opened
|
||||||
|
the app. A no-op anywhere else. */
|
||||||
|
keepTokenRemembered(token, TOKEN_COOKIE)
|
||||||
const user = useState<AuthUser | null>('auth:user', () => null)
|
const user = useState<AuthUser | null>('auth:user', () => null)
|
||||||
|
|
||||||
const loggedIn = computed(() => user.value !== null)
|
const loggedIn = computed(() => user.value !== null)
|
||||||
|
|||||||
65
frontend/app/plugins/desktop.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/* The window's own habits.
|
||||||
|
|
||||||
|
These are the two things the wiki does differently once it is a desktop app
|
||||||
|
rather than a page of the web — see utils/desktop.ts for the asking, and
|
||||||
|
src-tauri/ for the shell. The web build takes the first line and returns:
|
||||||
|
nothing here changes how a browser sees the wiki.
|
||||||
|
|
||||||
|
Runs after the auth plugin (plugins come in name order), which matters: by
|
||||||
|
now a signed-in person's account has already had its say about the interface
|
||||||
|
language, and an account outranks this device. */
|
||||||
|
|
||||||
|
import { isDesktop, leadsOutward, openExternal, storeGet, storeSet } from '~/utils/desktop'
|
||||||
|
|
||||||
|
/** Where a window keeps the interface language it was last shown. Also the
|
||||||
|
* name of the cookie the web build writes it to — the same choice, remembered
|
||||||
|
* the way this kind of host can be sure of remembering it. */
|
||||||
|
const LOCALE_KEY = 'mathew-locale'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
if (!import.meta.client || !isDesktop()) return
|
||||||
|
|
||||||
|
/* Links. The wiki's own addresses are relative and the router turns them as
|
||||||
|
it always does; an absolute one is about something outside this window —
|
||||||
|
and a window has one page, no tab strip, no back button a reader would
|
||||||
|
trust after the wiki had been replaced by some other site's document. So
|
||||||
|
those go out to the system's browser instead, and the window keeps
|
||||||
|
showing the wiki it was asked to show.
|
||||||
|
|
||||||
|
Captured on the way in, so it also catches the links drawn from article
|
||||||
|
Markdown — the rendered text belongs to the article, which has no reason
|
||||||
|
to know it is not in a browser. */
|
||||||
|
document.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event) => {
|
||||||
|
if (event.defaultPrevented || event.button !== 0) return
|
||||||
|
// a modified click is the reader asking for the browser's own gesture
|
||||||
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
||||||
|
const target = event.target as Element | null
|
||||||
|
const href = target?.closest?.('a')?.getAttribute('href')
|
||||||
|
if (!href || !leadsOutward(href)) return
|
||||||
|
event.preventDefault()
|
||||||
|
void openExternal(href)
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
/* The interface language, kept where a window can be certain of keeping it.
|
||||||
|
On the web the choice rides on the `mathew-locale` cookie, which the server
|
||||||
|
reads so the first draw already speaks it. A window draws nothing on a
|
||||||
|
server, and its own address — `tauri://localhost` — is one a webview is
|
||||||
|
not everywhere obliged to keep cookies for, so the choice is mirrored into
|
||||||
|
localStorage: a lost cookie costs a language switch at the next start, and
|
||||||
|
that is all.
|
||||||
|
|
||||||
|
An account's own choice outranks the mirror, as it outranks the cookie —
|
||||||
|
hence the guard, the auth plugin having run first. Whatever the UI ends up
|
||||||
|
speaking is then written back, so the mirror always says what this window
|
||||||
|
last showed. */
|
||||||
|
const { locale, choose } = useAppLocale()
|
||||||
|
const { user } = useAuth()
|
||||||
|
const remembered = storeGet(LOCALE_KEY)
|
||||||
|
if (remembered && !user.value?.locale) void choose(remembered)
|
||||||
|
|
||||||
|
watch(locale, (code) => storeSet(LOCALE_KEY, code))
|
||||||
|
})
|
||||||
116
frontend/app/utils/desktop.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/* Running as a desktop app.
|
||||||
|
|
||||||
|
One bundle answers both a browser and an app window (Tauri — see
|
||||||
|
src-tauri/), so nothing in `app/` is desktop code: it is the wiki asking at
|
||||||
|
run time whether it happens to be sitting in a window. The answer comes from
|
||||||
|
the shell, which marks the page it loads and marks no other — so the same
|
||||||
|
dist-desktop/ files opened in an ordinary browser behave as a web page, and
|
||||||
|
a web page that somehow met Tauri would only gain the shell's habits.
|
||||||
|
|
||||||
|
What a window does differently, then, is small and all about being a window
|
||||||
|
with one page and no browser chrome: a link leads out to the system's own
|
||||||
|
browser rather than replacing the wiki in the only view there is, and the
|
||||||
|
few bytes the app remembers of a person live where a window can be certain
|
||||||
|
of keeping them. */
|
||||||
|
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
|
||||||
|
/** The handle every Tauri webview injects into its page — present before the
|
||||||
|
* npm API is imported, and without it. Nothing else sets this name. */
|
||||||
|
export function isDesktop(): boolean {
|
||||||
|
if (!import.meta.client) return false
|
||||||
|
return (window as Window & { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A window is expected to have localStorage. A private browser may throw on
|
||||||
|
being asked for it at all, and a setting that cannot be remembered is worth
|
||||||
|
far less than a page that still draws — so the three doors below are the
|
||||||
|
only place any of it is touched, and none of them lets a throw through. */
|
||||||
|
|
||||||
|
export function storeGet(key: string): string | null {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeSet(key: string, value: string): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, value)
|
||||||
|
} catch {
|
||||||
|
/* nothing remembered; nothing else depends on it */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function storeClear(key: string): void {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
} catch {
|
||||||
|
/* as good as cleared, for as long as this page lives */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether an address leads outside this app — to a document on some other
|
||||||
|
* site, or to a mail client, or to a phone dialler. The app's own pages are
|
||||||
|
* the relative `/wiki/…` the router already knows; an absolute address that
|
||||||
|
* happens to name the wiki's public site counts as outward too, and opens
|
||||||
|
* there in the browser, which is the honest reading of a link written out in
|
||||||
|
* full. */
|
||||||
|
export function leadsOutward(href: string): boolean {
|
||||||
|
if (!import.meta.client || !href) return false
|
||||||
|
let url: URL
|
||||||
|
try {
|
||||||
|
url = new URL(href, window.location.href)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (url.protocol === 'mailto:' || url.protocol === 'tel:') return true
|
||||||
|
return (url.protocol === 'http:' || url.protocol === 'https:') && url.origin !== window.location.origin
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hand a document to whatever machine-level application owns it: the browser
|
||||||
|
* for a page, the mail client for `mailto:`. In a browser a link already does
|
||||||
|
* this, so the fallback is the browser's own new tab; in a window there is no
|
||||||
|
* other tab, and following the link in place would leave the wiki's page
|
||||||
|
* gone from the only view there is. */
|
||||||
|
export async function openExternal(url: string): Promise<void> {
|
||||||
|
if (!import.meta.client || !url) return
|
||||||
|
if (isDesktop()) {
|
||||||
|
try {
|
||||||
|
const { openUrl } = await import('@tauri-apps/plugin-opener')
|
||||||
|
await openUrl(url)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
/* no shell to hand it to, or this window is not allowed this one — the
|
||||||
|
least worse thing left is the browser's way out */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.open(url, '_blank', 'noopener')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep a session token where a window cannot lose it.
|
||||||
|
|
||||||
|
On the web a cookie is the whole of it: the server reads it, and the browser
|
||||||
|
keeps it as long as the API said the token lasts. A window's own address
|
||||||
|
(`tauri://localhost`) is one a webview is not everywhere obliged to keep
|
||||||
|
cookies for, and a session dying with the window would mean signing in again
|
||||||
|
every time the app was opened — for a token the API hands out for a week. So
|
||||||
|
here it is mirrored into localStorage and taken back from there whenever the
|
||||||
|
cookie comes up empty, whichever of the two this webview chose to keep.
|
||||||
|
|
||||||
|
The cookie still leads: it is the one already at hand, the one a web build
|
||||||
|
would have read, and the one the sign-out line clears. A browser asked this
|
||||||
|
question of the same files ignores all of it — there is nothing to mirror
|
||||||
|
where cookies work — so the wiki's behavior there is untouched. */
|
||||||
|
export function keepTokenRemembered(token: Ref<string | null>, key: string): void {
|
||||||
|
if (!import.meta.client || !isDesktop()) return
|
||||||
|
|
||||||
|
if (!token.value) {
|
||||||
|
const kept = storeGet(key)
|
||||||
|
if (kept) token.value = kept
|
||||||
|
}
|
||||||
|
// sync, so signing out clears the mirror in the same breath, not on a tick
|
||||||
|
watch(token, (value) => (value ? storeSet(key, value) : storeClear(key)), { flush: 'sync' })
|
||||||
|
}
|
||||||
36
frontend/desktop/build.mjs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/* The bundle the desktop app loads.
|
||||||
|
|
||||||
|
nuxt.config.ts reads a MATHEW_DESKTOP flag and, in that shape, writes the
|
||||||
|
wiki as plain files — no server, no `/api` proxy, since a window has neither
|
||||||
|
— into dist-desktop/public, which is the `frontendDist` src-tauri/tauri.conf.json
|
||||||
|
names. `tauri build` gets here through `npm run desktop:generate`, which is
|
||||||
|
that config's `beforeBuildCommand`.
|
||||||
|
|
||||||
|
The reason this file exists rather than a script line: `MATHEW_DESKTOP=1 nuxt
|
||||||
|
generate` is a bashism, and desktop builds are something Windows is asked for
|
||||||
|
too. Setting the variable here works from any shell.
|
||||||
|
|
||||||
|
The wiki the app ships pointed at is overridable like any other build
|
||||||
|
setting — set it in the environment before `npm run desktop:build`:
|
||||||
|
NUXT_PUBLIC_API_BASE=http://localhost:5000 a wiki on this machine
|
||||||
|
NUXT_PUBLIC_LEAN_URL=https://lean.example a Lean server the app can reach
|
||||||
|
(Lean's default is empty, which leaves /lean saying there is no server here
|
||||||
|
rather than reaching for a localhost that is not on the machine). */
|
||||||
|
|
||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||||
|
|
||||||
|
const run = spawnSync(npm, ['exec', '--', 'nuxt', 'generate'], {
|
||||||
|
cwd: fileURLToPath(new URL('..', import.meta.url)),
|
||||||
|
env: { ...process.env, MATHEW_DESKTOP: '1' },
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (run.error) {
|
||||||
|
console.error(`desktop: could not run \`nuxt generate\` — ${run.error.message}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(run.status ?? 1)
|
||||||
BIN
frontend/desktop/icon-source.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
88
frontend/desktop/make-icon.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Draw desktop/icon-source.png — the 1024² master the icon set is cut from.
|
||||||
|
|
||||||
|
The wiki's own mark is the ∑ in the sidebar (AppHeader.vue's .logo-mark) over
|
||||||
|
the dark reading surface, so the app icon is that: the sigma, in the accent the
|
||||||
|
wiki wears by default, on the dark navy of its dark theme.
|
||||||
|
|
||||||
|
python3 desktop/make-icon.py # writes desktop/icon-source.png
|
||||||
|
npm run desktop:icons # cuts src-tauri/icons/ from it
|
||||||
|
|
||||||
|
Requires Pillow (pip install pillow). Nothing else in the project uses it —
|
||||||
|
this is build tooling, not app code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
SIZE = 1024
|
||||||
|
PAD = 64 # the margin macOS wants round an app icon; harmless elsewhere
|
||||||
|
RADIUS = 200
|
||||||
|
TOP = (16, 21, 44) # the navy a touch above --bg, so the square reads as an object
|
||||||
|
BOTTOM = (6, 8, 14) # ... down to just under it
|
||||||
|
ACCENT = (124, 140, 255) # --accent, dark theme (#7c8cff)
|
||||||
|
MARK = "\u2211" # ∑ — the wiki's mark
|
||||||
|
|
||||||
|
FONTS = [
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf",
|
||||||
|
]
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent / "icon-source.png"
|
||||||
|
|
||||||
|
|
||||||
|
def load_font(size: int) -> ImageFont.FreeTypeFont:
|
||||||
|
"""The first system font that actually carries a sigma."""
|
||||||
|
for path in FONTS:
|
||||||
|
try:
|
||||||
|
font = ImageFont.truetype(path, size)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if font.getbbox(MARK)[2] > 0:
|
||||||
|
return font
|
||||||
|
raise SystemExit(f"no font with a {MARK} found — look for one of: {', '.join(FONTS)}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# vertical gradient, then clipped to a rounded square
|
||||||
|
gradient = Image.new("RGB", (1, SIZE))
|
||||||
|
for y in range(SIZE):
|
||||||
|
t = y / (SIZE - 1)
|
||||||
|
gradient.putpixel((0, y), tuple(round(a + (b - a) * t) for a, b in zip(TOP, BOTTOM)))
|
||||||
|
gradient = gradient.resize((SIZE, SIZE))
|
||||||
|
|
||||||
|
mask = Image.new("L", (SIZE, SIZE), 0)
|
||||||
|
ImageDraw.Draw(mask).rounded_rectangle(
|
||||||
|
[PAD, PAD, SIZE - PAD, SIZE - PAD], radius=RADIUS, fill=255
|
||||||
|
)
|
||||||
|
|
||||||
|
icon = Image.new("RGBA", (SIZE, SIZE), (0, 0, 0, 0))
|
||||||
|
icon.paste(gradient, (0, 0), mask)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(icon)
|
||||||
|
# a hairline of accent light along the edge, so the dark square does not
|
||||||
|
# vanish into a dark taskbar
|
||||||
|
draw.rounded_rectangle(
|
||||||
|
[PAD, PAD, SIZE - PAD, SIZE - PAD], radius=RADIUS, outline=(124, 140, 255, 56), width=3
|
||||||
|
)
|
||||||
|
|
||||||
|
font = load_font(600)
|
||||||
|
left, top, right, bottom = draw.textbbox((0, 0), MARK, font=font)
|
||||||
|
w, h = right - left, bottom - top
|
||||||
|
# centred on the box the glyph is drawn in, not on its typographic cell:
|
||||||
|
# an ascent-based centre would sit the sigma high
|
||||||
|
draw.text(
|
||||||
|
((SIZE - w) / 2 - left, (SIZE - h) / 2 - top),
|
||||||
|
MARK,
|
||||||
|
font=font,
|
||||||
|
fill=ACCENT + (255,),
|
||||||
|
)
|
||||||
|
|
||||||
|
icon.save(OUT)
|
||||||
|
print(f"wrote {OUT}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,4 +1,24 @@
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
|
||||||
|
/* The desktop build — the same wiki in an app window (Tauri, see src-tauri/).
|
||||||
|
A window has no Nuxt server and no nginx behind it, so the wiki is written
|
||||||
|
out as plain files for the system webview to read off disk and every address
|
||||||
|
it calls has to be absolute. `desktop/build.mjs` sets the flag; the Tauri CLI
|
||||||
|
carries TAURI_ENV_* into its own build hooks, so `tauri build` reaches this
|
||||||
|
branch without anyone remembering to export anything. The web build takes
|
||||||
|
none of it: same config file, different shape of output. */
|
||||||
|
const desktop = process.env.MATHEW_DESKTOP === '1' || !!process.env.TAURI_ENV_PLATFORM
|
||||||
|
|
||||||
|
/* Where a desktop build takes its wiki from. Baked in at build — the app has
|
||||||
|
no origin of its own to route `/api` through — and overridable for anyone
|
||||||
|
building against another wiki: NUXT_PUBLIC_API_BASE (a staging wiki, this
|
||||||
|
machine), and NUXT_PUBLIC_LEAN_URL for a Lean server the app can actually
|
||||||
|
reach. Lean's default stays empty on purpose: a window on a random laptop
|
||||||
|
has no localhost:8080 to find, and /lean says so plainly rather than
|
||||||
|
reaching for a server that is not there. */
|
||||||
|
const apiDefault = desktop ? 'https://mathew.aranroig.com' : 'http://localhost:5000'
|
||||||
|
const leanDefault = desktop ? '' : 'http://localhost:8080'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-07-15',
|
compatibilityDate: '2025-07-15',
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
@@ -37,26 +57,43 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* A page of the web is drawn on the server; a window on the machine cannot
|
||||||
|
be — the webview draws these same pages itself, from the bundle. The book,
|
||||||
|
the middleware and the popups all run client-side either way, so nothing
|
||||||
|
but where HTML first comes from changes. */
|
||||||
|
ssr: !desktop,
|
||||||
|
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
public: {
|
public: {
|
||||||
// Where the UI calls the API. The Docker image sets this to "" so the
|
// Where the UI calls the API. The Docker image sets this to "" so the
|
||||||
// browser calls /api on its own origin and nginx routes it to the
|
// browser calls /api on its own origin and nginx routes it to the
|
||||||
// backend; dev keeps the absolute localhost address.
|
// backend; dev keeps the absolute localhost address, and the desktop
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? 'http://localhost:5000',
|
// build the public wiki it ships pointed at.
|
||||||
|
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? apiDefault,
|
||||||
// Where the Lean editor server (lean4web — see lean/) lives: /lean
|
// Where the Lean editor server (lean4web — see lean/) lives: /lean
|
||||||
// frames this address. It is the browser's own way in, not the
|
// frames this address. It is the browser's own way in, not the
|
||||||
// server's — the iframe talks to its own origin from there on.
|
// server's — the iframe talks to its own origin from there on.
|
||||||
leanUrl: process.env.NUXT_PUBLIC_LEAN_URL ?? 'http://localhost:8080',
|
leanUrl: process.env.NUXT_PUBLIC_LEAN_URL ?? leanDefault,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/* The desktop bundle: files under dist-desktop/ and nothing to run. Written
|
||||||
|
there rather than into .output/ so a desktop build never overwrites the
|
||||||
|
server bundle a web build left, and `frontendDist` in tauri.conf.json has
|
||||||
|
a directory of its own to read. */
|
||||||
|
...(desktop
|
||||||
|
? { nitro: { preset: 'static', output: { dir: 'dist-desktop' } } }
|
||||||
|
: {}),
|
||||||
|
|
||||||
// The Nuxt server's own API calls (SSR) are relative once apiBase is "",
|
// The Nuxt server's own API calls (SSR) are relative once apiBase is "",
|
||||||
// and land on this Nitro route, which forwards them to the backend. The
|
// and land on this Nitro route, which forwards them to the backend. The
|
||||||
// Docker image bakes NUXT_API_PROXY_TARGET=https://mathew.aranroig.com in
|
// Docker image bakes NUXT_API_PROXY_TARGET=https://mathew.aranroig.com in
|
||||||
// at build; nginx routes that address on to the backend.
|
// at build; nginx routes that address on to the backend. A desktop build has
|
||||||
routeRules: {
|
// no Nuxt server to answer this, and no reason for one: the app calls the
|
||||||
'/api/**': { proxy: `${process.env.NUXT_API_PROXY_TARGET || 'http://localhost:5000'}/api/**` },
|
// API by its own absolute address.
|
||||||
},
|
routeRules: desktop
|
||||||
|
? {}
|
||||||
|
: { '/api/**': { proxy: `${process.env.NUXT_API_PROXY_TARGET || 'http://localhost:5000'}/api/**` } },
|
||||||
|
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
@@ -68,7 +105,18 @@ export default defineNuxtConfig({
|
|||||||
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#f7f8fb' },
|
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#f7f8fb' },
|
||||||
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#0a0c12' },
|
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#0a0c12' },
|
||||||
],
|
],
|
||||||
script: [
|
script: desktop
|
||||||
|
? [
|
||||||
|
/* The same pre-paint, from a file: a window is a host one may
|
||||||
|
sensibly hold to a `script-src 'self'` Content Security Policy,
|
||||||
|
and code written into the HTML would not survive one.
|
||||||
|
public/preboot.js carries this exact code — keep the pair in
|
||||||
|
step. (src-tauri/ sets no policy yet; keeping this one in a
|
||||||
|
file is what makes turning it on a config line, not a page
|
||||||
|
rewrite.) */
|
||||||
|
{ tagPosition: 'head', src: '/preboot.js' },
|
||||||
|
]
|
||||||
|
: [
|
||||||
{
|
{
|
||||||
// Apply saved theme & accent before first paint to avoid a flash
|
// Apply saved theme & accent before first paint to avoid a flash
|
||||||
tagPosition: 'head',
|
tagPosition: 'head',
|
||||||
|
|||||||
253
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/i18n": "^10.6.0",
|
"@nuxtjs/i18n": "^10.6.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||||
"highlight.js": "^11.12.0",
|
"highlight.js": "^11.12.0",
|
||||||
"isomorphic-dompurify": "^4.4.0",
|
"isomorphic-dompurify": "^4.4.0",
|
||||||
"katex": "^0.18.9",
|
"katex": "^0.18.9",
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
"vue-router": "^5.3.1"
|
"vue-router": "^5.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.12.1",
|
||||||
"@types/katex": "^0.16.8",
|
"@types/katex": "^0.16.8",
|
||||||
"@types/markdown-it": "^14.2.0",
|
"@types/markdown-it": "^14.2.0",
|
||||||
"sass": "^1.105.0"
|
"sass": "^1.105.0"
|
||||||
@@ -4756,6 +4758,257 @@
|
|||||||
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
"integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-DeyFHa3wynpyoqTDikDEDGTJIq4LQ5USfolQGRmGIWT6JMADyxZBTDa5cAdT3tDg73rUXufPaCwN7aXBos4OnQ==",
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-kEDEiGzG+yAc5FeLxtXpES/VN+F2C8H0r4gDVtfRLKxT9np9101d9REG/Kgo2lr0hKi0IpDsODiHnU3+naOmLg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"bin": {
|
||||||
|
"tauri": "tauri.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tauri-apps/cli-darwin-arm64": "2.12.1",
|
||||||
|
"@tauri-apps/cli-darwin-x64": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-arm-gnueabihf": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-arm64-gnu": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-arm64-musl": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-riscv64-gnu": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-x64-gnu": "2.12.1",
|
||||||
|
"@tauri-apps/cli-linux-x64-musl": "2.12.1",
|
||||||
|
"@tauri-apps/cli-win32-arm64-msvc": "2.12.1",
|
||||||
|
"@tauri-apps/cli-win32-ia32-msvc": "2.12.1",
|
||||||
|
"@tauri-apps/cli-win32-x64-msvc": "2.12.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-8bMnbpJ2jKO/ufrOPL65EMHC3ptw7fzeraoQkwibyfGLqIl+UCWsC0152kmNbW6r9NvVQduXqAZF5mH1QgTWhw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-G+2qte8GNqNlEZ9ftd21rbu/APVkl3r/nzNDI7LVxEoqJWVNe9GbEgQUMH1r4PHT20Z2UxR3KjCkltIb0e/Wjg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-5b3n4DaTW+A1Zntnh4Yn44VyLrlPEpB8kMOBRfWkzNnzolBk+AYhKIfKjCLmoC8Mob3ETodxAEOMkE2YPKEWzQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-APArHhDu+8pXSCkCQmvaz1nD3d8WTmlCoSS/oG475TJ6AATBvyQOAmf5qELgsQ4s2+8MOsop0Zd699RKELF1Cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-JaCxcLoV0oGtllVkLr4UAHWG/wTfkEunnUqnsEsGYOp0VRxnUwHg63wFBD0jU2ul5eFfSLIDS4RF2gfzsSo5Jg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-K5+VXM7+SGVDwZleA2+lMgvbTVuA5e1tkiVEawu1Yxj26ZyEfO8h8xTC1rBdRX2xgJDSwCk6vPUBCS6i+Z5slQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-Z1QGPr49HJZMktu+Spu6Q1b/f9ANadXvqZZZt2645ua1x1ev6prOgp85eiGv2jRzohFEMLo6aJyaLjLUX5KIvA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-nHuEhRPOMSJ/8RRhtMRxB9B9lVr7xxpe4DapBz8x1GIau3vdzjHHJ1hAfmw+PyayPLZ6XoaVIHPgI17rX6ii8w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-I0oPk2uGh86R8dAs2X5OxSP+/GqSc8jh1Px3l8EqrocSNivsRHDTh2k3PVOqdv163dlV6PIVvmhZAbH/lPfZrA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-baNZRBfepAJ5qNFRMwzgBk94xQmkcmTZrNDjZjnzrq2vAaTC3NAhallb/zo3hyoCGQlMS13sdovktrg46NZz6Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||||
|
"version": "2.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.1.tgz",
|
||||||
|
"integrity": "sha512-dIFaKslrzcbesb+bBh+E1R9km241pHyj32w6D9FPtvQeYpRUFbiCLEa/2S+S0UnIytfxepp4P+X8IerGqnXeYg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0 OR MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-opener": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-mBorYfVKh9Lt7s4ZFtKbMUTxBw+Y/sVmSlKCtkkoBgMTC/DIbt/kaPm67L/2djAUqDI1wbixy5OHHVmipjuGMw==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
|
||||||
|
|||||||
@@ -7,10 +7,15 @@
|
|||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "nuxt preview",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare",
|
||||||
|
"desktop:generate": "node desktop/build.mjs",
|
||||||
|
"desktop:dev": "tauri dev",
|
||||||
|
"desktop:build": "tauri build",
|
||||||
|
"desktop:icons": "tauri icon desktop/icon-source.png"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/i18n": "^10.6.0",
|
"@nuxtjs/i18n": "^10.6.0",
|
||||||
|
"@tauri-apps/plugin-opener": "^2.7.0",
|
||||||
"highlight.js": "^11.12.0",
|
"highlight.js": "^11.12.0",
|
||||||
"isomorphic-dompurify": "^4.4.0",
|
"isomorphic-dompurify": "^4.4.0",
|
||||||
"katex": "^0.18.9",
|
"katex": "^0.18.9",
|
||||||
@@ -22,6 +27,7 @@
|
|||||||
"vue-router": "^5.3.1"
|
"vue-router": "^5.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2.12.1",
|
||||||
"@types/katex": "^0.16.8",
|
"@types/katex": "^0.16.8",
|
||||||
"@types/markdown-it": "^14.2.0",
|
"@types/markdown-it": "^14.2.0",
|
||||||
"sass": "^1.105.0"
|
"sass": "^1.105.0"
|
||||||
|
|||||||
17
frontend/public/preboot.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* Apply saved theme & accent before first paint to avoid a flash.
|
||||||
|
*
|
||||||
|
* This is the desktop build's copy of the inline snippet nuxt.config.ts puts
|
||||||
|
* in <head> for the web (keep the two in step). Out here as a file because a
|
||||||
|
* window may be held to a Content Security Policy — see src-tauri/README.md —
|
||||||
|
* and `script-src 'self'` would have no room for a script written inline in
|
||||||
|
* the HTML.
|
||||||
|
* A classic <script src> in <head> still runs before the page paints, so the
|
||||||
|
* surfaces arrive already the right way round. */
|
||||||
|
try {
|
||||||
|
var d = document.documentElement
|
||||||
|
var s = localStorage.getItem('mathew-theme')
|
||||||
|
var t = s || (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||||
|
d.setAttribute('data-theme', t)
|
||||||
|
var a = localStorage.getItem('mathew-accent')
|
||||||
|
if (a && a !== 'violet') d.setAttribute('data-accent', a)
|
||||||
|
} catch (e) {}
|
||||||
6
frontend/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Rust's build output — hundreds of MB, never belongs in the wiki.
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Written by tauri-build on every build: the JSON schemas for the permissions
|
||||||
|
# listed under capabilities/. Generated, so regenerable.
|
||||||
|
gen/schemas
|
||||||
4912
frontend/src-tauri/Cargo.lock
generated
Normal file
27
frontend/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "mathew"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Mathew — a wiki for mathematics, in a window of its own"
|
||||||
|
authors = ["Aran Roig"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# The app is a library with a thin main: Tauri's own entry points want it that
|
||||||
|
# way, so the same `run()` also serves a mobile build should one ever be asked
|
||||||
|
# for (the window itself is desktop-shaped — see src/lib.rs).
|
||||||
|
[lib]
|
||||||
|
name = "mathew_desktop_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
# A link inside the window hands its document to the system's browser rather
|
||||||
|
# than replacing the wiki in the only view there is. The wiki asks for this
|
||||||
|
# itself (app/utils/desktop.ts); this is the door it goes out by.
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
|
||||||
|
# Only a desktop app can be launched twice onto one window.
|
||||||
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
tauri-plugin-single-instance = "2"
|
||||||
78
frontend/src-tauri/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# src-tauri/ — the app window
|
||||||
|
|
||||||
|
Mathew as a **desktop app** (Tauri 2, Linux / Windows / macOS): the wiki in a
|
||||||
|
window of its own, no browser around it. The shell is deliberately thin — the
|
||||||
|
app *is* the same Nuxt bundle the website serves, written out as plain files
|
||||||
|
and embedded in the binary. Nothing in `frontend/app/` is desktop code; the
|
||||||
|
bundle asks at run time whether it happens to be sitting in a window
|
||||||
|
(`app/utils/desktop.ts`) and only then takes the window's habits: a link leads
|
||||||
|
out to the system's browser instead of replacing the wiki in the only view
|
||||||
|
there is, and the session token and interface language are mirrored into
|
||||||
|
`localStorage` because a webview is not everywhere obliged to keep cookies for
|
||||||
|
its own `tauri://localhost` address.
|
||||||
|
|
||||||
|
The wiki needs a wiki to talk to: the window holds no server and no database.
|
||||||
|
It calls the **public Mathew** over HTTPS by default — the API base is baked in
|
||||||
|
at build, see the table below.
|
||||||
|
|
||||||
|
## Build & run
|
||||||
|
|
||||||
|
From `frontend/` — these need [Rust](https://rustup.rs) (stable) and the
|
||||||
|
[Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS;
|
||||||
|
on Linux that is the WebKitGTK stack:
|
||||||
|
`libwebkit2gtk-4.1-dev libgtk-3-dev libssl-dev libxdo-dev librsvg2-dev build-essential`.
|
||||||
|
|
||||||
|
| Script | What it does |
|
||||||
|
| --- | --- |
|
||||||
|
| `npm run desktop:dev` | `tauri dev` — opens the **Nuxt dev server** (`localhost:3000`, started for you) in the window, hot-reload included; `isDesktop()` is already true inside it |
|
||||||
|
| `npm run desktop:build` | `tauri build` — runs `desktop:generate` (below), compiles the shell, embeds the files, and leaves installers under `src-tauri/target/release/bundle/` (deb/rpm/AppImage on Linux, NSIS/MSI on Windows, .app/.dmg on macOS) |
|
||||||
|
| `npm run desktop:generate` | the bundle the window loads: `MATHEW_DESKTOP=1 nuxt generate` → `dist-desktop/public`, the `frontendDist` this config names (a desktop build never overwrites the web `.output/`) |
|
||||||
|
| `npm run desktop:icons` | re-cuts `src-tauri/icons/` from `desktop/icon-source.png` (redraw that one with `python3 desktop/make-icon.py`) |
|
||||||
|
|
||||||
|
The desktop shape lives in `nuxt.config.ts` under one flag
|
||||||
|
(`MATHEW_DESKTOP=1`, also implied by the `TAURI_ENV_*` the Tauri CLI passes to
|
||||||
|
its build hooks): `ssr: false`, no `/api` proxy route, and the pre-paint theme
|
||||||
|
script served from `public/preboot.js` instead of inline. The web build takes
|
||||||
|
none of it. Deep routes (`/wiki/en/bayes-theorem`, …) are not files in the
|
||||||
|
bundle — Tauri's own asset serving answers unknown paths with `index.html`, so
|
||||||
|
the client router takes over exactly as it does on a static host.
|
||||||
|
|
||||||
|
**Dev-server port**: `devUrl` is fixed to `3000`, and Nuxt falls back to `3001`
|
||||||
|
when `3000` is taken (see the main gotchas) — free the port before
|
||||||
|
`desktop:dev`, or the window loads whatever else answers there.
|
||||||
|
|
||||||
|
## Where the app reaches
|
||||||
|
|
||||||
|
| Build variable | Default (desktop) | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `NUXT_PUBLIC_API_BASE` | `https://mathew.aranroig.com` | the wiki the app reads and writes; point it at `http://localhost:5000` to test against this machine |
|
||||||
|
| `NUXT_PUBLIC_LEAN_URL` | empty | the Lean server `/lean` talks to; empty leaves that page saying there is no server here, which is the honest answer on a random laptop. The API's default CORS (`*`) carries the window's `tauri://localhost` origin fine — it sends a bearer header, not cookies |
|
||||||
|
|
||||||
|
Set them in the environment before `npm run desktop:build`.
|
||||||
|
|
||||||
|
## What the shell owns
|
||||||
|
|
||||||
|
- **one window per launch** — `tauri-plugin-single-instance`: pressing the
|
||||||
|
icon again brings the open window forward instead of stacking a second wiki
|
||||||
|
behind it (`src/lib.rs`).
|
||||||
|
- **the way out** — `tauri-plugin-opener` is the door `app/utils/desktop.ts`
|
||||||
|
hands outward links to the system's browser; `capabilities/default.json` is
|
||||||
|
what grants it. That and `core:default` are the whole permission list:
|
||||||
|
reading and writing the wiki is the webview's own business.
|
||||||
|
- **no CSP** (`csp: null` in `tauri.conf.json`): the window loads no foreign
|
||||||
|
script and talks to one API over HTTPS, and `/lean`'s Monaco wants
|
||||||
|
`eval`-like tricks a strict `script-src` would have to excuse. The one
|
||||||
|
script the page writes itself (the pre-paint theme, `public/preboot.js`)
|
||||||
|
already lives in a **file**, so turning a policy on later is a config line —
|
||||||
|
keep it that way if one is ever set.
|
||||||
|
|
||||||
|
Nothing here is signed: bundles come out unsigned, and updating is
|
||||||
|
reinstalling. An updater, signing, and a per-machine "which wiki?" setting are
|
||||||
|
the obvious next steps if this ever ships for real.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
The app's version lives in `tauri.conf.json` (`version`); it names the
|
||||||
|
installers and the platform bundle metadata. Commit `Cargo.lock` once a build
|
||||||
|
has produced one — for an application binary, a locked dependency set is what
|
||||||
|
makes a rebuild reproducible.
|
||||||
3
frontend/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
7
frontend/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "What the Mathew window may ask of the machine. Reading and writing the wiki is the webview's own business — plain HTTP out to the API and, if one is configured, a websocket to the Lean server — and needs no permission here. The one thing it cannot do by itself is hand a link to the system's browser, which is what `opener:default` lets app/utils/desktop.ts ask for.",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": ["core:default", "opener:default"]
|
||||||
|
}
|
||||||
BIN
frontend/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
frontend/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
frontend/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
frontend/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
frontend/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
frontend/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
frontend/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
frontend/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
frontend/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
frontend/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
BIN
frontend/src-tauri/icons/icon.icns
Normal file
BIN
frontend/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 951 B |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
38
frontend/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/* Mathew as a window.
|
||||||
|
|
||||||
|
The shell is thin on purpose. Everything the app *is* — the open book, the
|
||||||
|
editor, the five interface languages — is the Nuxt bundle it loads, written
|
||||||
|
out by `npm run desktop:generate` and read from disk by the system webview
|
||||||
|
(see src-tauri/README.md). The wiki reaches its own API by itself: no Rust
|
||||||
|
command stands between the two, so an article is fetched the same way in the
|
||||||
|
window as in a browser.
|
||||||
|
|
||||||
|
Two things are the shell's own:
|
||||||
|
|
||||||
|
- `opener`, the door a link goes out by. The wiki asks for it by name
|
||||||
|
(app/utils/desktop.ts); `capabilities/default.json` is what answers.
|
||||||
|
- one window per launch. Pressing the icon again brings the window that is
|
||||||
|
already open to the front instead of stacking a second copy of the wiki
|
||||||
|
behind it. */
|
||||||
|
|
||||||
|
#[cfg(desktop)]
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
let mut builder = tauri::Builder::default().plugin(tauri_plugin_opener::init());
|
||||||
|
|
||||||
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.unminimize();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Mathew");
|
||||||
|
}
|
||||||
7
frontend/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// An app window has no terminal to answer from: keep one from opening behind
|
||||||
|
// it on Windows, where a release build would otherwise start a console.
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
mathew_desktop_lib::run()
|
||||||
|
}
|
||||||
47
frontend/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Mathew",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.aranroig.mathew",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeBuildCommand": "npm run desktop:generate",
|
||||||
|
"frontendDist": "../dist-desktop/public"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"label": "main",
|
||||||
|
"title": "Mathew",
|
||||||
|
"width": 1440,
|
||||||
|
"height": 900,
|
||||||
|
"minWidth": 940,
|
||||||
|
"minHeight": 600,
|
||||||
|
"center": true,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"publisher": "Aran Roig",
|
||||||
|
"homepage": "https://mathew.aranroig.com",
|
||||||
|
"category": "Education",
|
||||||
|
"shortDescription": "A collaborative wiki of mathematics",
|
||||||
|
"longDescription": "Mathew is a wiki of mathematical concepts: Markdown with LaTeX, an open shelf of articles in several languages, a Markdown editor, a calculator, a graph plotter and a Lean 4 editor. This is the same wiki in an app window of its own; it reads and writes a Mathew wiki over HTTP, so it needs one to reach.",
|
||||||
|
"copyright": "© Aran Roig",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,19 @@
|
|||||||
# docker build -t mathew-lean ./lean
|
# docker build -t mathew-lean ./lean
|
||||||
# docker run -p 8080:8080 mathew-lean # → http://localhost:8080
|
# docker run -p 8080:8080 mathew-lean # → http://localhost:8080
|
||||||
#
|
#
|
||||||
# The build is heavy by nature (Mathlib is fetched and its precompiled
|
# The build is heavy by nature (Mathlib is fetched and its precompiled cache
|
||||||
# cache unpacked); expect a long build and an image of several GB.
|
# unpacked), but multi-stage keeps the junk out of the pushed image: the
|
||||||
|
# compilers, npm dev dependencies and caches stay in the builder, and the
|
||||||
|
# final image is a slim runtime with exactly what a session needs — the node
|
||||||
|
# server, the elan toolchain, the built client, and the project's Lean cache.
|
||||||
|
#
|
||||||
|
# To ship an image without Mathlib itself (~4GB instead of ~11GB), pass
|
||||||
|
# `--build-arg BAKE_MATHLIB=false` and hold the cache in a volume instead:
|
||||||
|
# the entrypoint fills a fresh volume once at first start. See
|
||||||
|
# "Keeping Mathlib out of the image" in lean/README.md.
|
||||||
|
|
||||||
FROM node:24-bookworm
|
# ---------- build ----------
|
||||||
|
FROM node:24-bookworm AS build
|
||||||
|
|
||||||
# lake fetches dependencies over git/curl, drives clang to link, and elan
|
# lake fetches dependencies over git/curl, drives clang to link, and elan
|
||||||
# (Lean's toolchain manager) installs the toolchains each project pins
|
# (Lean's toolchain manager) installs the toolchains each project pins
|
||||||
@@ -28,19 +37,35 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
RUN curl -sSfL https://elan.lean-lang.org/elan-init.sh | bash -s -- -y --default-toolchain none
|
RUN curl -sSfL https://elan.lean-lang.org/elan-init.sh | bash -s -- -y --default-toolchain none
|
||||||
ENV PATH="/root/.elan/bin:${PATH}"
|
ENV PATH="/root/.elan/bin:${PATH}"
|
||||||
|
|
||||||
|
# cypress (a root dev dependency) pulls an ~800MB browser bundle on install —
|
||||||
|
# nothing in this build runs browser tests, so skip it
|
||||||
|
ENV CYPRESS_INSTALL_BINARY=0
|
||||||
|
|
||||||
# The lean4web source. Pass a tag or branch to pin a build:
|
# The lean4web source. Pass a tag or branch to pin a build:
|
||||||
# docker build --build-arg LEAN4WEB_REF=v0.2.2 ./lean
|
# docker build --build-arg LEAN4WEB_REF=v0.2.2 ./lean
|
||||||
ARG LEAN4WEB_REF=main
|
ARG LEAN4WEB_REF=main
|
||||||
RUN git clone --depth 1 --branch "${LEAN4WEB_REF}" \
|
RUN git clone --depth 1 --branch "${LEAN4WEB_REF}" \
|
||||||
https://github.com/leanprover-community/lean4web.git /opt/lean4web
|
https://github.com/leanprover-community/lean4web.git /opt/lean4web
|
||||||
|
|
||||||
|
# Ship only the MathlibDemo project: the wiki opens the default project, and
|
||||||
|
# upstream's second demo ("Stable") pins its own toolchain — elan would carry
|
||||||
|
# a second ~3GB one that no session ever uses.
|
||||||
|
RUN rm -rf /opt/lean4web/Projects/Stable
|
||||||
|
|
||||||
WORKDIR /opt/lean4web
|
WORKDIR /opt/lean4web
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
# Build the Lean projects under Projects/ (MathlibDemo pulls Mathlib and
|
# Build the Lean projects under Projects/. For MathlibDemo this fetches
|
||||||
# unpacks its precompiled cache — the slow part of the image) and then the
|
# Mathlib and unpacks its precompiled cache — the slow part of the image.
|
||||||
# editor client, which the server serves alongside its websocket
|
# With BAKE_MATHLIB=false that step is skipped and only the pinned toolchain
|
||||||
RUN npm run build:projects
|
# is installed: the project ships unbuilt, and the entrypoint fills it on a
|
||||||
|
# first start (the intended pairing is a volume over the project's .lake).
|
||||||
|
ARG BAKE_MATHLIB=true
|
||||||
|
RUN if [ "${BAKE_MATHLIB}" = "true" ]; then \
|
||||||
|
npm run build:projects; \
|
||||||
|
else \
|
||||||
|
(cd Projects/MathlibDemo && lake --version > /dev/null); \
|
||||||
|
fi
|
||||||
|
|
||||||
# Mathew's theme layer: the wiki's font, stylesheet, and theme handshake
|
# Mathew's theme layer: the wiki's font, stylesheet, and theme handshake
|
||||||
# join the client's public/ so they ship in the built bundle, and a sed
|
# join the client's public/ so they ship in the built bundle, and a sed
|
||||||
@@ -56,9 +81,48 @@ RUN sed -i \
|
|||||||
|
|
||||||
RUN npm run build:client
|
RUN npm run build:client
|
||||||
|
|
||||||
|
# ---------- runtime ----------
|
||||||
|
# Only what serving sessions needs. The lean/lake binaries carry their own
|
||||||
|
# shared libraries — nothing else from the builder has to come along.
|
||||||
|
FROM node:24-bookworm-slim
|
||||||
|
|
||||||
|
# bash for scripts, curl for the healthcheck and Mathlib's cache fetch,
|
||||||
|
# git for lake's dependency operations
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash curl git ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# elan and the toolchain(s) the projects resolved at build time
|
||||||
|
COPY --from=build /root/.elan /root/.elan
|
||||||
|
ENV PATH="/root/.elan/bin:${PATH}"
|
||||||
|
|
||||||
|
WORKDIR /opt/lean4web
|
||||||
|
|
||||||
|
# The server, the built (wiki-skinned) client, and the manifests. Note the
|
||||||
|
# whole prod dependency tree: the server imports vscode-ws-jsonrpc out of the
|
||||||
|
# client workspace (npm hoists it), so a server-only `npm ci -w` would miss it.
|
||||||
|
COPY --from=build /opt/lean4web/package.json /opt/lean4web/package-lock.json ./
|
||||||
|
COPY --from=build /opt/lean4web/client/package.json ./client/
|
||||||
|
COPY --from=build /opt/lean4web/server ./server
|
||||||
|
COPY --from=build /opt/lean4web/client/dist ./client/dist
|
||||||
|
# The Lean project: toolchain pin, lakefile, and — unless BAKE_MATHLIB=false
|
||||||
|
# — Mathlib and its unpacked cache, the bulk of the image.
|
||||||
|
COPY --from=build /opt/lean4web/Projects ./Projects
|
||||||
|
|
||||||
|
RUN npm ci --omit=dev \
|
||||||
|
&& rm -rf /root/.npm
|
||||||
|
|
||||||
|
# Normally a no-op: the image already carries the built Mathlib. With
|
||||||
|
# BAKE_MATHLIB=false and a volume over the project's .lake, this fills a
|
||||||
|
# fresh volume once before the server starts (see lean/README.md).
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Bubblewrap cannot sandbox inside a container (no nested user namespaces by
|
# Bubblewrap cannot sandbox inside a container (no nested user namespaces by
|
||||||
# default) — Docker itself is the isolation here, so the server runs Lean
|
# default) — Docker itself is the isolation here, so the server runs Lean
|
||||||
# directly. PORT 8080 is lean4web's production default: editor + websocket.
|
# directly. PORT 8080 is lean4web's production default: editor + websocket.
|
||||||
|
# `npm run prod` would only add nodemon (a dev dependency) around this same
|
||||||
|
# `node index.mjs`, so the final image runs the server directly.
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
NO_BWRAP=true \
|
NO_BWRAP=true \
|
||||||
PORT=8080
|
PORT=8080
|
||||||
@@ -67,4 +131,5 @@ EXPOSE 8080
|
|||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s --retries=3 \
|
||||||
CMD curl -sf http://localhost:8080/health || exit 1
|
CMD curl -sf http://localhost:8080/health || exit 1
|
||||||
|
|
||||||
CMD ["npm", "run", "prod"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["node", "server/index.mjs"]
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ docker build -t mathew-lean ./lean
|
|||||||
docker run --rm -p 8080:8080 mathew-lean
|
docker run --rm -p 8080:8080 mathew-lean
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The build is **multi-stage**: compilers, dev dependencies and build caches stay
|
||||||
|
in the builder, and the pushed image is a slim runtime carrying only what
|
||||||
|
sessions need — the node server, the elan toolchain, the built client, and
|
||||||
|
Mathlib's precompiled cache (the bulk of it, ~7GB of olean files; total image
|
||||||
|
~11GB). The upstream "Stable" demo project is dropped: it pinned a second
|
||||||
|
~3GB toolchain, and the wiki only ever opens the default project
|
||||||
|
(`MathlibDemo`).
|
||||||
|
|
||||||
`GET http://localhost:8080/health` answers when the server is up — the image
|
`GET http://localhost:8080/health` answers when the server is up — the image
|
||||||
carries a `HEALTHCHECK` for it. The frontend finds the server at
|
carries a `HEALTHCHECK` for it. The frontend finds the server at
|
||||||
`NUXT_PUBLIC_LEAN_URL` (default `http://localhost:8080`, dev included), so out
|
`NUXT_PUBLIC_LEAN_URL` (default `http://localhost:8080`, dev included), so out
|
||||||
@@ -38,6 +46,29 @@ docker build --build-arg LEAN4WEB_REF=v0.2.2 -t mathew-lean ./lean
|
|||||||
|
|
||||||
(any branch or tag; the clone is shallow, so commit SHAs are not offered.)
|
(any branch or tag; the clone is shallow, so commit SHAs are not offered.)
|
||||||
|
|
||||||
|
## Keeping Mathlib out of the image
|
||||||
|
|
||||||
|
Mathlib's compiled cache is what makes this image (and pushing it) heavy. If
|
||||||
|
the registry or the CI runner chokes on it, build without Mathlib and let a
|
||||||
|
volume hold it instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --build-arg BAKE_MATHLIB=false -t mathew-lean ./lean # ~4GB
|
||||||
|
docker run --rm -p 8080:8080 -v lean-mathlib:/opt/lean4web/Projects/MathlibDemo/.lake mathew-lean
|
||||||
|
```
|
||||||
|
|
||||||
|
The `BAKE_MATHLIB=false` image still ships the pinned elan toolchain, but not
|
||||||
|
the project's `.lake`: the entrypoint notices a fresh volume and fills it once
|
||||||
|
at first start — `lake update` fetches the sources pinned in
|
||||||
|
`lake-manifest.json`, Mathlib's post-update hook downloads the matching
|
||||||
|
precompiled cache, `lake build` writes the project's own module. Expect ~10
|
||||||
|
minutes and a few hundred MB of download; restarts after that are instant,
|
||||||
|
and **image rebuilds never touch Mathlib again** (the volume stays). The
|
||||||
|
trade: the cache must survive on the host, first start needs network, and it
|
||||||
|
redoes once if you recreate with a clean volume — hence the default staying
|
||||||
|
`BAKE_MATHLIB=true`. The commented blocks in `docker-compose.yml` wire this
|
||||||
|
up for the production stack.
|
||||||
|
|
||||||
## The protocol the wiki's client speaks
|
## The protocol the wiki's client speaks
|
||||||
|
|
||||||
The websocket endpoint is `/websocket/<project>` (project names come from
|
The websocket endpoint is `/websocket/<project>` (project names come from
|
||||||
@@ -117,21 +148,27 @@ The image clones lean4web at build time, so a rebuild is an update:
|
|||||||
docker compose build --pull lean && docker compose up -d lean
|
docker compose build --pull lean && docker compose up -d lean
|
||||||
```
|
```
|
||||||
|
|
||||||
Rebuilding also re-runs `Projects/*/leanweb-build.sh` — the demo projects are
|
Rebuilding also re-runs the project build — Mathlib's pin moves with upstream
|
||||||
rebuilt and Mathlib's cache re-fetched, so treat this like the heavy build it
|
and its cache re-downloads, so treat this like the heavy build it is (run it
|
||||||
is (several GB of image; run it on the machine that hosts the editor).
|
on the machine that hosts the editor; pin `LEAN4WEB_REF` for a build that
|
||||||
|
stands still).
|
||||||
|
|
||||||
|
CI follows the same split: `.gitea/workflows/deploy-lean.yml` rebuilds, pushes
|
||||||
|
and redeploys the lean image **only when `lean/**` changes**; the regular
|
||||||
|
deploy pipeline ignores `lean/**` entirely and just pulls whatever
|
||||||
|
`mathew-lean:latest` the registry holds. (In volume mode the pushed image is
|
||||||
|
~4GB and a lean rebuild doesn't fetch Mathlib at all.)
|
||||||
|
|
||||||
## Adding Lean projects
|
## Adding Lean projects
|
||||||
|
|
||||||
Anything under `Projects/` of the *image source* becomes a selectable project
|
Anything under `Projects/` of the *running image* becomes a selectable project
|
||||||
(`#project=<name>`). To ship one with the image, build a custom image that
|
(`#project=<name>`). To ship one with the image, add it in the Dockerfile's
|
||||||
copies it in before the project build step:
|
builder stage, before the project build step:
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM mathew-lean AS base # or edit lean/Dockerfile directly
|
# in lean/Dockerfile, build stage, before the BAKE_MATHLIB step
|
||||||
COPY MyProject /opt/lean4web/Projects/MyProject
|
COPY MyProject /opt/lean4web/Projects/MyProject
|
||||||
WORKDIR /opt/lean4web/Projects/MyProject
|
RUN cd Projects/MyProject && lake build
|
||||||
RUN lake build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
See upstream's [Adding Projects](https://github.com/leanprover-community/lean4web/blob/main/doc/Projects.md)
|
See upstream's [Adding Projects](https://github.com/leanprover-community/lean4web/blob/main/doc/Projects.md)
|
||||||
|
|||||||
27
lean/entrypoint.sh
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Mathew — lean server entrypoint.
|
||||||
|
#
|
||||||
|
# In the usual image this does nothing: the built Mathlib already sits under
|
||||||
|
# the project and the server starts straight away.
|
||||||
|
#
|
||||||
|
# With the image built as `--build-arg BAKE_MATHLIB=false` (see README's
|
||||||
|
# "Keeping Mathlib out of the image"), the project's .lake is meant to be a
|
||||||
|
# volume: a fresh one gets filled once, at first start, so Mathlib's ~7GB of
|
||||||
|
# compiled cache never travels through the registry.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PROJECT=/opt/lean4web/Projects/MathlibDemo
|
||||||
|
|
||||||
|
if [ ! -f "$PROJECT/.lake/build/lib/lean/MathlibDemo.olean" ]; then
|
||||||
|
echo "[mathew-lean] $PROJECT/.lake holds no built Mathlib — fetching it once (expect ~10 minutes)"
|
||||||
|
cd "$PROJECT"
|
||||||
|
# lake update pulls the sources pinned in lake-manifest.json, and Mathlib's
|
||||||
|
# post-update hook fetches the matching precompiled cache; lake build then
|
||||||
|
# only writes the project's own module. The image's pinned toolchain
|
||||||
|
# (elan) stays as it is — no lean-toolchain re-curl, no surprise downloads.
|
||||||
|
lake update -R
|
||||||
|
lake build
|
||||||
|
echo "[mathew-lean] Mathlib ready"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||