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

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

Package basics

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

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

Package commands

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

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

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

makori fmt rewrites separate pull lines into this grouped form automatically.

Standard library

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.

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