Course → Linux AppImage
Wraps a finished book-to-course course into one executable file for Linux, self-contained like the APK: copy it to any
desktop Linux computer and start it — no Python, no WebKitGTK, nothing to install. Inside: the official Electron build
(Chromium + Node), a small main.js, the course, an icon and the Noto Color Emoji font (the player's UI uses emoji, and
many distros ship no emoji font). main.js does what the course's serve.py does — serves
the page on 127.0.0.1, saves progress to a file and runs code exercises' tests — so the "Run tests" button works on
the desktop (unlike the phone app). Nothing is compiled; the AppImage is ~105 MB + the course (that's Chromium; unused
Chromium translations are left out) + ~9 MB for the emoji font.
Why Electron and not the system WebView: Linux has no WebView every computer is guaranteed to have, and bundling this machine's WebKitGTK would tie the file to this distro's glibc. Official Electron builds run on any current desktop distro.
The wrapper and the scripts are fixed and tested — your job is the conversation around them: find the course, get the toolchain in place with the user's consent, build, and explain how to start and install the app.
script (in scripts/) | purpose |
|---|---|
toolchain.py [--json] | what is installed, what is missing, where to download it; exit 0 = ready |
setup_toolchain.py --dry-run | list exactly what would be downloaded (name, size, URL) — downloads nothing |
setup_toolchain.py --download | download only the missing parts into ~/.local/share/course-to-appimage (no sudo, SHA-256 verified) |
build_appimage.py COURSE_DIR [--install] | build the AppImage into <COURSE_DIR>-appimage/; --install adds it to the app menu |
build_appimage.py COURSE_DIR --uninstall | remove the menu entry, icon and ~/Applications copy (progress stays) |
All scripts are Python 3 standard library only (Python is needed on the build machine, not in the app).
Workflow
1. Find the course
A course folder has index.html, assets/app.js and course/data.js. Look where the user points, in the current
directory, or among recent book-to-course output. If they give a .zip (from pack_course.py), unzip it next to the zip
first — it contains one top folder named after the course; that folder is COURSE_DIR. If content/ exists but
course/data.js is missing or older than the lessons, the course was not rebuilt — run book-to-course's
build_course.py COURSE_DIR first (when that skill is available), otherwise ask the user to.
2. Check the toolchain
bashpython3 scripts/toolchain.py
Needed: appimagetool (packs the file), Electron 30+ (the engine, ~115 MB official zip — an unpacked folder or
the zip itself, found in Downloads, ~/.cache/electron, $ELECTRON_DIST or the skill's own folder) and the Noto Color
Emoji font (taken from the system's fonts when installed — usually it is — otherwise a pinned ~10 MB download). The runtime file
is optional. It checks that appimagetool actually starts. If it prints READY, go to step 4 (if only the runtime is
missing, mention in one line that the build will fetch it from GitHub — don't stop for it).
3. Something is missing → ask, don't assume
Tell the user, in their language, what is missing — use the report's lines, they already contain the official download
links (GitHub releases of AppImage/appimagetool, electron/electron and AppImage/type2-runtime, googlefonts/noto-emoji)
and package-manager commands. Run setup_toolchain.py --dry-run so you can quote the real sizes, versions and URLs, then ask them to
choose (AskUserQuestion works well here):
- "I'll install it myself" — give a short checklist: what to download, from where, and where to put it
(appimagetool:
chmod +x, e.g. as~/.local/bin/appimagetool; Electron: theelectron-vX.Y.Z-linux-x64.zip—arm64on ARM — can simply stay in Downloads, the build unpacks it). Warn that the distro'selectronpackage is not suitable (built against this system's libraries). Then stop and wait — end your turn. Do not poll, do not build, do not download anything. When they write that it's done, reruntoolchain.py; if something is still missing or "found but unusable", say exactly what and wait again. - "Download it for me" — run
python3 scripts/setup_toolchain.py --download(~140 MB when everything is missing; official GitHub releases — MIT, the font OFL — checksums verified; installs only into~/.local/share/course-to-appimage, no sudo). It is a one-time download — later builds reuse it.
Why the care: downloading over 100 MB and running a binary are the user's decisions, not yours, and a user who said they'd install it themselves is mid-installation — building or downloading behind their back collides with that.
4. Build
bashpython3 scripts/build_appimage.py "<COURSE_DIR>"
Takes a few seconds. Defaults that are right for almost everyone — mention them, don't ask:
- Output
<COURSE_DIR>-appimage/<course-id>-<arch>.AppImage, next to the course folder;appimage.jsonthere remembers the app id, name, icon colour and an auto-incremented version. - App id
net.booktocourse.<course_id>, name = course title. Override with--app-id/--nameonly when asked — the app id names the learner's data folder, so changing it later starts with empty progress. - Built for this machine's architecture (x86_64 or aarch64 — Electron has no 32-bit Linux build).
Offer --install (copies the AppImage to ~/Applications, adds a menu entry with icon under Education, no sudo) —
ask in the same message as the hand-over if the user hasn't said; it's quick to do afterwards by rerunning with --install.
Once installed, every later build refreshes the installed copy automatically.
The build ends with a self-test (it starts the AppImage with --paths, which loads Electron and main.js). If the build
or the self-test fails, the output says what failed; see references/troubleshooting.md.
5. Hand it over
Tell the user in a few lines: where the AppImage is (absolute path), its size, and how to start it:
- double-click it in the file manager, or
./<file>.AppImagein a terminal. On another computer: copy the one file (USB stick, cloud drive, network); if it lost the executable bit,chmod +x/ Properties → "Allow executing as program". - with
--install: from the application menu (Education), with its own icon.
Also say, briefly:
- Progress lives in
~/.local/share/<app-id>/progress/progress.jsonof whoever runs the app (not in the course folder'sprogress/— the page's footer wording comes from the folder version). It survives updates: rebuild after adding chapters and start the new file. - Code exercises (if the course has them) work fully, unlike on the phone: the AppImage is an ordinary program on the
computer, so "Run tests" runs the exercise's command (e.g.
go test) with the compiler installed there — exactly likestart.sh. The learner edits the files in their own editor; editable copies live in~/.local/share/<app-id>/exercises/, and each exercise in the app shows that full path, an "Open folder" button and a readycd …command. The language toolchain (e.g. Go) must be installed on that computer — without it the page says the program is not installed. Bundling compilers is not done: hundreds of MB per language. Lessons, quizzes, tests and flashcards need nothing. - Requirements of the target: a desktop Linux (x86_64/arm64 as built) — the usual desktop libraries (GTK 3, NSS, X11/ Wayland, ALSA) that every GNOME/KDE/XFCE/Cinnamon install has, as for any Electron app. A bare server/container without a desktop lacks them.
What the app does (so you can answer questions)
AppRunstarts the bundled Electron; where unprivileged user namespaces are blocked (e.g. Ubuntu 24.04's AppArmor default) or when run as root, it adds--no-sandbox, because an AppImage cannot carry Chromium's setuid sandbox helper. The window only shows the bundled course; links to other sites open in the user's own browser.main.jsserves the course on 127.0.0.1 with the same API asserve.py(/api/ping,/api/progress,/api/run-tests; host check; the test command comes fromexercise.json, only the folder name from the page), on a port derived from the app id so the page's localStorage (theme) survives restarts.- In exercise blocks it replaces the player's relative "exercises//" and "cd exercises/" (meant for the course
folder) with the real path, adds an "Open folder" button (
/api/open-exercise) and makes the copy button copy the fixed command. AppRunpoints fontconfig at a config that includes the system's/etc/fonts/fonts.confplus the bundled emoji font as a fallback; the system's own emoji font still wins where installed.- On start it copies new exercises and refreshes each exercise's
exercise.jsonand test files; the learner's own files are never overwritten._solution/folders are not packed, and the starter files are taken fromcourse/data.js(what the page shows as start files), because the course folder'sexercises/may hold someone's solved versions. It adds common toolchain dirs (~/go/bin,/usr/local/go/bin,~/.cargo/bin, …) to PATH, since a desktop session's PATH often lacks them. - One instance per app (a second start focuses the window). Ctrl +/−/0 zoom, Ctrl+R reload, F11 full screen; window
size and zoom are remembered. Chromium's data lives in
~/.local/share/<app-id>/electron. - Options:
--paths,--open-exercises.
To change the wrapper, edit assets/app/main.js (Electron main process, Node standard library only — there is no npm
step) or assets/appdir/AppRun.


