Mako uses a file-based package system with a mako.toml manifest. Dependencies
can be local paths or git repositories. The mako pkg command manages the full
lifecycle: initializing, adding, fetching, locking, and auditing packages.
Every Mako package has a mako.toml at its root:
[package]
name = "myapp"
version = "0.1.0"
[dependencies]
"helper" = { path = "../helper", version = "0.1.0" }
"logger" = { git = "https://github.com/org/mako-logger.git", version = "0.2.0" }
| Field | Required | Description |
|---|---|---|
name |
Yes | Package name (used for import namespacing) |
version |
Yes | SemVer version string (e.g., "0.1.0") |
Each dependency is keyed by its import name and specifies a source:
[dependencies]
"dep_name" = { path = "../relative/path", version = "0.1.0" }
"dep_name" = { git = "https://...", version = "1.0.0" }
"dep_name" = { git = "https://...", branch = "main" }
"dep_name" = { git = "https://...", tag = "v1.2.3" }
| Source | Description |
|---|---|
path |
Local filesystem path (relative to the manifest) |
git |
Git repository URL (cloned to .mako/deps/) |
Additional fields:
- version — SemVer constraint for resolution
- branch — Git branch to track (default: main)
- tag — Specific git tag to pin
Once a dependency is declared in mako.toml, its symbols are available under the
dependency key as a namespace:
# mako.toml
[dependencies]
"helper" = { path = "../helper", version = "0.1.0" }
// main.mko — symbols accessed via helper.fn_name()
fn main() {
print_int(helper.add(20, 22))
print(helper.greet("world"))
}
// helper/lib.mko — exports functions directly
fn add(a: int, b: int) -> int {
return a + b
}
fn greet(name: string) -> string {
return "hi " + name
}
Path dependencies are merged at compile time. The compiler resolves transitive dependencies automatically.
For single-file imports within the same package (no mako.toml needed):
import "./helpers.mko"
fn main() {
print_int(lib_add(2, 3))
print(lib_greet("mako"))
}
The imported file's functions become available in the importing file's scope.
By convention, helper files prefix their functions with lib_ or a meaningful
module prefix to avoid name collisions.
When you run mako run main.mko, the compiler automatically finds and compiles
all imported files -- you don't need to list them on the command line.
Give an import a namespace with as. This is the cleanest way to avoid naming
conflicts when pulling in multiple files:
import "./db.mko" as db
import "./routes.mko" as routes
fn main() {
db.init()
routes.serve(8080)
}
With an alias, you call functions through the alias name (db.init()) instead
of relying on prefixed function names. This works for both local file imports
and standard library imports.
import "strings"
import "path"
import "net/http"
import "sync"
These resolve from the standard library directory (std/, overrideable via the
MAKO_STD environment variable). Standard library modules are accessed through
their module name as a namespace:
import "strings"
fn main() {
print(strings.trim(" hello "))
print(strings.contains("mako lang", "mako"))
}
When you have multiple imports, use the grouped syntax. This works for both standard library and local file imports:
import (
"strings"
"path"
"./db.mko"
"./routes.mko"
)
fn main() {
print(strings.trim(" hi "))
print(path.clean("/a/../b"))
}
The formatter (mako fmt) automatically rewrites two or more single import
statements into a grouped block.
The mako pkg subcommand manages the dependency lifecycle:
Initialize a new package with a mako.toml:
mako pkg init mylib
Creates:
mylib/
mako.toml
main.mko
The generated mako.toml:
[package]
name = "mylib"
version = "0.1.0"
[dependencies]
Add a dependency to the current package:
# Add a local path dependency
mako pkg add helper ../helper
# Add a git dependency
mako pkg add logger https://github.com/org/mako-logger.git
# Add with specific version
mako pkg add utils ../utils --version 0.2.0
This appends to [dependencies] in mako.toml.
Remove a dependency:
mako pkg remove helper
Removes the entry from [dependencies] and cleans cached artifacts.
Download git dependencies to the local cache:
mako pkg fetch
Git dependencies are cloned into .mako/deps/<name>/. This command requires
network access. Path dependencies do not need fetching.
Generate or update the lockfile (mako.lock):
mako pkg lock
The lockfile pins exact versions and git commit hashes for reproducible builds:
# mako.lock (auto-generated, do not edit)
[[package]]
name = "helper"
version = "0.1.0"
source = "path:../helper"
[[package]]
name = "logger"
version = "0.2.0"
source = "git:https://github.com/org/mako-logger.git#abc1234"
Commit mako.lock to version control for reproducible builds across
environments.
Update dependencies to their latest compatible versions:
# Update all
mako pkg update
# Update specific dependency
mako pkg update logger
Re-resolves versions within SemVer constraints and updates mako.lock.
List all dependencies (direct and transitive):
mako pkg list
Output:
helper 0.1.0 (path: ../helper)
logger 0.2.0 (git: https://github.com/org/mako-logger.git @ abc1234)
Run security and license audits on locked dependencies:
mako pkg audit
Auditing checks:
- CVE advisories: matches against mako-cve.toml advisory database
- License policy: checks against mako-license.toml allow/deny rules
- Integrity: verifies git checksums match lockfile
$ mako pkg audit
[ok] helper 0.1.0 — no advisories
[ok] logger 0.2.0 — license: MIT (allowed)
audit: 2 packages checked, 0 issues
A workspace groups multiple related packages under a single root. This is useful for monorepo-style projects with shared libraries.
[workspace]
members = ["core", "helper", "app"]
The workspace root has no [package] section — it only declares members.
my-project/
mako.toml # workspace root
core/
lib.mko
helper/
lib.mko
app/
main.mko
Workspace members can depend on each other. The compiler resolves these transitively:
// core/lib.mko
fn scale(n: int) -> int {
return n * 2
}
// helper/lib.mko — depends on core
fn add(a: int, b: int) -> int {
return core.scale(a) + b
}
fn greet(name: string) -> string {
return "hi " + name
}
// app/main.mko — depends on helper (which depends on core)
fn main() {
print_int(helper.add(20, 22)) // core.scale(20) + 22 = 62
print(helper.greet("path-dep"))
}
# Check all members
mako check .
# Run a specific member
mako run -p app
# Test all members
mako test .
# Format all members
mako fmt -w .
my-service/
mako.toml
main.mko
handlers/
users.mko
health.mko
models/
user.mko
db/
queries.mko
import "./handlers/users.mko"
import "./handlers/health.mko"
import "./db/queries.mko"
fn main() {
let fd = http_bind(18100)
// ...
}
Since file imports bring symbols into the same scope, use prefixes to avoid collisions:
// handlers/users.mko
fn users_list(c: int) {
let _ = http_respond_json(c, 200, "[]")
}
fn users_create(c: int) {
let body = http_body(c)
let _ = http_respond_json(c, 201, body)
}
// handlers/health.mko
fn health_check(c: int) {
let _ = http_respond_json(c, 200, "{\"ok\":true}")
}
The built-in formatter enforces consistent style:
# Print formatted output to stdout
mako fmt path.mko
# Write formatted files in place
mako fmt -w .
# List files that need formatting
mako fmt -l .
# Show diff of what would change
mako fmt -d .
import statements into import ( ... ) blocksBefore formatting:
import "strings"
import "path"
import "sync"
After mako fmt:
import (
"strings"
"path"
"sync"
)
Run tests with the built-in test runner:
# Run all tests in a directory
mako test examples/testing
# Filter by test name
mako test examples/testing -r TestAdd -v
# Run tests multiple times
mako test examples/testing --count 3
Test functions are named with the Test prefix:
fn TestAdd() {
assert_eq(1 + 1, 2)
}
fn TestStringConcat() {
let s = "hello" + " " + "world"
assert_eq(s, "hello world")
}
fn TestMapOperations() {
let mut m = make(map[string]int)
m["x"] = 42
assert_eq(m["x"], 42)
assert_eq(len(m), 1)
}
| Function | Purpose |
|---|---|
assert_eq(a, b) |
Assert equality (integers, strings) |
assert(cond) |
Assert boolean condition is true |
Some tests require external services (databases, TLS). These are gated behind environment variables:
# Enable TLS tests (requires OpenSSL)
MAKO_LIVE_TLS=1 mako test examples/testing
# Enable HTTP/2 tests
MAKO_LIVE_NGHTTP2=1 mako test examples/testing
# Enable QUIC tests
MAKO_LIVE_QUIC=1 mako test examples/testing
The default test suite runs green without any live services.
For published packages, Mako resolves from the local registry cache:
.mako/registry/<name>/<version>/
SemVer resolution follows standard rules:
- "0.1.0" — exact version
- "^0.1" — compatible with 0.1.x
- "~0.1.2" — approximately 0.1.2 (patch-level changes)
The registry format stores package metadata and source tarballs locally after
mako pkg fetch.
# Build to binary
mako build main.mko -o out/myapp
# Run directly (build + execute)
mako run main.mko
# Run with arguments
mako run main.mko -- --port 8080
# Check without building (type checking only)
mako check main.mko
| Variable | Purpose |
|---|---|
MAKO_STD |
Override standard library path |
MAKO_LIVE_TLS |
Enable TLS tests |
MAKO_LIVE_NGHTTP2 |
Enable HTTP/2 tests |
MAKO_LIVE_QUIC |
Enable QUIC tests |
A realistic project structure:
my-api/
mako.toml
main.mko
lib/
router.mko
db.mko
models.mko
mako.toml:
[package]
name = "my-api"
version = "0.1.0"
[dependencies]
main.mko:
import "./lib/router.mko"
import "./lib/db.mko"
fn main() {
db_init()
let fd = http_bind(18100)
if fd < 0 {
print("bind failed")
return
}
print("my-api on :18100")
let mut n = 0
while n < 1000 {
let c = http_accept(fd)
if c >= 0 {
router_dispatch(c)
let _ = http_close(c)
n = n + 1
}
}
let _ = http_close_listener(fd)
}
lib/router.mko:
fn router_dispatch(c: int) {
let method = http_method(c)
let path = http_path(c)
if str_eq(path, "/health") {
let _ = http_respond_json(c, 200, "{\"ok\":true}")
} else {
if str_eq(path, "/api/items") {
if str_eq(method, "GET") {
router_items_list(c)
} else {
if str_eq(method, "POST") {
router_items_create(c)
} else {
let _ = http_respond(c, 405, "method not allowed\n")
}
}
} else {
let _ = http_respond(c, 404, "not found\n")
}
}
}
fn router_items_list(c: int) {
let count = db_item_count()
let resp = json_ss("count", format_int(count))
let _ = http_respond_json(c, 200, resp)
}
fn router_items_create(c: int) {
let body = http_body(c)
let name = json_get_string(body, "name")
if len(name) == 0 {
let _ = http_respond_json(c, 400, "{\"error\":\"name required\"}")
} else {
let _ = db_add_item(name)
let _ = http_respond_json(c, 201, json_ss("name", name, "status", "created"))
}
}
lib/db.mko:
fn db_init() {
let _ = sqlite_query_int("/tmp/myapi.sqlite", "CREATE TABLE IF NOT EXISTS items(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
}
fn db_item_count() -> int {
return sqlite_query_int("/tmp/myapi.sqlite", "SELECT COUNT(*) FROM items")
}
fn db_add_item(name: string) -> int {
return sqlite_query_int("/tmp/myapi.sqlite", "INSERT INTO items(name) VALUES ('" + name + "')")
}
| Command | Purpose |
|---|---|
mako pkg init <name> |
Create new package |
mako pkg add <name> <source> |
Add dependency |
mako pkg remove <name> |
Remove dependency |
mako pkg fetch |
Download git dependencies |
mako pkg lock |
Generate/update lockfile |
mako pkg update |
Update to latest compatible versions |
mako pkg list |
List all dependencies |
mako pkg audit |
Security and license audit |
mako fmt -w . |
Format all files |
mako test <dir> |
Run tests |
mako build <file> -o <out> |
Compile to binary |
mako run <file> |
Build and run |
mako check <file> |
Type-check only |
Next: Speed & memory safety.