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

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

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

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.

Imports (multi-file)

Most real projects need more than one file. Mako's import statement handles this -- and mako run automatically compiles everything that's imported.

Basic file import

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

Aliased imports

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

Grouped imports

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.

Standard library imports

Import standard library modules by name (no ./ prefix). They're accessed through their module name:

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

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