Packaging an app¶
During development lumenc run parses your markup and CSS on every launch.
That is what makes hot reload work, and it is the wrong thing to ship: a
released app should do the parsing once, before anyone downloads it.
This page covers going from an app directory to something you can hand
someone. The short answer is lumenc package; the rest of the page is the
pieces underneath it and when you would reach for them on their own. It
describes a markup app throughout, and the last section covers what differs for
an app written against one of the SDKs.
Package the app¶
This writes myapp/dist/myapp/: a folder holding your app as an executable,
the Lumen runtime library beside it, lumen.toml, and your app's files at the
same relative paths your markup names them by. Copy that folder to another
machine, run the executable, and the app starts. Nobody needs Lumen installed,
and nobody needs a compiler.
The Lumen runtime library sits in the folder next to the executable. On
Linux and macOS the shared engine and the Rust standard library it was built
against travel beside it, and any runtime
modules the app declares are
staged into a modules/ subfolder: path and bundled modules are copied
from where the declaration points, and a version module comes from the
registry, so the shipped folder carries the exact library lumen.lock pins.
The script host the app runs on is staged there too: lumen-candela, which
runs the bytecode the package compiled the app's candela scripts to, with no
compiler in the folder. A candela package needs no staging: its scripts
compiled into the executable with the app's own. All of it belongs to the app;
keep the folder together when you move it.
The engine is the app's own¶
The engine in the folder carries only the optional subsystems the app uses.
Of the tray, notifications, file dialogs, hotkeys, the HTTP client, the
renderers, the developer tools and the rest, a package keeps the ones the
app's sources call into and leaves the others out, and
[capabilities] names any it
should keep or leave out regardless. The summary line lists both sets:
lumenc package: wrote myapp/dist/myapp/myapp for linux-x86_64 (5 app files
beside it, engine linked for the app with 4 capabilities (layout-taffy,
window-winit, render-cpu, render-gpu) and without 11 capabilities (...))
The engine is linked for the app on the machine doing the packaging, from the
release's engine kit: the recorded link of the toolchain's own engine, which
is replayed with only the app's subsystems on it and without the exports
nothing in the folder uses. No compiler is involved, and the link is quick.
It needs a linker, the same one --static needs: a C toolchain on
Linux, plus the development files of GTK 3 that the engine links against,
the Xcode Command Line Tools on macOS, and the Visual Studio Build Tools with
the "Desktop development with C++" workload on Windows. The first package for
a platform downloads its engine kit and caches it.
When the engine cannot be linked here, the folder carries the toolchain's
full engine and the package says why in one line: no kit was published or
could be downloaded, this lumenc is a build from source rather than the
release the kit belongs to, the package is for another platform, or the link
itself failed (the line quotes the linker). An app whose [capabilities]
turns a subsystem off is stopped instead, since the full engine could not
leave it out.
A subsystem the package leaves out is gone from the folder. If the app's
sources call one of its builtins while [capabilities] turns it off, the
package step warns, and the running app prints a warning the first time the
script makes that call:
lumen: warning: `tray_icon` does nothing here: this package was built without
os-tray. Name it in lumen.toml [capabilities] to package it in.
A Windows package stays one library plus the executable, with any portable
plugins the app declares in modules/ and its candela packages compiled in;
the script hosts its scripts run on are linked into the library.
Runtime modules do not load beside it, because there is no shared engine
there for one to load into, so an app that declares one of the modules that
ship with the toolchain is packaged with
--static instead, which compiles them in.
A runtime module from a path or version source cannot be packaged for
Windows either way; a portable plugin from either source can.
Choose the name and the destination yourself:
To hand the result to someone rather than run it, ask for an archive as well:
That writes myapp/dist/myapp.zip beside the folder, holding the folder
itself, so unpacking it gives the same directory back rather than scattering an
executable and its libraries.
The markup, the stylesheet, and the scripts are compiled into the executable,
so src/ stays behind; a multi-page app compiles every page in.
Everything else in the app directory travels: images, fonts, sounds,
translation catalogues, data files. Dotfiles and a target/ directory are left
behind, and so are the output folder itself and the app's dist/ folder, where
lumenc writes packages and sites by default, wherever this package is
written.
The candela standard library travels too, in a libs/ folder beside the
executable. Scripts read it as they compile, so import "std/..." and the
array methods resolve in the folder the same way they do under lumenc run.
LICENSE, NOTICE and THIRD-PARTY-LICENSES travel beside the executable as
well. A packaged app ships the Lumen engine and the libraries the engine is
built from, and several of those ask that their license text reach whoever
receives the binary. Keep the three files in the folder when you hand it on.
Packaging a markup app needs no Rust toolchain. It copies a prebuilt launcher
and appends your compiled app to the copy. On macOS it links the app in
instead, because a signed Mach-O executable cannot carry trailing data, and
that step needs the Xcode Command Line Tools (xcode-select --install).
Packaging for another platform¶
The targets are linux-x86_64, linux-aarch64, macos-x86_64,
macos-aarch64, windows-x86_64, and windows-aarch64. For a markup app packaging is file
assembly rather than compilation, so any host can produce any of them; an SDK
app is compiled, and what that needs is below.
The runtime library and the launcher for the other platform come from a
published Lumen release: the one you installed, or the newest one if this
lumenc was not installed from a release. They are downloaded once, kept in a
cache keyed by that release, and checked against the checksums published with
it. If you already have those two files, point at them instead and nothing is
downloaded:
A macOS package built from another platform ships the compiled app as a file beside the executable rather than inside it, since linking it in needs a macOS linker. It runs the same way.
An app declaring [dependencies] cross-packages. A bundled module comes
from the target's module archive, downloaded from the same release as the
toolchain files, verified, and cached beside them; a version module comes
from the registry, resolved for the platform being packaged rather than for
yours, so the folder carries that platform's build. A path module cannot
cross-package - a local library is built for one platform - so package that
one on a machine of the target platform instead.
One self-contained executable¶
This writes the same folder with one difference: the app is a single
executable. The engine is inside it, and so is every runtime module the app
declares and the candela host its scripts run on, so there is no runtime
library beside it and no modules/ subfolder. It carries no Rhai or Lua
host, so --static refuses an app with a script in either language. The
engine inside is the app's own, chosen the way a folder
package's is: of the optional subsystems the kit offers (tray,
notifications, dialogs, hotkeys, the HTTP client and the rest), the
executable carries the ones the app's sources show it uses, and
[capabilities] names any it
should carry or leave out regardless. The renderer follows
[render] backend: cpu or gpu links
that one renderer and none of the other, which is the way to keep a small app
small, and auto links both so the app starts on a machine without a GPU. The
candela standard library still travels in libs/, because scripts read it off
disk as they compile whatever links the engine, and the three license files
travel beside the executable as they do in a folder package. Copy the
executable, libs/, the license files, and the app's files, and that is the
whole app.
Reach for it when you want one file to hand over rather than a folder to keep
together, and on Windows when your app declares a module that ships with the
toolchain, which is the only shape that loads one there. Portable plugins are
not linked in; they travel beside the executable in modules/, since they
load into it the way they load into any other host.
It needs a linker on the machine doing the packaging: a C toolchain on Linux
(build-essential, base-devel, or your distribution's equivalent), the
Xcode Command Line Tools on macOS, and the Visual Studio Build Tools with the
"Desktop development with C++" workload on Windows. The first --static
package for a platform also downloads that platform's link kit from the
release channel and caches it, which takes a moment and happens once.
Three things it will not do, each with an exit code of 2 and a message saying so:
- Package an app written against an SDK. Those bring their own executable from their own toolchain.
- Package for a platform other than the one you are on. The link runs through the tools installed here.
- Link a runtime module from a
pathorversionsource. Only the modules that ship with the toolchain (bundled = true) are in the kit; on Linux and macOS, package without--staticand the library is staged beside the executable instead. Candela packages and portable plugins from either source package as usual.
What a packaged app does at startup¶
The executable reads the app compiled into it, opens the runtime library
sitting next to it, and runs; a --static executable skips the opening and
runs what it already carries. Its own directory is the app directory: relative
paths in your markup, and lumen.toml, resolve against it, so the folder works
wherever it is copied.
Pass --headless --ticks N to a packaged app to run it window-free for a fixed
number of ticks, which is how you smoke-test a package in CI.
Compile without packaging¶
lumenc build runs the same compile step and writes the result to a file:
It parses src/main.lmn and the stylesheet, runs the whole cascade, splices
includes and imports, records which language each part of the script is in and
which runtime module runs it, and writes one artifact. A candela script is
compiled to bytecode, and only the bytecode goes in: a shipped app runs it on
the bytecode host, with no compiler and no source. A Rhai or Lua script goes
in as source. Run it back with:
The app directory is still needed here: it supplies lumen.toml and the files
your markup refers to. What the artifact replaces is the parse, not the
directory.
Reach for build when you want the compiled app on its own: to embed it in a
host application through the C ABI, to measure startup without the parser, or
to check that an app compiles at all. Reach for package when you want
something to ship.
Two things follow from compiling ahead of time, whichever command you use:
- Startup skips the parse and the cascade.
- Hot reload is off. There is no source being watched, so edits need a rebuild.
Everything else behaves the same. Colours and metrics stay reachable through CSS variables and design tokens, because the artifact carries the cascaded stylesheet rather than freezing resolved values into the tree.
lumenc check is the fast way to confirm an app parses before you compile it.
Limits of the compiled form¶
An app that reaches for a file no part of the app directory holds keeps pointing at the absolute path it was given, which will not exist on anyone else's machine. Keep what your app needs inside the app directory.
A multi-page app compiles whole: every page goes into the
executable along with the page names navigation resolves against, so a packaged
app routes without carrying any .lmn files. Adding a page then means
rebuilding, since there are no page files left to reload.
Archive the app's files¶
This packs every regular file under the app directory into one .lpak
archive, skipping dotfiles, target/, and src/; an archive holds the app's
assets, and the code is compiled rather than read by name. Use it when you want
those files as a single addressable blob rather than a folder, for instance to
serve them from one file or to keep a build output tidy.
Run against the archive with --assets:
Every image, icon, and sound the markup names is then read out of the archive. Lookups are keyed by the path relative to the app directory, the same path you write in the markup, so nothing in the app changes. A file the archive does not carry still comes from disk, which lets you keep one loose file for a quick edit without repacking. Fonts are the exception: they load through the system font database and are read from disk even when the archive carries them.
Pair it with an artifact to run an app as two files plus lumen.toml:
Rebuild the archive whenever an asset changes. A missing or corrupt archive stops the run rather than quietly falling back to the directory.
Trim the runtime¶
An app that makes no network calls and opens no file dialogs does not need
the code for either. lumenc package already leaves such subsystems out of
the engine it ships; lumenc bundle --static instead compiles a runtime
library from Lumen's source with only the app's subsystems in it.
It prints the capability set it resolved, builds the trimmed runtime, and
copies the library into out/ with the source tree's LICENSE, NOTICE and
THIRD-PARTY-LICENSES beside it. Put that library in a package in place of the
one lumenc package copied, and the packaged app opens the trimmed build
instead.
Detection is deliberately cautious: a subsystem is dropped only on a clear
signal that the app never uses it, and anything ambiguous is kept. Override
either direction in lumen.toml:
See the capabilities table for what each key covers.
This command compiles the runtime from Lumen's source, so it needs a copy of
that source tree; point LUMEN_WORKSPACE_DIR at it. Without one, the command
still prints the resolved capability set and then stops.
Build steps your app needs¶
Some apps need something built before they can run: a C library the script
loads, a generated data file, a downloaded asset. Declare those as hooks in
lumen.toml and Lumen runs them for you.
[[hooks]]
when = "prebuild"
os = "linux"
run = "cc -shared -fPIC -O2 -o libmd.so md.c"
inputs = ["md.c"]
outputs = ["libmd.so"]
A prebuild hook runs before lumenc run, build, bundle, and package. A
prerun hook runs only before lumenc run, after every prebuild hook.
lumenc check never runs hooks, so a check stays free of side effects. A
candela script that loads a C library a hook builds is still checked: the
dylib block's own declarations are what the calls are checked against, and
the check opens no library. The
check reference lists the forms that do need the
library built first.
Listing inputs and outputs makes the hook skippable: when the outputs are
already newer than the inputs, the command does not run again. Leave either
list out and the hook runs every time.
Hooks run in declaration order with the app directory as their working
directory, and a failing hook stops the command. Give a hook an os when it
only makes sense on one platform, as in the example above; declare one entry
per platform to cover them all.
A hook is a shell command that a lumen.toml asks for, so treat an app from
someone else the way you would treat a project with a build script. Pass
--no-hooks to any of run, build, bundle, or package to skip them.
The full key list is in the lumen.toml reference.
Apps written against an SDK¶
An app authored with the Rust, C++, or Python SDK is a program in that
language, not a markup directory, and it is built by that language's own
toolchain. lumenc run, lumenc build, and lumenc package all detect one
from its contents, or from [app] kind in lumen.toml, and hand the build to
cargo, cmake, or the interpreter.
lumenc package then assembles the same folder around whatever that build
produced, so packaging one is the same command:
Every kind produces an executable, and the runtime library goes beside it, the same as for a markup app. How the executable is produced is what differs:
- Rust.
cargo build --releaseruns, and the binary it reports is copied in under your app's name. On Linux and macOS a Rust app links the engine rather than compiling a copy into itself, so the executable is small and the engine travels beside it, out of the same build. On Windows the runtime is inside the executable and nothing travels with it. That engine is whatever the app's own cargo build linked, so[capabilities]does not trim it, and a package of a Rust app with the table set says so. - C++. CMake configures and builds, and the executable from the build tree is copied in. If the project builds more than one executable, the most recent one is packaged and the others are named on the way past; give the app its own directory to keep that unambiguous.
- Python. The app is frozen into an executable with
PyInstaller, which bundles the interpreter and the
app's modules into one file. Install it first (
pip install pyinstaller).
Unlike a markup app, an SDK app reads its markup, stylesheet, and scripts at run time, so those files travel with it. What stays behind is the source it was compiled from and the build tree that compile left.
A C++ or Python app gets an engine linked for it the way a markup app does,
with one difference: its program can drive any subsystem through the C ABI,
and the source scan cannot read it, so the engine keeps every subsystem an
app's code could call. Only the development ones and the renderer
[render] backend does not pick are left out, and [capabilities] decides
the rest.
Cross-packaging an SDK app¶
--target works here too, and the SDK's own toolchain does the compiling:
For a Rust app the target triple is passed to cargo, so
rustup target add aarch64-unknown-linux-gnu is what makes it work. For a C++
app, set CMAKE_TOOLCHAIN_FILE to a toolchain file for that platform; there is
nothing Lumen can supply in its place, so packaging says so rather than
building this machine's binary under another platform's name. A Python app is
frozen against the interpreter doing the freezing and can only be packaged for
the platform you are on.
A Rust app needs nothing from the release channel: the engine it links comes
out of its own cargo build, so rustup target add is the whole requirement.
The other kinds open the C library, and that one is fetched for the platform
you asked for.
Every flag on every command here is in the CLI reference.