Getting Started
OverviewLanguage GuideFull Reference
Book
Table of ContentsIntroductionPrefaceGetting StartedLanguage TourOwnershipErrorsConcurrencyStdlibNetworkingDataPackagesSpeed & SafetyCross-PlatformToolingCookbookAppendix
Reference
Standard LibraryKeywordsPerformanceSecurityBuilt-in FunctionsStatusDebuggingABI
How-To
Getting StartedHTTP APIsErrorsPackagesConcurrencyMemoryWASITestingRelease Builds
Project
RoadmapVisionChangelogContributing

Packages and Dependencies

Packages and Dependencies

This guide covers creating reusable packages, declaring dependencies, and organizing larger projects into workspaces.

Package basics

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

Project layout

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)

Adding a local dependency

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).

Transitive dependencies

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)

Git dependencies

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.

Lockfile

Pin exact versions for reproducible builds:

mako pkg lock

This writes mako.lock with content hashes. Commit it to version control.

Package commands

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

Workspaces

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"]

Workspace commands

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.

Security audits

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.

Pulls (multi-file)

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.

Basic file pull

// 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).

Pack-qualified types

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) => … }.

Maps of pack structs

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).

Pack-qualified enums

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))
}

Explicit aliases

pull "./db.mko" as db
pull "./routes.mko" as routes
// dual: import db "./db.mko"

fn main() {
    db.connect()
    routes.serve(8080)
}

Blank and dot

pull _ "fmt"           // load/typecheck only — no names
pull . "./helpers.mko" // merge without prefix (use sparingly)

Grouped pulls

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.

Standard library

Pull standard library units by name (no ./ prefix). Always qualify:

pull "strings"

fn main() {
    print(strings.trim("  hello  "))
}

Walkthrough: building a multi-file project

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

Extracting a shared package

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().

Next steps

Edit this page on GitHub Report an issue