This guide covers creating reusable packages, declaring dependencies, and organizing larger projects into workspaces.
Every Mako project has a mako.toml at its root:
name = "myapp"
version = "0.1.0"
Create one with:
mako init myapp --name myapp
# or for a library:
mako pkg init mylib
Package-per-directory: all non-test .mko files in a directory form one
package and must share the same pack / package name (Go model).
myapp/
mako.toml
main.mko # application entry (fn main)
lib.mko # library unit (optional name; merged with siblings)
helpers.mko # same pack — merged automatically
When another package depends on yours, Mako merges all non-test units except
main.mko (binary entry stays out of the library surface). Cross-file calls
inside the package resolve before the import prefix is applied.
util/
lib.mko # pack util · greet
more.mko # pack util · shout (calls greet)
Suppose you have a helper library next to your app:
projects/
helper/
mako.toml # name = "helper"
lib.mko # fn add(a: int, b: int) -> int { return a + b }
app/
mako.toml
main.mko
In app/mako.toml:
name = "app"
version = "0.1.0"
[dependencies]
"helper" = { path = "../helper", version = "0.1.0" }
Or use the CLI:
cd app
mako pkg add helper ../helper
In app/main.mko, call functions through the dependency namespace:
fn main() {
print_int(helper.add(2, 3))
}
The namespace comes from the key in [dependencies] -- rename it with:
"math" = { path = "../helper" }
Then call math.add(2, 3).
If helper depends on core, and app depends on helper, Mako walks each
package's mako.toml transitively. Each package uses its own declared names:
app -> helper -> core
(helper calls core.scale)
(app calls helper.add)
For a concrete local package:
[dependencies]
"tool" = { path = "../tool", version = "0.1.0" }
Then fetch:
mako pkg fetch
This clones into .mako/deps/tool/. Use --offline flags to prevent network
access in CI.
Pin exact versions for reproducible builds:
mako pkg lock
This writes mako.lock with content hashes. Commit it to version control.
| Command | Purpose |
|---|---|
mako pkg init mylib |
Create a new package |
mako pkg add name path=../name |
Add or update a path dependency |
mako pkg add name ../name |
Same (positional) |
mako pkg remove name |
Remove a dependency |
mako pkg list |
Show packages and their status |
mako pkg fetch |
Clone git dependencies |
mako pkg lock |
Write/update mako.lock |
mako pkg audit |
Check advisories and license policy |
For larger projects with multiple packages that build together:
mako init myws --workspace
This creates:
myws/
mako.toml # [workspace] members = ["lib", "app"]
lib/
mako.toml
lib.mko
app/
mako.toml # [dependencies] "lib" = { path = "../lib" }
main.mko
Root mako.toml:
[workspace]
members = ["lib", "app"]
From the workspace root:
| Command | Behavior |
|---|---|
mako check . |
Typecheck all members |
mako build . |
Build members with main.mko |
mako test . |
Run tests in all members |
mako fmt . |
Format all members |
mako run -p app |
Run a specific member |
mako check -p lib |
Check a single member |
If only one member has main.mko, mako run . runs it directly.
Create mako-cve.toml beside your lockfile:
[[advisory]]
id = "CVE-2024-1234"
name = "util"
version = "<=1.2.3"
severity = "high"
And mako-license.toml for license policy:
allow = ["MIT", "Apache-2.0"]
deny = ["GPL-3.0"]
[licenses]
helper = "MIT"
Then run:
mako pkg audit
This checks offline -- no network required.
Most real projects need more than one file. Mako pulls are always
pack-qualified so call sites stay clear. mako run compiles everything
that’s pulled.
// utils.mko
pack utils
fn format_name(first: string, last: string) -> string {
return first + " " + last
}
// main.mko
pull "./utils.mko"
fn main() {
print(utils.format_name("Grace", "Hopper"))
}
mako run main.mko
# Grace Hopper
Default qualifier: the pulled file’s pack name (if not main), else the
path basename (utils from utils.mko).
Exported structs (and other named types) use the same qualifier as functions:
// eng.mko
pack eng
export struct Table {
n: int
}
export fn table_new() -> Table {
return Table { n: 0 }
}
export fn table_grow_pair(t: Table, by: int) -> (Table, int) {
let mut tt = t
tt.n = tt.n + by
return (tt, by)
}
// main.mko
pull "./eng.mko"
fn use(t: eng.Table) -> int {
return t.n
}
fn main() {
let t: eng.Table = eng.Table { n: 0 } // pack-qualified struct lit
let t2, n = eng.table_grow_pair(t, 5)
match t2 {
eng.Table { n: rows } => print_int(rows),
}
print_int(use(t2))
print_int(n)
}
eng.Table in annotations, return types, struct literals, and struct patterns
is the pack form of the rewritten name eng__Table. Multi-return of pack
structs works the same as local structs: let a, b = eng.f() and
match eng.f() { (a, b) => … }.
pull "./eng.mko"
fn main() {
let mut m = make(map[int]eng.Table)
m[1] = eng.Table { n: 10 }
print_int(m[1].n)
}
Also map[string]eng.Table, map[float]eng.Table, and pack types as keys
(map[eng.Table]int, map[eng.Table]Point). Same ops as other maps: len /
has / delete, comma-ok, range, and maps_keys / maps_values /
maps_clone / maps_equal / maps_copy / maps_clear. Full map value grid
(incl. map[K][]T, nested map[K]map[…] depth ≤3, bags, channels, tuples) is in
GUIDE.md §4c and 10-collections.md.
Map monomorphs are demand-driven (only used shapes emit C helpers).
Exported enums use the same qualifier. Variants stay short names after pull; you may qualify them with the pack (and optionally the type):
// eng.mko
pack eng
export enum Color { Red, Green(int) }
pull "./eng.mko"
fn score(c: eng.Color) -> int {
match c {
eng.Red => 1,
eng.Green(n) => n,
// or: eng.Color.Red / eng.Color.Green(n)
}
}
fn main() {
let a = eng.Red
let b = eng.Green(7)
let c = eng.Color.Green(3)
print_int(score(a) + score(b) + score(c))
}
pull "./db.mko" as db
pull "./routes.mko" as routes
// dual: import db "./db.mko"
fn main() {
db.connect()
routes.serve(8080)
}
pull _ "fmt" // load/typecheck only — no names
pull . "./helpers.mko" // merge without prefix (use sparingly)
When you have several pulls, group them into one block:
pull (
"strings"
"net/http"
"./db.mko" as db
"./routes.mko" as routes
)
mako fmt rewrites separate pull lines into this grouped form automatically.
Pull standard library units by name (no ./ prefix). Always qualify:
pull "strings"
fn main() {
print(strings.trim(" hello "))
}
Here's how to go from a single file to a well-organized multi-file project, step by step.
Step 1: Start the project
mako init taskapi --name taskapi
cd taskapi
You get mako.toml and main.mko.
Step 2: Add a data layer
Create db.mko alongside main.mko:
// db.mko
pack db
fn init() {
let _ = sqlite_query_int("/tmp/tasks.db",
"CREATE TABLE IF NOT EXISTS tasks(id INTEGER PRIMARY KEY, title TEXT, done INT)")
}
fn add_task(title: string) -> int {
return sqlite_query_int("/tmp/tasks.db",
"INSERT INTO tasks(title, done) VALUES ('" + title + "', 0)")
}
fn task_count() -> int {
return sqlite_query_int("/tmp/tasks.db", "SELECT COUNT(*) FROM tasks")
}
Step 3: Add route handlers
Create routes.mko:
// routes.mko
pack routes
pull "./db.mko"
fn health(c: int) {
let _ = http_respond_json(c, 200, "{\"ok\":true}")
}
fn add_task(c: int) {
let body = http_body(c)
let title = json_get_string(body, "title")
let _ = db.add_task(title)
let _ = http_respond_json(c, 201, "{\"created\":true}")
}
fn stats(c: int) {
let count = db.task_count()
let _ = http_respond_json(c, 200, json_ss("count", format_int(count)))
}
Step 4: Wire it together in main.mko
// main.mko
pull "./db.mko"
pull "./routes.mko"
fn main() {
db.init()
let fd = http_bind(8080)
print("taskapi on :8080")
while true {
let c = http_accept(fd)
let path = http_path(c)
if str_eq(path, "/health") {
routes.health(c)
} else {
if str_eq(path, "/tasks") {
routes.add_task(c)
} else {
if str_eq(path, "/stats") {
routes.stats(c)
} else {
let _ = http_respond(c, 404, "not found\n")
}
}
}
let _ = http_close(c)
}
}
Step 5: Run it
mako run main.mko
# taskapi on :8080
That's it. The compiler follows the imports and compiles db.mko and
routes.mko automatically.
Your project now looks like this:
taskapi/
mako.toml
main.mko # entry point, imports everything
db.mko # data layer
routes.mko # HTTP handlers
Once code is useful across multiple projects, move it into its own package.
Step 1: Create the shared package
mkdir -p ../shared
cd ../shared
mako pkg init shared
Put reusable code in lib.mko:
// shared/lib.mko
fn add(a: int, b: int) -> int {
return a + b
}
fn greet(name: string) -> string {
return "hi " + name
}
Step 2: Add it as a dependency
Back in your app:
cd ../taskapi
mako pkg add shared ../shared
This adds to your mako.toml:
[dependencies]
shared = { path = "../shared" }
Step 3: Use it
// main.mko
fn main() {
print(shared.greet("world"))
print_int(shared.add(1, 2))
}
Package functions are called through the dependency name as a namespace --
shared.greet(), not greet().