Bundling And Standalone Executables
geblang build produces a self-contained binary from a Geblang package. The
resulting file runs on any machine with the same OS and architecture as the
machine that built it. No Geblang installation, no stdlib directory, and no
geblang.yaml manifest are required on the target machine.
How Bundling Works
A Geblang bundle is a standard executable with a zip archive appended to it.
┌──────────────────────────────────────────────────┐
│ geblang interpreter binary (ELF/Mach-O/PE) │
├──────────────────────────────────────────────────┤
│ zip archive │
│ BUNDLE.json manifest │
│ src/app/main.gb source files │
│ src/app/main.gbc precompiled bytecode │
│ stdlib/collections.gb bundled source stdlib │
│ stdlib/collections.gbc │
│ ... │
├──────────────────────────────────────────────────┤
│ 8-byte zip size (little-endian uint64) │
│ 4-byte magic: GEBX │
└──────────────────────────────────────────────────┘
At startup the interpreter checks whether a 12-byte trailer is present and
whether it contains the GEBX magic value. If so, it reads the zip, parses
BUNDLE.json, and runs the bundled entry module instead of looking for
command-line arguments.
The zip archive is a standard zip file and can be inspected with unzip -l or
any zip reader.
What Gets Bundled
geblang build walks the import graph starting from the entry module and
collects every non-native module it can reach:
- User source modules are included under
src/inside the zip. - Imported package dependencies declared in
geblang.yamlare included when they are installed undervendor/<name>/and reached by the import graph. - Source-stdlib modules (Geblang-written standard library files) are
included under
stdlib/inside the zip. - Native modules - Go-backed modules like
io,sys,collections,http,db, and so on - are part of the interpreter binary itself and are not duplicated in the zip.
Each collected source file is also compiled to bytecode and stored alongside it
as a .gbc file. The precompiled bytecode is loaded directly into the
interpreter's bytecode cache on first run, so the bundled program starts at
full speed with no warm-up compilation step.
Only imported modules are bundled. geblang build does not copy the entire
package directory, the entire vendor/ directory, test files, or generated
artifacts. If a package dependency is declared but no module from that package
is imported, it is not included in the bundle. Non-code files (templates, static
assets, data files) are embedded only when listed under resources: in the
manifest; see Embedding Resources.
Embedding Resources
Programs often ship non-code files alongside their source: HTML templates,
static assets, data files. List them under resources: in geblang.yaml and
geblang build embeds them in the bundle ZIP at their project-relative path,
next to src/ and stdlib/.
name: myapp
source: src
resources:
- templates # a directory: embedded recursively
- static # another directory
- data/*.json # a glob: matched files only
Each entry is either a directory (embedded recursively) or a glob pattern. A
pattern that matches nothing is a build error, so a typo fails loudly rather
than silently shipping an empty bundle. Resource paths may not collide with the
reserved src/ or stdlib/ bundle directories.
At run time a program finds its embedded files through sys.bundleDir(), which
returns the bundle's extract directory (see First-Run
Extraction) or the empty string when the program is not
running from a bundle. Resolve resources against it, falling back to the project
directory in development, so the same code path works in both cases:
import io;
import sys;
func loadTemplate(string name): string {
let base = sys.bundleDir();
if (base == "") { base = "."; } /* dev: read from the project tree */
return io.readText(base + "/templates/" + name);
}
Because resources keep their project-relative path inside the bundle, the same
relative path (templates/page.html) resolves correctly whether base is the
project directory in development or the extract directory in a built binary.
Package Layout
A bundle is built from a Geblang package. The package needs:
- A
geblang.yamlmanifest that declares the package name and source root. - At least one source module that exports a
main(list<string> args)function to serve as the entry point.
Minimal package layout:
myapp/
geblang.yaml
src/
myapp/
main.gb
geblang.yaml:
name: myapp
source: src
src/myapp/main.gb:
module myapp.main;
import io;
import sys;
export func main(list<string> args): void {
io.println("Hello from a bundled app!");
}
The entry module name must match a module declaration that the package
resolver can find. In this layout the canonical name is myapp.main.
The same entry file runs directly during development: geblang <file>
auto-invokes an exported top-level main when the file declares one, forwarding
command-line arguments and using an int return value as the exit code. So a
module that export func main(list<string> args) behaves identically whether
run directly or built. A file with no exported main runs as a plain script.
Running geblang build
geblang build --entry <module.name> --out <output-path> [<package-dir>]
| Argument | Required | Description |
|---|---|---|
--entry |
yes | Canonical name of the entry module (must export main) |
--out |
yes | Path for the output binary |
--resource |
no | Extra resource to embed, in addition to the manifest resources:. Repeatable. --resource <path> keeps the project-relative path; --resource <path>=<bundlePath> remaps it (a directory's contents mirror under <bundlePath>), so a build step can embed processed copies without altering the source tree |
--allow-ffi |
no | Bake an FFI allow-list entry (path or glob) into the binary. Repeatable. Adds to the manifest permissions.ffi. See Capabilities below |
--allow-onnx |
no | Bake the local ONNX inference capability into the binary |
--allow-process-control |
no | Bake the privileged process-control capability into the binary |
--allow-browser |
no | Bake the headless-browser automation capability into the binary |
--docker |
no | Also write a Dockerfile next to the binary (see below) |
--docker-port |
no | Add EXPOSE <port> to the generated Dockerfile |
--force |
no | Overwrite an existing generated Dockerfile |
<package-dir> |
no | Package root directory (default: .) |
Build the example package above:
geblang build --entry myapp.main --out ./dist/myapp ./myapp
Or from inside the package directory:
cd myapp
geblang build --entry myapp.main --out ../dist/myapp
The output binary is set executable (chmod 755). On Unix you can run it
directly:
./dist/myapp
Capabilities in built binaries
Privileged capabilities - FFI, local ONNX inference, and process control - are
off by default and are normally turned on with a launch flag (--allow-ffi,
--allow-onnx, --allow-process-control, --allow-browser). A built binary has no launch-flag
step, so it carries its capabilities baked in at build time and just runs.
Declare them in geblang.yaml, the reproducible source of truth:
permissions:
ffi:
enabled: true
libraries:
- glob: /opt/torch/lib/*.so
onnx: true
processControl: true
browser: true
This same block also enables those capabilities for geblang run / geblang test in the project, so dev and the built binary behave identically.
Alternatively, pass them to geblang build for an ad-hoc build; the flags add to
whatever the manifest already declares:
geblang build --entry app.main --out ./dist/app \
--allow-ffi '/opt/torch/lib/*.so' --allow-onnx --allow-process-control --allow-browser
The resolved capability set is recorded in the bundle, so the end user runs the
binary with no flags. A binary built without any of these stays locked down: a
gated call throws PermissionError, exactly as an unflagged geblang run would.
Third-party notices
Alongside the binary, geblang build writes a <output-path>.NOTICES.txt
file containing the third-party attribution notices for the components the
binary embeds (the Geblang runtime and its dependencies). Ship this file with
the binary to keep the distribution licence-compliant. The same notices are
also available at runtime through the built-in --notices flag (below). The
same applies to binaries produced by gebweb build, which delegates to
geblang build.
Standard flags and program arguments
Every built binary recognises a few standard flags, but only when one is the first argument:
--help/-hprints the binary's usage.--versionprints<app> <version> (geblang <runtime-version>).--noticesprints the embedded third-party notices.
Because these are double-dash flags checked only in first position, they never
clash with a bare-word subcommand (such as a licenses or version command)
or any argument your own program defines: those are forwarded unchanged. A
literal -- also forwards everything after it verbatim, so
./dist/myapp -- --version passes --version to your program instead of
printing the Geblang version.
Otherwise, pass arguments the same way you would to any other binary:
./dist/myapp --port 8080 --verbose
The argument list is forwarded to the entry module's main function via
sys.args().
Docker Output
geblang build --docker writes a Dockerfile into the output directory
alongside the binary (1.19.0). The image copies the binary and its
NOTICES sidecar into gcr.io/distroless/base-debian12 (glibc included -
the binary links libc dynamically) and runs it as the entrypoint:
geblang build --entry app.main --out dist/app --docker --docker-port 8085 .
cd dist && docker build -t myapp . && docker run -p 8085:8085 myapp
EXPOSE is only emitted when --docker-port is given - a built binary is
not necessarily a server. An existing Dockerfile is left unchanged
unless --force is passed, so manual edits survive rebuilds. Arguments
after docker run <image> flow to the binary as normal program
arguments; the standard flags below work inside the container too. The
typical reverse-proxy deployment runs the container on an internal port
and fronts it with nginx.
Cross-Platform Builds
geblang build embeds the running runtime into the output, so by default the
binary it writes targets the platform you run it on. To build for another
platform, embed the bundle into a runtime compiled for that target instead:
geblang build --runtime <path> reads the runtime at that path rather than the
running one. Because the runtime is pure Go with no cgo it cross-compiles to any
supported target with the Go toolchain, and the bundle itself is platform-
independent, so any host can produce a binary for any target.
The shipped helper scripts/cross-build.sh does both steps (cross-compile the
runtime, then embed the bundle) from a source checkout; the Go toolchain is the
only requirement:
scripts/cross-build.sh --target linux/amd64 --entry app.main --out build/app
scripts/cross-build.sh --target darwin/arm64 --entry app.main --out build/app
scripts/cross-build.sh --target windows/amd64 --entry app.main --out build/app.exe
Any host (Linux, macOS, Windows) can build for linux, darwin, or windows
on amd64 or arm64. The package directory is set with --dir (default: the
current directory); extra build flags go after --:
scripts/cross-build.sh --target linux/amd64 --entry app.main --out build/app -- --no-assert
Windows limitations
Geblang builds and runs on Windows, but a few unix-oriented capabilities are unavailable there and report a clear error at runtime; programs that do not use them run unchanged:
- FFI (
ffi,clib.*) and local ONNX inference (onnx) load native shared libraries through dlopen, which is unix-only here. - Advisory file locking (
io.lock/io.tryLock) is unix-only. - The
hnswvector-store backend uses an exact brute-force index on Windows rather than the approximate HNSW graph; results are exact and performance differs on very large indexes. - Interactive console widgets (
cli.choose/cli.multiChoose) and the REPL line editor fall back to plain line input (no raw-key handling).
Standard Flags Of Built Binaries
Every built binary answers a small set of standard flags, recognised only when they are the FIRST argument:
| Flag | Effect |
|---|---|
--help, -h |
Application name/version, usage, and this flag list |
--version |
<name> <version> (geblang <engine version>), from geblang.yaml |
--notices, --licences |
Prints the embedded third-party licence notices |
-- |
Passes everything after it to the application untouched |
Any other first argument (and all later arguments) flow to the
application's main(list<string> args) unchanged, so an application
that defines its own --help can still receive it via
./app -- --help.
Full Example: A CLI Tool
Package layout:
greet/
geblang.yaml
src/
greet/
main.gb
formatter.gb
geblang.yaml:
name: greet
source: src
src/greet/formatter.gb:
module greet.formatter;
export func format(string name): string {
return "Hello, " + name + "!";
}
src/greet/main.gb:
module greet.main;
import io;
import sys;
import greet.formatter as fmt;
export func main(list<string> args): void {
if (args.length() == 0) {
io.println("Usage: greet <name>");
sys.exit(1);
}
io.println(fmt.format(args[0]));
}
Build and run:
geblang build --entry greet.main --out ./greet-bin ./greet
./greet-bin Ada
# Hello, Ada!
Both greet.main and greet.formatter are discovered automatically by the
import-graph walk and bundled together.
Bundling Package Dependencies
Geblang's package installer places git dependencies under vendor/. Bundling
uses the same package resolver as normal execution, so imported dependency
modules are included automatically once they are installed.
Example application manifest:
name: webapp
source: src
dependencies:
authlib:
git: https://example.com/authlib.git
version: main
After running geblang install, the dependency is available at:
webapp/
geblang.yaml
src/
main.gb
vendor/
authlib/
geblang.yaml
src/
authlib/
tokens.gb
Application code can import it normally:
module webapp.main;
import io;
import authlib.tokens as tokens;
export func main(list<string> args): void {
io.println(tokens.issue("ada"));
}
Build the application:
geblang build --entry webapp.main --out ./webapp-bin ./webapp
The resulting executable contains webapp.main, authlib.tokens, any other
non-native modules reached from those imports, and the source stdlib modules
they need. The target machine does not need vendor/, geblang.yaml, or a
separate Geblang installation.
geblang build does not fetch missing dependencies. If geblang.yaml declares
a git dependency and vendor/<name>/ is absent, run geblang install before
building.
Full Example: A Web Server
webapi/
geblang.yaml
src/
webapi/
main.gb
routes.gb
geblang.yaml:
name: webapi
source: src
src/webapi/routes.gb:
module webapi.routes;
import http;
import web;
export func register(any app): void {
web.get(app, "/", func(dict<string, any> request): dict<string, any> {
return http.jsonResponse({"status": "ok"});
});
}
src/webapi/main.gb:
module webapi.main;
import io;
import http;
import web;
import webapi.routes as routes;
export func main(list<string> args): void {
let port = args.length() > 0 ? args[0] : "8080";
let app = web.new();
routes.register(app);
io.println("Listening on :" + port);
http.serve("127.0.0.1:" + port, func(dict<string, any> request): dict<string, any> {
return web.handle(app, request);
});
}
Build a distributable API server:
geblang build --entry webapi.main --out ./webapi-server ./webapi
./webapi-server 9000
First-Run Extraction
On the first invocation of a bundled binary, the zip is extracted to a temporary directory under the system temp path:
/tmp/geblang-<hash>/
src/
...
stdlib/
...
The directory name includes a SHA-256 hash of the zip contents, so different bundle versions use different directories and never collide. On subsequent runs, the extracted directory already exists and extraction is skipped - the startup overhead is paid only once.
Precompiled bytecode is loaded into the interpreter's bytecode cache at extraction time so recompilation is avoided even after the cache directory is cleared.
Inspecting A Bundle
Because the bundle is a valid zip appended to the binary, standard tools work:
# List bundled files
unzip -l ./dist/myapp
# Extract the source files manually
unzip ./dist/myapp -d ./bundle-contents
BUNDLE.json inside the zip records the entry module name, the Geblang version
the bundle was built with, and the canonical name, zip path, and source hash
for every bundled module.
Native Compilation (Experimental)
Experimental and unstable.
geblang build --nativeis a preview of an ahead-of-time path that compiles a Geblang program to a standalone native binary by transpiling it to Go. The set of supported language features and stdlib modules below WILL change between releases, and the flag, its output, and its diagnostics are not yet covered by any stability guarantee. For production builds use plaingeblang build(the bundled-interpreter binary), which supports the whole language.
Instead of bundling the interpreter, geblang build --native lowers the entry
program (and the stdlib modules it uses) to self-contained Go source and
compiles it with the local Go toolchain. For dispatch-bound code (tight loops,
recursion, virtual method dispatch) the result runs several times faster than
the bundled VM; gains shrink for allocation- or string-heavy code.
geblang build --native --entry <module> --out <path> [<package-dir>]
It requires a local Go toolchain at least as new as the one that built your
geblang, and builds offline (no module downloads). The entry convention is the
same as plain geblang build: the entry module must export func main() (or
export func main(list<string> args), optionally returning : int); a missing
main is a build error.
Safety: it fails at build time, never silently
The native compiler is a growing subset of the language. When it meets a feature
or stdlib function it does not yet support, it fails the build with a clear
diagnostic naming the file, line, and reason, and writes no binary. It never
emits a binary that behaves differently from the interpreter: supported programs
are byte-for-byte identical to geblang run / geblang test, and unsupported
ones do not build. So it is safe to try --native on any program and fall back
to geblang build if it reports something unsupported.
What is supported
- The core language: functions, classes (inheritance and virtual dispatch),
interfaces, generics, enums (data variants, instance methods, interface
implementation, and the
values()/fromName()static surface),match(type / list / enum / guard patterns), exceptions, generators, async/await, comprehensions, destructuring, closures, optional chaining, spread, named/optional/variadic arguments,with, string interpolation, slicing, and dynamic navigation ofany-typed values (indexing andascasts, e.g. over ajson.parseresult). - Standard-library modules backed by the Go standard library:
io,sys,collections,math,json,strings(including the regex methods),encoding,crypt(the hash functions),time,random,re(including compiled patterns),bytes,url(including theURLobject),csv,xml,datetime(functional and object surfaces),template,reflect.
What is not supported yet (these diagnose, use geblang build)
- Modules backed by third-party libraries:
yaml,toml,uuid,markdown,pcre, andunicodenormalization. - Stateful / I/O modules:
db,http,net,sockets,log, messaging, and similar. - Calling a method on an
any-typed value (cast it to a concrete type first); assigning into anany-typed index. - Arbitrary-precision integers: native arithmetic uses a fast machine-width path that wraps on overflow rather than promoting to big integers.
- Partial-application
_placeholder arguments: use a typed wrapper function instead.
Performance note
Repeated string concatenation in a loop (acc = acc + part) is O(n^2) in the
generated Go because Go strings are immutable, so a tight concat loop can be
slower than the VM. Build large strings with a list and join.
Limitations
- OS and architecture: a built binary runs on the OS and architecture it
was produced for. Cross-compilation IS supported:
geblang build --runtime <path>embeds the bundle into a runtime compiled for another target, and the shippedscripts/cross-build.shhelper does both steps (the runtime is pure Go and cross-compiles withCGO_ENABLED=0). See "Cross-Platform Builds" above. - No hot-reload: Bundled source is extracted once and cached. Changes to the original source files have no effect on a built bundle.
- Import-graph walking:
geblang builddiscovers modules by parsing import statements statically. Dynamic module loading (using string variables as import paths) is not currently traversed. Any module loaded this way must be imported explicitly elsewhere in the package so it is included in the bundle. - Vendored dependencies: imported modules from installed package
dependencies are bundled, but unimported files under
vendor/are not copied wholesale. - Native extensions: Go-backed
ext.*extensions are not bundled. They must be present on the target machine alongside the bundle binary.