magus v0.3.0 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
6 min read

Getting started

This is the guided, command-line path through magus: install the binary, bootstrap a workspace, write your first magusfile, run a target, bind a spell, and compose a ci pipeline you can run against only the projects a change touched. Follow it top to bottom. Every command here is real; run each one as you read.

If you would rather try magus without installing anything, the interactive playground runs the same engine in your browser.

1. Install magus

magus ships as a single self-contained binary. Follow the Download guide for your platform, PATH setup, and signature verification, then confirm the binary is on your PATH:

magus version

The Download guide also covers magus self update and shell completion. This page assumes magus resolves on your PATH from here on.

2. Bootstrap a workspace with magus init

From the root of your repository:

magus init

magus init bootstraps a magus workspace in the current directory. It:

  • Writes magus.yaml, the workspace config. By default this goes to the global user config location ($XDG_CONFIG_HOME/magus/); pass --local to write it into the repo instead, which is what you want for checked-in, team-shared config.
  • Stubs a starter magusfile.buzz in the repo root: a working file with every canonical stage already declared as a no-op, ready for you to fill in.
  • Wires the VCS merge driver so magus.yaml and the magusfile merge cleanly. The VCS is taken from --vcs (git or hg), or chosen interactively when stdin is a terminal.

For a non-interactive run (for example in CI), pick the VCS explicitly and write the config into the repo:

magus init --local --vcs git

Other flags: --force overwrites an existing config file, and --global writes only the global config and skips the per-repo magusfile stub and merge driver.

3. Read your first magusfile

Open the magusfile.buzz that magus init created. It looks roughly like this: every exported function is a runnable target, and the current directory is already registered as a project on defaults.

import "magus";
import "os";

// Each exported function is a runnable target. Leave a stage as a no-op
// until you wire it.
export fun preflight(ctx: magus\Context, args: [str]) > void {}
export fun generate(ctx: magus\Context, args: [str]) > void { ctx.needs(preflight); }
export fun format(ctx: magus\Context, args: [str]) > void { ctx.needs(generate); }
export fun lint(ctx: magus\Context, args: [str]) > void { ctx.needs(format); }
export fun build(ctx: magus\Context, args: [str]) > void { ctx.needs(format); os\exec("echo", ["Hello from magus"]); }
export fun test(ctx: magus\Context, args: [str]) > void { ctx.needs(format); }

// 'ci' is the conventional anchor that `magus affected ci` keys off.
export fun ci(ctx: magus\Context, args: [str]) > void {
    ctx.needs(lint, build, test);
}

Three ideas carry the whole model:

  • Targets are exported functions. There is no registration call for a target: export a fun, and its name becomes a runnable target. See targets for the full model and the CLI grammar.
  • magus\needs declares prerequisites. ctx.needs(format) says "run format first" - you pass the target function itself, so a typo is an undefined variable caught at load, not a run-time miss. magus builds a DAG from these edges, runs shared prerequisites once, and parallelizes independent branches.
  • ci is the anchor. It is an ordinary target you compose with magus\needs. magus does not hardcode its steps, but it is the target magus affected keys off, and it always runs read-only.

List what magus discovered, then run the starter build:

magus ls            # list every discovered project, its files, outputs, and deps
magus run build     # run the build target for the project under the cwd

magus ls prints every project it found; magus run build runs your build target (the starter one echoes a greeting after format). With no project argument, magus run selects the project containing the current directory.

4. Bind your first spell

The starter build shells out with os\exec. That is fine for a one-off, but real toolchains belong in a spell: a library of tool-native operations plus the cache metadata that toolchain needs. A spell runs nothing on its own; it contributes operations (go-build, go-test, go-vet, ...) that your targets compose, and it tells the cache which files are inputs and outputs. See spells, and Spells vs Targets for where the line falls.

Bind the built-in go spell by importing it and listing it in magus\project, then compose its ops into your targets. Op keys are the CLI command in kebab-case, so they are reached by subscript (go["go-build"]), and every op call takes the target's ctx as its first argument:

import "magus";
import "magus/spell/go";

magus\project({ "spells": [go] });

// Each exported function is a runnable target; its body calls the spell's ops.
export fun build(ctx: magus\Context, args: [str]) > void {
    ctx.needs(format);
    go["go-build"](ctx);
}

export fun format(ctx: magus\Context, args: [str]) > void { go["go-fmt"](ctx); }

// go-vet, golangci-lint, and govulncheck are all static analysis, so they
// compose into the canonical `lint` target, not bespoke targets.
export fun lint(ctx: magus\Context, args: [str]) > void {
    go["go-vet"](ctx);
    go["golangci-lint"](ctx);
    go["govulncheck"](ctx);
}

export fun test(ctx: magus\Context, args: [str]) > void {
    ctx.needs(format);
    go["go-test"](ctx);
}

Now the canonical targets run the real toolchain:

magus run format    # gofmt -l . (check only)
magus run lint      # go vet, golangci-lint, govulncheck
magus run build     # go build
magus run test      # go test ./...

A note on read vs write: targets are read-only by default, so magus run format checks formatting rather than rewriting it. To mutate in place, attach the built-in rw charm:

magus run format:rw   # gofmt -w . (rewrite files)
magus run lint:rw     # golangci-lint --fix (apply autofixes)

Charms are shared, composable execution modifiers attached after :; see charms. If you want autofix as the local default so you do not type :rw each time, set default_charms: [rw] in magus.yaml (charms.md covers the safeguards that keep CI read-only regardless).

5. Compose a ci target and run only what changed

ci is where the pieces come together. Compose it from your other targets with magus\needs; magus fans them out in parallel where the DAG allows and runs shared prerequisites once:

export fun ci(ctx: magus\Context, args: [str]) > void {
    ctx.needs(lint, build, test);
}

Run the whole pipeline locally:

magus run ci        # lint, build, test (read-only; rw is stripped from ci)

The payoff arrives with magus affected. Instead of running ci for every project, it runs ci only for the projects a version-control change touched, plus everything transitively downstream of them in the dependency graph:

magus affected ci             # run ci only for projects your changes touched
magus affected ci --base main # compare against a specific base ref

If you ever wonder why a project is in the affected set, ask:

magus affected ci --explain ./path/to/project

magus affected ci is the command your CI runs on every pull request. It keys off the ci anchor, computes the affected set from the VCS diff, and does the minimum work. For CI fan-out across runners, magus affected --plan emits a provider-neutral shard plan; see magus affected for --plan, --stdin, and bisect.

Recap

You now have the full loop:

  1. magus init bootstrapped magus.yaml, a starter magusfile, and the merge driver.
  2. Exported functions in magusfile.buzz became your targets.
  3. magus ls and magus run build listed and ran them.
  4. Binding the go spell let your targets compose real ops (go-build, go-test, go-fmt, ...).
  5. A ci target composed with magus\needs runs the pipeline, and magus affected ci runs it only for what changed.

Next steps

The documentation index is the map. From here, the core concepts:

  • targets - targets, the CLI grammar, and name resolution.
  • spells - spells, their ops, and the built-in spell catalog.
  • charms - rw and other execution modifiers attached with :.
  • workspace - how magus discovers projects, and multi-project (monorepo) layout.
  • cache - the content-addressed cache that makes re-runs fast.
  • config - every magus.yaml key, its MAGUS_* env var, and CLI flag.
  • sandbox - how spell subprocesses are confined to the workspace.

And the reference:

getting-startedinstallinitmagusfiletutorialcli
Last updated (15118091)
Earlier changes on this page (2)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Operation

A single tool-native command a target composes; the middle of the work hierarchy (Spell to Operation to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Engine

The interpreter a magusfile runs on; magus embeds the Buzz engine. See engines.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

Sandbox

The restricted filesystem and environment a target runs in, so builds stay reproducible and side-effect-free. See sandbox.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

One-off

A single magus invocation that runs a target and exits, using a per-process pool; the opposite of the long-lived daemon or a service. See daemon.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.