This guide covers creating reusable packages, declaring dependencies, and organizing larger projects into workspaces.
Every Makori 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 lockfile version 2 with deterministic SHA-256 hashes of the root
manifest and recursive .mko sources. Commit it to version control. The
makori pkg install, makori build, makori run, and makori check commands rehash
locked dependencies and fail if their content changed, including nested source
files. They also fail closed when a transitive manifest cannot be read or the
lockfile has malformed or contradictory fields.
Use makori pkg update only after inspecting an intentional dependency change.
The same command migrates legacy version 1 lockfiles; install does not silently
trust their older non-cryptographic hashes.
| Command | Purpose |
|---|---|
makori pkg init mylib |
Create a new package |
makori pkg add name path=../name |
Add or update a path dependency |
makori pkg add name ../name |
Same (positional) |
makori pkg remove name |
Remove a dependency |
makori pkg list |
Show packages and their status |
makori pkg fetch |
Clone git dependencies |
makori pkg lock |
Write/update mako.lock |
makori 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 |
|---|---|
makori check . |
Typecheck all members |
makori build . |
Build members with main.mko |
makori test . |
Run tests in all members |
makori fmt . |
Format all members |
makori run -p app |
Run a specific member |
makori check -p lib |
Check a single member |
If only one member has main.mko, makori 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. makori 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
)
makori fmt rewrites separate pull lines into this grouped form automatically.
Pull standard library units by name (no ./ prefix). Always qualify:
pull "strings"
pull "os/env"
fn main() {
print(strings.trim(" hello "))
print(strings.concat(strings.split("a,b", ","), "-"))
print(env.expand("home=$HOME"))
}
Keyword-safe names: concat (not join), matches (not match).
Index: STDLIB.md.
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().