The Project File

fantasm.toml holds the settings a build needs, so they live with the source instead of in whatever shell script or just recipe happens to invoke the assembler. A project with one is built by typing fantasm and nothing else.

The file is optional. Everything in it has a command-line equivalent, and a source file assembles perfectly well without one.

init

fantasm init asks four questions — project name, target machine, output format, entry-point filename — and writes both fantasm.toml and a stub main.asm. It refuses to overwrite either unless you say so, or pass --force.

Answer the questions on the command line to skip the prompts:

fantasm init --name starfield --device zxnext --format nex --main src/main.asm

Setting FANTASM_INTERACTIVE=false suppresses the prompts entirely and takes the defaults for anything not given, which is what you want in a script.

Discovery

fantasm build in a directory containing fantasm.toml uses it. That is the ordinary way to build.

The bare word fantasm prints a usage summary and builds nothing, project file or not. Any argument at all is enough — fantasm build, fantasm -n, fantasm --all — but no arguments means no build.

Point at one elsewhere with -p/--project:

fantasm build -p ../shared/fantasm.toml

Auto-detection only happens when you name no source file. fantasm build one.asm in a directory with a fantasm.toml ignores the file — pass -p if you meant to use both.

A Complete Example

[project]
name = "starfield"
version = "0.3.1"
main = "src/main.asm"
output = "dist/starfield.nex"

[assembler]
device = "zxnext"
format = "nex"
include_dirs = ["src", "lib"]
find_unused = true
gc_modules = true

[defines]
DEBUG = 1
MAX_STARS = 64
BUILD_ID = "nightly"
CHEATS = false

[lint]
w1049 = "deny"

[artifacts]
labels = "dist/starfield.labels"
sld = "dist/starfield.sld"

[tasks]
pre_build = ["mkdir -p dist"]
post_build = ["echo built"]

Every table is optional, and so is every key in it.

[project]

KeyMeaning
nameThe project’s name. Also defined as __NAME__ in your source.
versionThe version. Also defined as __VERSION__.
mainThe entry-point source file.
outputWhere the assembled image is written.

__NAME__ and __VERSION__ are text your program can assemble, not just labels for the build:

banner:
    dz  __NAME__, " v", __VERSION__

SIZEOF answers their length in bytes, and EQU can join them with +:

BANNER  equ __NAME__ + " v" + __VERSION__
        dz  BANNER

Using text where a number is required is an error (E1075) rather than a silent zero, so ld a, __NAME__ is refused.

[assembler]

Everything here maps to a command-line flag. Anything omitted takes its default.

KeyValuesMeaning
devicezx16, zx48, zx128, zxnextThe machine. zxnext enables Z80N automatically.
ram2mb, 1mb, 512k, or a bare number in KiBRAM variant. Defaults to the machine’s maximum.
formatbin, hex, sna, nexOutput format.
originaddressWhere assembly starts, if the source has no ORG.
max_code_sizebytesRefuse a build emitting more code than this.
include_dirslist of pathsWhere INCLUDE and INCBIN look.
z80nboolZ80N extended instructions. Implied by device = "zxnext".
cspectboolCSpect’s exit and break pseudo-ops.
gc_modulesboolDiscard MODULE code the program never reaches.
gc_librariesboolDiscard !library code the program never reaches, leaving a placement MODULE alone.
case_insensitiveboolTreat Loop and loop as one label.
find_unusedboolRun the unused-label sweep. Every other warning is shown regardless.
verboseboolPer-file progress and timings.

ram narrows the paging range rather than describing the hardware. Setting ram = "512k" on a Next makes a bank beyond that range an assembly error — 512K is 32 banks, so bank 32 is refused — and a program that must run on a 512K machine fails at build time instead of on the desk of whoever has one.

max_code_size limits how much code there is, not the address it reaches. A program that ORGs at $8000 and emits 2 KB uses 2 KB of the budget, not 34 KB of it. The machine’s own ceiling still applies on top — zx16 refuses anything above $8000.

An unknown device is a warning, not an error. device = "zx81" builds, having told you once. Check the spelling if a build behaves as though the setting never took.

warnings was the spelling for find_unused until 2.0, and it named a switch that never turned any other warning off. It still resolves and says so once (W1103).

target was the spelling for device until 2.0. It still resolves and says what replaced it once (W1103), and is withdrawn in 2.1. The [[target]] table is unchanged — a target is an image the project builds, which is what it has always meant everywhere else.

[defines]

Constants available to every file in the build, exactly as -D would define them.

[defines]
DEBUG = 1
MAX_STARS = 64
BUILD_ID = "nightly"
CHEATS = false
  • Numbers are defined as written.
  • Strings become text constants, assemblable with db and dz.
  • Booleans become 1 and 0, so CHEATS = false gives you 0 — usable with IF and with ld c, CHEATS.

Names are case-sensitive unless case_insensitive is on.

[artifacts]

KeyMeaning
labelsWrite a symbol list here.
sldWrite Source Level Debugging data here, for a debugger to consume.

labels only lists symbols marked GLOBAL. The file is empty until something is exported, which is intended rather than broken — the .sld file is what a debugger reads. A GLOBAL constant holding text is written as NAME = "text" rather than as an address.

[tasks]

Shell commands run around the build.

[tasks]
pre_build = ["mkdir -p dist"]
post_build = ["nexdeploy dist/starfield.nex"]

Each string is handed to sh -c (or cmd /C on Windows). A command exiting non-zero stops the build and reports its exit code, so a failed generator does not quietly produce an image built from stale inputs.

-v prints each command before it runs.

Do not drive a second fantasm run from pre_build. It restates flags this file already holds, and the two drift apart silently. Use Building Several Images instead.

Paths

Every path in the file resolves against the directory holding it, not against wherever you happened to be standing. fantasm -p ~/projects/game/fantasm.toml from anywhere builds the same thing.

Paths given on the command line resolve against the current directory, as you would expect.

Precedence

  1. The command line beats the file, and a directive in the source beats both. Any flag you pass overrides the same setting in fantasm.toml; a !device or format written in a source is read after either and wins — see Output Formats.
  2. Every valueless flag has a --no- form--no-z80n, --no-cspect, --no-case-insensitive, --no-find-unused, --no-verbose, --no-gc-modules — for turning off something the file turned on. Without it, gc_modules = true could only be undone by editing the file. The last flag given wins, so --gc-modules --no-gc-modules is off.
  3. Saying nothing is not saying false. A key left out takes its default; one written false is a choice you made. The same holds on the command line: -O 0 is the origin zero and beats an origin in the file, rather than reading as “unset”.
  4. include_dirs accumulates. -I adds to the file’s list rather than replacing it, with command-line directories searched first. Everything else replaces.
  5. format beats the output extension, and -f beats format. An output named .bin with format = "nex" is written as a NEX — and renamed to .nex, since the extension it carries names a format that is not the one chosen.

[lint]

What each warning is filed as, named by its diagnostic code.

[lint]
default = "warn"
w1049 = "deny"     # an unused label fails the build
w1025 = "allow"    # discarded characters at end of line, unreported
allowFiled nowhere. It reaches neither the terminal nor the editor.
warnReported, and the build carries on.
denyFiled as an error, which fails the build.

default sets what an unnamed warning is, so default = "allow" with a handful of deny entries is a project that has chosen its own short list. Left out, it is warn.

Codes, not names. w1049, not unused_labelDiagnostics carries a heading per code and not one short name, and a name in a project file is an interface nobody could rename afterwards. 1049, w1049 and W1049 are the same entry.

Only a warning may be named. An entry naming an error is W1132 and one naming no code at all is W1131; demoting E1003 would let a build succeed with an unresolved name, which is not a style opinion. A refused entry is dropped and the rest of the table still applies.

The table is project-wide. A rule says what this codebase considers a fault, which does not change between two images of it, so a [[target]] does not restate it.

Your Own Tables

A project can keep its deployment settings in the same file:

[assembler]
device = "zxnext"

[lint]
w1124 = "allow"

[spectnext]
deploy = "dist/starfield.nex"

An unrecognised key is a warning (W1124), and a table FantASM does not know is one too. That is what catches gc_modues = true, which used to be accepted in silence and do nothing — the warning names the nearest real key where there is one.

Silence a table of your own with [lint] w1124 = "allow", as above. It is the whole of W1124 rather than that one name, so a misspelt [assembler] key stops being reported as well; a project doing this is choosing to check its own spelling.

Multiple Targets

A project that builds more than one file — a payload and a loader that incbins it, a release and a debug variant — declares each as a [[target]]. See Building Several Images.

Documentation