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
A package with both library and application code:
myapp/
mako.toml
main.mko # application entry point (fn main)
lib.mko # library code (preferred for deps)
When another package depends on yours, Mako includes lib.mko if it exists,
otherwise all top-level .mko files (excluding tests and main).
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 remote packages (requires git on PATH):
[dependencies]
"tool" = { git = "https://example.com/tool.git", tag = "v0.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's import statement handles
this -- and mako run automatically compiles everything that's imported.
// utils.mko
fn format_name(first: string, last: string) -> string {
return first + " " + last
}
// main.mko
import "./utils.mko"
fn main() {
print(format_name("Grace", "Hopper"))
}
mako run main.mko
# Grace Hopper
Use as to give an import a namespace. This avoids naming conflicts and makes
it clear where each function comes from:
import "./db.mko" as db
import "./routes.mko" as routes
fn main() {
db.connect()
routes.serve(8080)
}
When you have several imports, group them into one block:
import (
"./routes.mko"
"./db.mko"
"strings"
"net/http"
)
mako fmt will rewrite separate import lines into this grouped form
automatically.
Import standard library modules by name (no ./ prefix). They're accessed
through their module name:
import "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
fn db_init() {
let _ = sqlite_query_int("/tmp/tasks.db",
"CREATE TABLE IF NOT EXISTS tasks(id INTEGER PRIMARY KEY, title TEXT, done INT)")
}
fn db_add_task(title: string) -> int {
return sqlite_query_int("/tmp/tasks.db",
"INSERT INTO tasks(title, done) VALUES ('" + title + "', 0)")
}
fn db_task_count() -> int {
return sqlite_query_int("/tmp/tasks.db", "SELECT COUNT(*) FROM tasks")
}
Step 3: Add route handlers
Create routes.mko:
// routes.mko
fn route_health(c: int) {
let _ = http_respond_json(c, 200, "{\"ok\":true}")
}
fn route_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 route_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
import "./db.mko"
import "./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") {
route_health(c)
} else {
if str_eq(path, "/tasks") {
route_add_task(c)
} else {
if str_eq(path, "/stats") {
route_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().