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.
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.
A Windows package stays one library plus the executable. Runtime modules do
not load beside it, because there is no shared engine there for one to load
into, so an app that declares them is packaged with
--static instead.
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 is the output folder itself.
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, so there is no runtime library beside it and no modules/
subfolder. The engine inside is the app's own: 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 candela standard library still travels in libs/, because
scripts read it off disk as they compile whatever links the engine. Copy the
executable, libs/, 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 [dependencies], which is the
only shape that loads a module there.
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
pathorversionmodule. Only the modules that ship with the toolchain (bundled = true) are in the kit; package without--staticand the library is staged beside the executable instead.
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 engine runs each part of the script, and
writes one artifact. A candela script is compiled to bytecode as well, and both
forms go in: the artifact runs the same either way, and the bytecode is what a
runtime shipped without the candela compiler loads. 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 bundle --static works out which subsystems an
app uses and builds a runtime library carrying only those.
It prints the capability set it resolved, builds the trimmed runtime, and
copies the library into out/. 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.
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. - 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.
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.