MetaScript Build
Configure your MetaScript project using build.ms - a fully typed MetaScript file.
Philosophy
"You already have a perfectly good programming language. Why invent another one for configuration?"
MetaScript follows Zig's approach: no separate config language. Your build.ms IS MetaScript code with full IDE support and compile-time validation.
File Structure
Minimal Project
my-project/
├── build.ms # Package config & deps
├── ms.lock # Lockfile (generated)
└── src/
└── main.msWith Complex Build Logic
my-project/
├── build.ms # Simple, flat, CLI-editable
├── build/
│ ├── utils.ms # Complex logic
│ ├── tasks.ms # Task definitions
│ └── targets.ms # Build targets
├── ms.lock
└── src/
└── main.msConvention: Keep build.ms flat. Offload complexity to build/*.ms. This keeps msc add/remove working.
Minimal Example
// build.ms - Simplest possible package
export const package = {
name: "hello",
version: "1.0.0",
};
export const deps = {};No imports needed. Just object literals. Fully typed.
Standard Example
// build.ms - Typical project
export const package = {
name: "my-app",
version: "1.0.0",
edition: "2025",
license: "MIT",
backends: ["c", "js"],
};
// Cross-platform dependencies (minimum versions, MVS)
export const deps = {
json: "1.5.0",
http: "2.0.0",
};
// C-backend specific
export const deps_c = {
libuv: { version: "1.48.0", native: true },
};
// JS-backend specific (npm packages)
export const deps_js = {
"npm:express": "4.18.0",
"npm:lodash": "4.17.0",
};
// Erlang-backend specific (hex packages)
export const deps_erlang = {
"hex:cowboy": "2.10.0",
};
// Development dependencies
export const deps_dev = {
"test-runner": "1.0.0",
};
// Compile-time macros
export const deps_macro = {
derive: "1.0.0",
};Package Metadata
export const package = {
// Required
name: "my-app",
version: "1.0.0",
// Recommended
edition: "2025",
license: "MIT",
backends: ["c", "js", "erlang"],
// Optional
description: "What the package does",
author: "Name <email@example.com>",
repository: "https://github.com/user/repo",
homepage: "https://docs.example.com",
keywords: ["utility", "helper"],
};Supported Backends
| Backend | Description |
|---|---|
"c" | Native binary via C |
"js" | JavaScript (ES2020+) |
"erlang" | BEAM VM |
"raiser" | Raiser VM (scripting, hot-reload) |
Dependency Variables
| Variable | Purpose | CLI Managed |
|---|---|---|
deps | Cross-platform deps | Yes |
deps_c | C-backend deps | Yes |
deps_js | JS-backend deps | Yes |
deps_erlang | Erlang-backend deps | Yes |
deps_dev | Dev dependencies | Yes |
deps_macro | Macro dependencies | Yes |
Rule: deps* = simple literals (CLI-editable). Everything else = user-controlled.
Version Selection (MVS)
MetaScript uses Minimal Version Selection from Go. No semver ranges (^, ~).
export const deps = {
json: "1.5.0", // means "I need at least 1.5.0"
};How MVS Works
Your build.ms: json: "1.5.0" (at least 1.5.0)
Dependency A: json: "1.3.0" (at least 1.3.0)
Dependency B: json: "1.6.0" (at least 1.6.0)
MVS selects: json@1.6.0 (minimum satisfying ALL)
npm would select: json@1.9.0 (latest in range)Why MVS:
- Simple: No range syntax to learn
- Fast: O(n) graph traversal
- Reproducible: Same inputs = same outputs
- Predictable: You know what "1.5.0" means
Dependency Sources
export const deps = {
// Registry - minimum version
json: "1.5.0",
http: "2.0.0",
// Git - pinned by content hash
router: {
url: "git+https://github.com/user/router#v1.0.0",
hash: "sha256:abc123...",
},
// Git branch (resolved to commit at fetch time)
experimental: {
url: "git+https://github.com/user/lib#main",
hash: "sha256:def456...",
},
// Tarball - pinned by hash
legacy: {
url: "https://example.com/legacy-1.0.tar.gz",
hash: "sha256:ghi789...",
},
// Local path (monorepo/development)
shared: {
path: "../libs/shared",
},
};Source Prefixes
| Prefix | Source | Example |
|---|---|---|
| (none) | MetaScript registry | json: "1.5.0" |
npm: | npmjs.org | "npm:lodash": "4.17.0" |
hex: | hex.pm | "hex:cowboy": "2.10.0" |
git+ | Git repository | { url: "git+https://...", hash: "..." } |
https:// | Tarball URL | { url: "https://...", hash: "..." } |
path: | Local path | { path: "../libs/shared" } |
CLI Operations
Add Dependencies
msc add json # Add to deps (latest version)
msc add json@2.0.0 # Specific minimum version
msc add --c libuv # Add to deps_c
msc add --js npm:express # Add to deps_js
msc add --erlang hex:cowboy # Add to deps_erlang
msc add -D test-runner # Add to deps_dev
msc add --macro derive # Add to deps_macroAdd from Git/URL
msc fetch --save git+https://github.com/user/router#v1.0.0
msc fetch --save git+https://github.com/user/lib#main
msc fetch --save https://example.com/pkg.tar.gzRemove Dependencies
msc remove json # Remove from deps
msc remove --c libuv # Remove from deps_cUpdate Dependencies
msc update # Update all deps
msc update json # Update specific depComplex Build Logic
Keep build.ms flat, import complex logic:
// build.ms
import { createTasks } from "./build/tasks.ms";
import { computePlatformDeps } from "./build/utils.ms";
export const package = {
name: "my-app",
version: "1.0.0",
backends: ["c", "js", "erlang"],
};
// CLI manages these (simple literals)
export const deps = {
json: "1.5.0",
http: "2.0.0",
};
// Complex logic imported, not inline
export const dependencies = computePlatformDeps(deps);
export const tasks = createTasks();// build/utils.ms
import { env, target } from "@ms/build";
export function computePlatformDeps(baseDeps: Record<string, string>) {
return {
...baseDeps,
...(target.os === "linux" && { epoll: "1.0.0" }),
...(target.os === "macos" && { kqueue: "1.0.0" }),
...(env("CI") && { "ci-reporter": "1.0.0" }),
};
}Lockfile (ms.lock)
Generated file that pins exact versions:
{
"version": 1,
"generated": "2025-12-10T10:00:00Z",
"metascript": "0.1.0",
"packages": {
"json@1.5.0": {
"version": "1.5.0",
"source": "registry+https://pkg.metascriptlang.org",
"integrity": "sha256:abc123...",
"backends": ["c", "js", "erlang"]
}
}
}Properties:
- Content-addressed integrity hashes
- Source provenance per package
- Backend tags for each dependency
- Generated, not hand-edited
Example Configurations
Library Package
export const package = {
name: "@yourname/utils",
version: "1.0.0",
license: "MIT",
backends: ["c", "js"],
};
export const deps = {
json: "1.5.0",
};
export const deps_dev = {
"test-runner": "1.0.0",
};CLI Application
export const package = {
name: "my-cli",
version: "1.0.0",
backends: ["c"],
};
export const deps = {
args: "1.0.0",
fs: "1.0.0",
};
export const deps_c = {
libuv: { version: "1.48.0", native: true },
};Web Application
export const package = {
name: "my-web-app",
version: "1.0.0",
backends: ["js"],
};
export const deps = {
http: "2.0.0",
json: "1.5.0",
};
export const deps_js = {
"npm:express": "4.18.0",
"npm:cors": "2.8.0",
};Erlang/OTP Application
export const package = {
name: "my-otp-app",
version: "1.0.0",
backends: ["erlang"],
};
export const deps = {
json: "1.5.0",
};
export const deps_erlang = {
"hex:cowboy": "2.10.0",
"hex:jason": "1.4.0",
};Game Scripts (Raiser)
export const package = {
name: "my-game-scripts",
version: "1.0.0",
backends: ["raiser"],
};
export const deps = {
math: "1.0.0",
};
export const raiser = {
hotReload: true,
debug: true,
};Build Commands
msc build # Build all backends
msc build -t c # Build C backend
msc build -t js # Build JS backend
msc build -t erlang # Build Erlang backend
msc build -t raiser # Build Raiser bytecodeRunning with Raiser
msc run --target=raiser src/main.ms # Run directly
msc run --watch --target=raiser src/ # Watch mode with hot-reloadBest Practices
- Keep
build.msflat - Simple object literals for deps - Use
build/*.msfor complex logic - Keeps CLI editing working - Pin Git deps by hash - Reproducible builds
- Use MVS mindset - Specify minimum versions you need
- Commit
ms.lock- Reproducible builds for your team
Next Steps
- CLI Commands - Full command reference
- Language Reference - Syntax guide
- Package Registry - Publishing packages