Projects
Maggie projects are organized around a manifest file, maggie.toml, that
declares metadata, source layout, dependencies, and build settings. The
project system handles dependency resolution, namespace mapping, and image
generation -- everything needed to go from a collection of .mag files to
a reproducible, distributable application.
This chapter covers the maggie.toml format, dependency management, project
layout conventions, and the CLI commands that tie it all together. Most of
the content here is configuration and tooling rather than Maggie expressions,
so examples are shown as TOML snippets, directory trees, and shell commands.
'maggie.toml' includes: 'toml' >>> true
The Project Manifest
Every Maggie project starts with a maggie.toml file in the project
root. This is the single source of truth for how the project is built,
what it depends on, and where its source lives.
The manifest has four main sections:
[project]-- name, namespace, and version[source]-- where to find source files and the entry point[dependencies]-- external libraries the project uses[image]-- settings for image persistence
Here is a minimal manifest:
[project]
name = "my-app"
namespace = "MyApp"
version = "0.1.0"
[source]
dirs = ["src"]
entry = "Main.start"
When mag is invoked without explicit file paths, it looks for a
maggie.toml in the current directory (or walks up the directory tree
to find one). If found, it automatically loads the project with all
its dependencies.
# These are equivalent when maggie.toml exists:
mag -m Main.start
mag ./src/... -m Main.start
The [project] Section
The [project] section identifies the project and sets its root
namespace. It also supports optional metadata fields.
[project]
name = "weather-service"
namespace = "Weather"
version = "1.2.0"
description = "A weather data aggregation service"
license = "MIT"
authors = ["Alice <alice@example.com>"]
repository = "https://github.com/example/weather-service"
maggie = ">=0.10.0"
name -- a short identifier for the project. Used in dependency
resolution when other projects depend on this one. By convention,
use lowercase with hyphens (e.g., my-app, ui-toolkit).
namespace -- the root namespace for all classes defined in this
project. Classes in src/models/User.mag will be registered under
Weather::Models::User in the global class table. This namespace
is also what consumers see when they import this project as a
dependency.
version -- a semver-style version string.
description, license, authors, repository -- optional
metadata fields. Currently informational (useful for mag help and
future package registries).
maggie -- a version constraint on the Maggie toolchain. If set,
the manifest is rejected at load time if the running mag version
does not satisfy the constraint. Supported forms: >=X.Y.Z,
<X.Y.Z, >=X.Y.Z <A.B.C (range), ~X.Y.Z (pessimistic --
allows patch updates within the same minor version).
If namespace is omitted, it defaults to the PascalCase form of
the project name. A project named weather-service would get the
namespace WeatherService.
The [source] Section
The [source] section tells the compiler where to find .mag files
and which method to call after loading.
[source]
dirs = ["src"]
entry = "Main.start"
exclude = ["*_scratch.mag", "*_test.mag"]
dirs -- a list of directories (relative to the project root) that
contain Maggie source files. All .mag files in these directories are
loaded recursively. Defaults to ["src"] if omitted.
You can list multiple source directories:
[source]
dirs = ["src", "lib", "generated"]
entry = "Main.start"
entry -- the method to call after all source files are loaded.
This is resolved relative to the project namespace. For a project
with namespace MyApp, an entry of Main.start calls the start
class method on MyApp::Main.
exclude -- a list of glob patterns. Files matching any pattern
are skipped during compilation. Patterns are matched against both
the filename and the relative path from the source directory.
Uses filepath.Match syntax (e.g., *_test.mag, scratch_*.mag).
The entry point is invoked with the -m flag or the mag run
subcommand:
mag -m Main.start # Calls MyApp::Main.start
mag run # Run the default entry point
mag run -t server # Run a named target
You can also specify an entry point directly on the command line, overriding whatever is in the manifest.
The [dependencies] Section
Dependencies are external libraries that your project uses. Each dependency has a name (the TOML key) and a source specification.
Maggie supports two kinds of dependencies:
- Git dependencies -- cloned from a remote repository
- Path dependencies -- local filesystem paths
[dependencies]
yutani = { git = "https://github.com/chazu/yutani-mag", tag = "v0.5.0" }
shared-lib = { path = "../shared-lib" }
bleeding = { git = "https://github.com/example/lib", branch = "main" }
pinned = { git = "https://github.com/example/exact", commit = "abc123" }
Git dependencies require a git URL and exactly one ref specifier:
tag, branch, or commit. These are mutually exclusive -- if
you specify more than one, the manifest is rejected at load time.
- tag -- checks out a specific git tag (most common for releases)
- branch -- tracks a branch; the lock file pins the exact commit
- commit -- pins an exact commit hash
Path dependencies use a path relative to the project root (or an
absolute path). These are useful during development when you want to
work on a library and its consumer simultaneously.
Each dependency can also specify an explicit namespace override:
[dependencies]
ui-toolkit = { path = "../ui-toolkit", namespace = "CustomUI" }
Without the override, the namespace is resolved automatically (see the section on namespace mapping below).
Dev-Dependencies
The [dev-dependencies] section declares dependencies that are only
needed during development and testing. They use the same format as
regular dependencies but are excluded from production builds.
[dev-dependencies]
test-helpers = { path = "../test-helpers" }
mock-server = { git = "https://example.com/mock", tag = "v1.0.0" }
Dev-dependencies are loaded when running mag test, using the REPL
(mag -i), or in other development contexts. They are excluded from
mag build production binaries. A dependency name cannot appear in
both [dependencies] and [dev-dependencies].
The [image] Section
The [image] section controls how VM images are saved. An image is
a snapshot of the entire VM state -- all classes, methods, and globals
-- serialized to a file that can be loaded instantly on startup.
[image]
output = "my-app.image"
include-source = true
output -- the filename for the saved image, relative to the project root.
include-source -- when true, method source text is preserved in
the image. This enables Compiler fileOut:to: to reconstruct source
files from a running image. When false, source text is stripped to
reduce image size.
Saving an image from the command line:
# Build and save image from project
mag --save-image my-app.image
# Run from a saved image instead of compiling source
mag --image my-app.image -m Main.start
Images are useful for deployment (skip compilation on startup) and for development (save and restore VM state across sessions).
Dependency Management with mag deps
The mag deps subcommand manages project dependencies. It reads
maggie.toml, resolves all dependencies (including transitive ones),
fetches any missing git repositories, and writes a lock file.
# Resolve and fetch all dependencies
mag deps
# Re-resolve, ignoring the lock file (fetch latest for each tag)
mag deps update
# Show the dependency tree
mag deps list
mag deps -- the default action. For each git dependency, it clones the repository if it does not exist locally, or skips the fetch if the locked commit matches the requested tag. For path dependencies, it verifies the path exists. Transitive dependencies (dependencies of dependencies) are resolved recursively.
mag deps update -- re-resolves all dependencies from scratch,
ignoring the existing lock file. Use this after changing dependency
versions in maggie.toml or when you want to pick up upstream
changes for a tag that has been force-pushed.
mag deps list -- prints the resolved dependency tree, showing each dependency's name, source, resolved namespace, and any transitive dependencies.
Dependencies are loaded in topological order: transitive dependencies before direct dependencies, and all dependencies before the project itself. This ensures that when a class references a dependency class, the dependency class is already registered.
Test Configuration
The [test] section configures test execution. It declares where
test source files live, which entry point to call, and an optional
timeout.
[test]
dirs = ["test"]
entry = "TestRunner.run"
timeout = 30000
exclude = ["*_slow.mag"]
dirs -- directories containing test source files. These are
compiled in addition to the project's [source].dirs.
entry -- the class method to call when running mag test. The
entry point should return a small integer exit code (0 = pass).
timeout -- maximum time in milliseconds for the test run. If
exceeded, mag test exits with code 2. Set to 0 (or omit) for no
timeout.
exclude -- glob patterns to exclude from test sources.
Running tests:
mag test # run tests using [test] config
mag test --timeout 5000 # override timeout
mag test --entry OtherRunner.run # override entry point
Dev-dependencies are loaded automatically when running mag test.
Script Hooks
The [scripts] section defines lifecycle hooks that run shell
commands at build and test boundaries. Empty strings are no-ops.
[scripts]
prebuild = "mag fmt --check"
postbuild = "echo Build complete!"
pretest = "echo Running tests..."
posttest = "echo Tests done!"
prebuild -- runs before mag build compiles sources.
postbuild -- runs after mag build writes the binary.
pretest -- runs before mag test executes the test entry point.
posttest -- runs after mag test completes.
Scripts run via sh -c in the project directory. If a prebuild or
pretest script fails (non-zero exit), the build or test is aborted.
Postbuild and posttest failures are reported as warnings.
Multi-Target Builds
Projects can declare multiple build targets using [[target]] array
tables. Each target has its own entry point, output binary name, and
optional source directory and go-wrap overrides.
[source]
dirs = ["src"]
entry = "Main.start"
[[target]]
name = "server"
entry = "MyApp::Server.start"
output = "my-server"
full = true
[[target]]
name = "cli"
entry = "MyApp::CLI.main"
output = "my-cli"
extra-dirs = ["tools"]
exclude-dirs = ["admin"]
Each target inherits the project's [source].dirs and adds its own
extra-dirs (or removes dirs listed in exclude-dirs). The
[source].exclude patterns and any target.exclude patterns are
combined. Go-wrap packages from both the top-level [go-wrap] and
target.go-wrap are merged.
Building targets:
mag build # build the default (first) target
mag build -t server # build a specific target
mag build --all # build all targets
mag build -t cli -o /usr/local/bin/mycli
Running targets:
mag run # run the default entry point
mag run -t server # run a named target
If no [[target]] sections are defined, the project behaves as
before -- a single implicit target derived from top-level config.
Existing manifests require no changes.
The Lock File
When mag deps resolves dependencies, it writes a lock file at
.maggie/lock.toml. This file records the exact commit hash for
each git dependency, ensuring reproducible builds across machines
and over time.
# .maggie/lock.toml (auto-generated, do not edit)
[[deps]]
name = "yutani"
git = "https://github.com/chazu/yutani-mag"
tag = "v0.5.0"
commit = "a1b2c3d4e5f6..."
[[deps]]
name = "shared-lib"
path = "../shared-lib"
Key points about the lock file:
- It is auto-generated by
mag deps. Do not edit it by hand. - Commit it to version control for reproducible builds.
mag deps updateregenerates it from scratch.- Path dependencies record only the path (no commit hash).
- If the lock file is missing,
mag depscreates one fresh.
The lock file lives inside the .maggie/ directory alongside
fetched dependencies:
my-app/
maggie.toml
.maggie/
lock.toml # locked dependency versions
deps/
yutani/ # cloned git repository
shared-lib/ # resolved path (symlink or absolute)
src/
Main.mag
Dependency Namespace Mapping
Every dependency is mapped to a namespace that determines how its classes appear in the global class table. The namespace is resolved using a three-level priority order:
1. Consumer override -- an explicit namespace in the
dependency declaration in your maggie.toml
2. Producer manifest -- the [project].namespace in the
dependency's own maggie.toml
3. PascalCase fallback -- the dependency name converted to
PascalCase (e.g., my-lib becomes MyLib)
[dependencies]
# Uses yutani's own maggie.toml namespace (e.g., "Yutani")
yutani = { git = "https://github.com/chazu/yutani-mag", tag = "v0.5.0" }
# Consumer override: maps to "CustomUI" regardless of producer manifest
ui-toolkit = { path = "../ui-toolkit", namespace = "CustomUI" }
# No manifest, no override: PascalCase("my-lib") = "MyLib"
my-lib = { path = "../my-lib" }
All classes from a dependency are prefixed with its resolved
namespace. A dependency with namespace Yutani containing
src/widgets/Button.mag registers its class as
Yutani::Widgets::Button in Globals.
When a consumer overrides a dependency's namespace, internal imports
within that dependency are automatically remapped. For example, if
Yutani internally imports Yutani::Events and the consumer overrides
to ThirdParty::Yutani, that import becomes
ThirdParty::Yutani::Events.
If two dependencies map to the same namespace, mag reports a hard
error before loading begins, listing all collisions and suggesting
that you add namespace overrides to resolve them.
Reserved Namespaces
Certain namespaces are reserved because they collide with core VM class names. You cannot use any of these as the root segment of a dependency namespace:
Object, Class, Boolean, True, False, UndefinedObject, SmallInteger, Float, String, Symbol, Array, Block, Channel, Process, Mutex, WaitGroup, Semaphore, CancellationContext, Result, Success, Failure, Dictionary, Character, Compiler, File, Error, Exception, and others.
Only the root segment is checked. namespace = "Array" is rejected,
but namespace = "MyLib::Array" is fine because the root segment is
MyLib.
If a dependency name happens to match a reserved namespace via the
PascalCase fallback (e.g., a dependency named array), you must add
an explicit namespace override:
[dependencies]
# This would fail: PascalCase("array") = "Array" (reserved)
# array = { path = "../array-utils" }
# Fix: add a namespace override
array = { path = "../array-utils", namespace = "ArrayUtils" }
'Object' = 'Object' >>> true
Project Layout Conventions
Maggie uses a directory-as-namespace convention. When loading files from directories, namespaces are derived automatically from the directory structure relative to the source root.
my-app/
maggie.toml # namespace = "MyApp"
src/
Main.mag # MyApp::Main (or just Main)
models/
User.mag # MyApp::Models::User
Post.mag # MyApp::Models::Post
views/
UserView.mag # MyApp::Views::UserView
utils/
StringHelpers.mag # MyApp::Utils::StringHelpers
Path segments relative to the source directory become PascalCase
namespace segments joined by ::. A file at src/models/User.mag
in a project with namespace MyApp gets the full namespace
MyApp::Models.
An explicit namespace: declaration at the top of a .mag file
overrides the directory-derived namespace. This is useful when the
desired namespace does not match the directory structure.
Root-level files (directly in the source directory) use only the project namespace, with no additional segments.
A recommended project layout:
my-app/
maggie.toml # project manifest
.maggie/ # managed directory (gitignore this)
deps/ # fetched dependencies
lock.toml # locked versions
src/ # main source directory
Main.mag # entry point class
models/ # domain model classes
views/ # UI or presentation classes
services/ # business logic
test/ # test files
lib/ # shared library code
The .maggie/ directory is managed by the toolchain and should be
added to .gitignore. It contains fetched dependencies and the
lock file (the lock file itself should be committed, but the rest
of .maggie/ should not).
Entry Points
The -m flag tells mag which method to call after loading all
source files. The argument is a class name and method selector
separated by a dot.
mag -m Main.start # Call the 'start' class method on Main
mag -m App.run # Call the 'run' class method on App
mag -m Tests.runAll # Call 'runAll' on Tests
The class name is resolved relative to the project namespace. If
your project has namespace MyApp and you pass -m Main.start,
it calls MyApp::Main.start. If no project manifest exists, the
bare class name is used.
The entry point method must be a class-side method (not an instance method). A typical entry point looks like this:
# src/Main.mag
Main subclass: Object
classMethod: start [
'Application starting...' printString.
app := self new.
app run.
]
method: run [
"Main application loop"
]
Without -m, mag loads all source files but does not call any
entry point. This is useful with -i for interactive exploration:
# Load project and drop into REPL
mag -i
# Load project, run entry point, then drop into REPL
mag -m Main.start -i
A Complete Example
Here is a complete maggie.toml showing all sections together:
[project]
name = "todo-app"
namespace = "Todo"
version = "0.1.0"
description = "A todo list application"
license = "MIT"
maggie = ">=0.10.0"
[source]
dirs = ["src"]
entry = "Main.start"
exclude = ["*_scratch.mag"]
[dependencies]
yutani = { git = "https://github.com/chazu/yutani-mag", tag = "v0.5.0" }
shared-models = { path = "../shared-models" }
http-client = { git = "https://github.com/example/http-client-mag", tag = "v1.0.0", namespace = "Http" }
[dev-dependencies]
test-helpers = { path = "../test-helpers" }
[image]
output = "todo-app.image"
include-source = true
[test]
dirs = ["test"]
entry = "TodoTests.run"
timeout = 30000
[scripts]
prebuild = "mag fmt --check"
And the corresponding project layout:
todo-app/
maggie.toml
.maggie/
lock.toml
deps/
yutani/
http-client/
src/
Main.mag # Todo::Main
models/
Task.mag # Todo::Models::Task
TaskList.mag # Todo::Models::TaskList
views/
TaskView.mag # Todo::Views::TaskView
Source files use import: to reference dependency classes:
# src/views/TaskView.mag
namespace: 'Todo::Views'
import: 'Yutani::Widgets'
import: 'Todo::Models'
TaskView subclass: YutaniWidget
instanceVars: task
method: task: aTask [ task := aTask ]
method: render [
self setText: task title.
]
Building and running:
# Fetch dependencies
mag deps
# Run the application
mag -m Main.start
# Save an image for fast startup
mag --save-image todo-app.image
# Run from the saved image
mag --image todo-app.image -m Main.start
# Build a standalone binary (runs entry point only)
mag build -o todo-app
# Build a full Maggie system with your project baked in
mag build --full -o todo-app
# ./todo-app runs Main.start
# ./todo-app -i REPL with all your classes loaded
# ./todo-app help browse your classes interactively
Summary
The project system provides a structured way to organize Maggie code:
- maggie.toml -- declares project metadata, source layout,
dependencies, and image settings in a single file
- mag deps -- resolves and fetches dependencies (git and path),
writes a lock file for reproducibility
- Directory-as-namespace -- directory structure automatically
maps to namespaces, keeping the class table organized
- Namespace mapping -- three-level resolution (consumer override,
producer manifest, PascalCase fallback) with collision detection
and reserved namespace protection
- Images -- snapshot VM state for fast startup and deployment
- Entry points -- the -m flag calls a class method after
loading, connecting the project to its runtime behavior
For namespace and import syntax within source files, see the Modules chapter. For content-addressed code distribution, see the Distribution chapter.