Projects and packagesalpha
How do I lay out a multi-file project and pull in mops packages?
A moxzi project is an ordinary mops project: a mops.toml at the top, sources underneath, and one entry .mo per canister. The constraint that governs the whole layout is that the compiler records source paths in what it emits — so where a file sits relative to the project root changes the bytes, and moxzi maps every file into a machine-independent virtual path before compiling it.
A minimal project#
mkdir -p demo/src/lib && cd demo
cat > mops.toml <<'EOF'
[package]
name = "demo"
version = "0.1.0"
[dependencies]
base = "0.16.0"
EOF
cat > src/lib/Greeting.mo <<'EOF'
import Text "mo:base/Text";
module {
public func format(name : Text) : Text { "Hello, " # Text.toUppercase(name) # "!" };
};
EOF
cat > src/Main.mo <<'EOF'
import Greeting "lib/Greeting";
persistent actor {
public query func greet(name : Text) : async Text { Greeting.format(name) };
};
EOF
mops install
moxzi build src/Main.mo -o main.wasm
src/Main.mo -> main.wasm + main.did (220374 bytes, 16 file(s), 90 steps, 1.74s)
Sixteen files: your two, plus everything mo:base/Text transitively imports. Nothing was passed on the command line — the mops.toml was enough.
The virtual filesystem#
Every source file is compiled under a VFS path, not its path on your disk:
| Kind | Mapping | Example |
|---|---|---|
| project source | /src/<path relative to root> | /src/src/Main.mo |
| package source | /pkg/<name>/<path relative to the package's src> | /pkg/base/Text.mo |
| outside the root | /ext/<bare filename> | /ext/scratch.mo |
Note /src/src/Main.mo: the root is the project directory, and src/ is a directory inside it, so it appears twice. That is correct and deliberate — the mapping is purely mechanical.
The root is the nearest ancestor of the entry file holding a mops.toml or a dfx.json; failing that, the entry file's own directory. --root <DIR> overrides it. Because the mapping never contains an absolute host path, two developers on two machines produce identical bytes, and a local build agrees with an on-chain one.
/ext/… is a fallback, not a design. A file outside the root loses its directory structure, and two such files with the same basename collide. Keep sources under the root.
Inspecting the closure#
moxzi deps answers "what would actually be compiled, and under what name":
moxzi deps src/Main.mo
root : /…/demo
pkgs : mops sources
entry : /src/src/Main.mo
files : 16 (176579 bytes)
24989 /pkg/base/Array.mo
<- /…/demo/.mops/base@0.16.0/src/Array.mo
…
--paths-only prints just the VFS names, one per line, which diffs cleanly between two checkouts or between a local and an on-chain build. The pkgs : line tells you how the package map was obtained, so a silent fall back to the weaker resolver is visible rather than something you infer from downstream type errors.
How packages resolve#
pkgs : value | What happened |
|---|---|
mops sources | the mops CLI resolved the transitive graph — the complete answer, and the preferred one |
mops.toml + .mops (mops not found) | mops is not installed; moxzi read the declared dependencies against the installed .mops/ tree |
none (no mops.toml) | not a mops project; only --package pairs and relative imports resolve |
moxzi looks for mops on PATH and then in the usual npm-ish locations (/opt/homebrew/bin, /usr/local/bin, ~/.local/bin, ~/.npm-global/bin); MOXZI_MOPS names it outright. If mops is found but cannot run, moxzi refuses loudly rather than falling back:
error: `/opt/homebrew/bin/mops sources` failed: env: node: No such file or directory
mops runs on node. Put it on PATH -- a GUI-launched editor or a non-login shell often has neither.
Dependencies cannot be resolved, so this build would fail with type errors inside
packages rather than with this message. Refusing to guess.
That is the right failure: the fallback resolver cannot see aliases declared inside a dependency's own mops.toml, so guessing would have produced a confusing type error deep inside a package instead.
Without mops#
--package NAME PATH works exactly as moc --package does, and repeats:
moxzi build main.mo -o main.wasm --package base /path/to/base/src
main.mo -> main.wasm + main.did (220230 bytes, 15 file(s), 89 steps, 1.11s)
Explicit pairs are additive to whatever mops supplies, so you can override a single package inside an otherwise normal project. The same flag exists on moxzi deps.
Import spellings#
An import string resolves in a fixed order: X → X.mo, then X/lib.mo, then X verbatim. So import Greeting "lib/Greeting" finds lib/Greeting.mo, and import P "pkgdir" finds pkgdir/lib.mo. An import moxzi cannot find on disk is a warning, not an error — the compiler may still resolve it — but it is the usual cause of a later, far more confusing type error:
warning: could not resolve import "mo:nonexistent/Lib"; not uploading it
Error: compilation failed:
/src/e.mo:1.9-1.29: type error [M0020], unresolved import mo:nonexistent/Lib
Importing a Candid interface (idl:)#
A program can talk to a canister whose Motoko source it does not have by importing that canister's .did file:
import Counter "idl:interfaces/counter.did";
persistent actor {
transient let remote : Counter.Self = actor ("aaaaa-aa");
public func bump() : async Nat { await remote.bump(1) };
};
The path is an ordinary relative path — the idl: scheme says how the file is read, not where it lives. The import binds a module, not the service itself: every type the file declares comes across under its Motoko name (type get_thing = … becomes GetThing, unless PascalCasing it would collide), plus Self — the service's own actor type, which is what you annotate an actor ("…") reference with.
Two shapes of file are refused, with the same M0004 verdicts moc gives: a file with no service (there is no interface to import), and a file that declares a type named Self (that name is reserved for the imported service). A method whose Candid name is not a valid Motoko identifier is M0160. Types round-trip exactly — a program built against an idl: import re-emits the same Candid moc would, byte for byte (scripts/idl_import_gate.sh).
Actor class libraries#
If a file in the closure other than the entry declares an actor class, moxzi compiles and links it as its own canister module before the entry starts, in dependency order, and hands the linked result to the entry's compile. This happens whether or not you passed --no-link: that flag governs the final artifact, not the libraries. A build containing an actor class library therefore needs linker.wasm present even with --no-link.
Things you cannot do#
| Why | |
|---|---|
| Build several canisters in one command | one entry file per invocation; drive multiple canisters from a script or dfx.json |
Have moxzi run mops install for you | dependency installation stays mops' job; moxzi only reads the result |
Point /src at two roots | one --root; anything outside it degrades to /ext/ |
| Use git dependencies through the fallback resolver | they live under .mops/_github with a mangled name and are skipped; install mops so mops sources handles them |
| Rely on absolute host paths in imports | they would leak into the artifact and break byte parity, which is why the VFS exists |
Next#
- Compiling and linking — what happens to that closure once it is resolved.
- Reading diagnostics — decoding
M0020and its neighbours. - CLI quickstart — the single-file path, if this is more structure than you need.