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.asmSetting 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.tomlAuto-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]
| Key | Meaning |
|---|---|
name | The project’s name. Also defined as __NAME__ in your source. |
version | The version. Also defined as __VERSION__. |
main | The entry-point source file. |
output | Where 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 BANNERUsing 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.
| Key | Values | Meaning |
|---|---|---|
device | zx16, zx48, zx128, zxnext | The machine. zxnext enables Z80N automatically. |
ram | 2mb, 1mb, 512k, or a bare number in KiB | RAM variant. Defaults to the machine’s maximum. |
format | bin, hex, sna, nex | Output format. |
origin | address | Where assembly starts, if the source has no ORG. |
max_code_size | bytes | Refuse a build emitting more code than this. |
include_dirs | list of paths | Where INCLUDE and INCBIN look. |
z80n | bool | Z80N extended instructions. Implied by device = "zxnext". |
cspect | bool | CSpect’s exit and break pseudo-ops. |
gc_modules | bool | Discard MODULE code the program never reaches. |
gc_libraries | bool | Discard !library code the program never reaches, leaving a placement MODULE alone. |
case_insensitive | bool | Treat Loop and loop as one label. |
find_unused | bool | Run the unused-label sweep. Every other warning is shown regardless. |
verbose | bool | Per-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
dbanddz. - Booleans become
1and0, soCHEATS = falsegives you0— usable withIFand withld c, CHEATS.
Names are case-sensitive unless case_insensitive is on.
[artifacts]
| Key | Meaning |
|---|---|
labels | Write a symbol list here. |
sld | Write 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
- 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!deviceorformatwritten in a source is read after either and wins — see Output Formats. - 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 = truecould only be undone by editing the file. The last flag given wins, so--gc-modules --no-gc-modulesis off. - Saying nothing is not saying
false. A key left out takes its default; one writtenfalseis a choice you made. The same holds on the command line:-O 0is the origin zero and beats anoriginin the file, rather than reading as “unset”. include_dirsaccumulates.-Iadds to the file’s list rather than replacing it, with command-line directories searched first. Everything else replaces.formatbeats the output extension, and-fbeatsformat. An output named.binwithformat = "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, unreportedallow | Filed nowhere. It reaches neither the terminal nor the editor. |
warn | Reported, and the build carries on. |
deny | Filed 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_label — Diagnostics 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.