10 KiB
Build Directives
A project can contain multiple files. The build system decides how each file is used based on directives in your source, and each file's extension selects the tool that compiles or assembles it.
Projects with multiple files
All files in a project must be referenced from the main project file with one of these reference types:
- Include file — a source file embedded in another using an
#includeor similar directive. - Linked file — a source file compiled/assembled and linked into the
project alongside the main file, using the
//#linkor;#linkdirective. A binary file can be included in an assembly project with theincbindirective. - Resource file — a file that must be included in the project but is
neither included nor linked from the main file (for example it is
included from a non-main file). Use the
//#resourceor;#resourcedirective.
Here's a C example:
//#link "lzg.c"
#include "lzg.h"
If a file cannot be found, check the syntax. Often double quotes are
required, e.g. #include "file.inc".
Only system files that are part of the compiler toolchain can be found
with the <foo.h> syntax.
Assembler files can include files using the .include or .incbin
directives:
.segment "CHARS"
.incbin "tileset.chr"
With DASM, make sure to quote the filename in incbin.
C preprocessor defines
The preprocessor macro __MAIN__ is defined when compiling the main C
file of a project. You can use this to run tests on your module files:
//#link "file_needed_for_test.c"
#ifdef __MAIN__
#include "file_needed_for_test.h"
void main() {
test_my_module();
}
#endif
The macro __8BITWORKSHOP__ is defined on all C files.
Build directives
Build directives are comment lines that tell the build system how to compile, assemble, or link a project. They can appear in any source file and are never seen by the toolchain:
- C, SDCC, and cmoc sources mark them with
//. - Assembly sources mark them with
;.
The keyword follows the comment marker and a #, and the line must
begin with the comment (leading whitespace is allowed).
//#symbol — define a symbol
Defines a preprocessor macro, an assembler symbol, or a linker symbol:
//#symbol [<phase>] NAME[=VALUE]
The optional phase is one of:
| Phase | Meaning |
|---|---|
c, cc, compiler |
C preprocessor define (default) |
as, asm, assembler |
assembler symbol |
ld, link, linker |
linker symbol |
A compiler define takes a text value (cc65 -D, sdcc -D). Assembler
and linker symbols take an integer expression:
//#symbol VERSION=3
//#symbol as START=$8000
//#symbol ld NES_MAPPER=4
A compiler define set in the main file applies to every C file in the
project, so a library can size its storage from a macro the game sets
(//#symbol MAX_ENTITIES=24, with an #ifndef default in the library
header). A linker symbol is only a link-time value: it can supply a
count read at run time but cannot size a C array, and SDCC needs a
leading underscore (//#symbol ld _LIMIT=24 for extern char LIMIT).
A linker symbol defined by a source file replaces (rather than
duplicates) a symbol of the same name that the platform already defines,
so //#symbol ld NES_MAPPER=... can select a different mapper without
editing the platform defaults. Linker values must be integer
expressions; a text value is reported as a build error rather than
passed to the linker.
//#flag — pass a raw argument
Passes arguments straight to one tool phase, with no interpretation:
//#flag <phase> <arg> [<arg> ...]
//#flag c -Osir
//#flag as --cpu 65c02
//#flag ld -m map.txt
Arguments may be quoted with single or double quotes when they contain spaces. This is an escape hatch that the build system cannot validate.
If a flag names a file (for example urust's custom board), also list that file as a resource so the IDE and CLI copy it into the build filesystem:
//#resource "vicdual.toml"
//#flag c --board vicdual.toml
//#tooldef — set a typed build parameter
Sets a named build parameter that the build system understands:
//#tooldef <phase> NAME=VALUE
The supported parameters are:
| Phase | Name | Meaning |
|---|---|---|
ld |
cfgfile |
linker configuration file to use (e.g. for CC65's ld65) |
ld |
libargs |
comma-separated list of library/symbol arguments |
ld |
code_start, data_start |
where SDCC's linker places the code and the RAM data (e.g. 0x4000) |
c |
sdcc |
SDCC version, 3 or 4 (see SDCC versions below) |
//#tooldef ld cfgfile=apple2-hgr2.cfg
//#tooldef ld libargs=,nes.lib
//#tooldef ld code_start=0x4000
//#tooldef c sdcc=3
Commenting out a directive
To disable a directive, break the marker; for example, use ////# in C
or ;;# in assembly. A directive that is already commented out is
deliberately not recognized.
Legacy #define directives
For backwards compatibility the build system still understands the older
#define-based forms:
| Legacy form | Same as |
|---|---|
#define CFGFILE <file> |
//#tooldef ld cfgfile=<file> |
#define LIBARGS <a,b,...> |
//#tooldef ld libargs=<a,b,...> |
#define NES_MAPPER <n> |
//#symbol ld NES_MAPPER=<n> |
#define CC65_FLAGS <a,b,...> |
//#flag c <a> <b> ... |
These may be prefixed with ; or / as well (for example ;#define),
so they can appear in assembly sources. New code should prefer the
explicit //# directives above.
CC65 custom config files
CC65's linker is controlled with configuration files that define memory areas and the layout of the final binary or ROM.
Tell the linker to use a custom config file from your main C program:
//#tooldef ld cfgfile=apple2-hgr2.cfg
//#resource "apple2-hgr2.cfg"
The legacy form is also supported:
#define CFGFILE apple2-hgr2.cfg
//#resource "apple2-hgr2.cfg"
Note the lack of quotes around the file name.
CC65 #embed directive
You can include binary files directly in
CC65 .c files, a feature not available from the CC65 command line. For
example, given an uploaded file image-c64.multi.lz4:
const char image_c64_multi_lz4[] = {
#embed "image-c64.multi.lz4"
};
This creates a byte array named image_c64_multi_lz4 with the contents
of the binary file.
The #embed directive is also supported in Oscar64 and Wiz files.
SDCC versions
Z80 C files build with SDCC 3.6.5 by default, which compiles several times faster. SDCC 4 produces smaller code (roughly 10-20% on the samples we tried) but compiles more slowly, mostly in the register allocator. To build a project with SDCC 4, add this line to the main file:
//#tooldef c sdcc=4
The whole project then compiles, assembles, and links with SDCC 4.
Platforms whose libraries were compiled with SDCC 3.6.5 can't use SDCC 4,
and //#tooldef c sdcc=4 on one of them is a build error:
| Platform | Library |
|---|---|
coleco, msx-libcv, sms-* |
libcv / libcvu |
gb |
gb.lib |
cpc.* |
cpctelera, cpcrslib |
//#tooldef c sdcc=3 selects SDCC 3.6.5 explicitly, which keeps working if
the default changes. The default is SDCC_DEFAULT_VERSION in
src/common/toolmeta.ts.
Unless the source has a #pragma opt_code_size, opt_code_speed or
opt_code_balanced, SDCC runs with --no-peep --nolospre --max-allocs-per-node 500 for fast builds. Adding one of those pragmas
turns the full optimizer back on.
Calling convention
SDCC 4 passes function arguments in registers; SDCC 3 passed them on the
stack. C code works either way, but an assembly function that reads its
arguments from the stack breaks under SDCC 4. Mark such a function
__sdcccall(0), in its prototype too, to keep the old convention:
void set_color(byte c) __sdcccall(0) __naked {
__asm
ld hl, #2
add hl, sp
ld a, (hl) ; c is on the stack
out (0x40), a
ret
__endasm;
}
Without __sdcccall(0), SDCC 4 passes c in register A.
Warnings
SDCC 4 warns about more code than SDCC 3, e.g. int main(int argc).
Warnings don't stop an SDCC 4 build and aren't marked in the editor. When
a build fails, the IDE marks its warnings along with its errors.
SDCC #pragma
With SDCC, enable additional optimization by adding one of these lines to the top of the file:
#pragma opt_code_size // for smaller code
#pragma opt_code_speed // for faster code
Without a pragma, the IDE turns off some optimizations to keep builds fast. Either pragma slows builds but produces faster and smaller code.
Game Boy ROM banks
The Game Boy builds with SDCC 3.6.5 (see SDCC versions above).
#pragma bank N at the top of a file puts its code and
constant data in ROM bank N, which the CPU sees at $4000-$7FFF. Link
the file from your main file, and call its functions through __banked
prototypes. Each call switches to the function's bank and back again:
// bank1.c
#pragma bank 1
void say_hello(char* buf) __banked { ... }
// main.c
//#link "bank1.c"
//#symbol ld GB_MAPPER=0x19
void say_hello(char* buf) __banked;
The ROM grows to fit the highest bank, and the build writes the ROM size
into the header. If GB_MAPPER is still 0 (ROM only), the build selects
MBC5. A bank holds 16 KB; overflowing it is a build error. Banked data
can only be read while its bank is mapped, so copy it out from a
__banked function. See the ROM Bank Switching preset.
File extensions
The extension of your source file determines the tool used to assemble
or compile it. The current platform resolves extensions that several
tools share (.c, .s, .bas), so the mapping depends on both. Common
cases:
| Extension | File type | Tool (platform) |
|---|---|---|
.c |
C source file | cc65 (6502), sdcc (Z80), cmoc (6809) |
-sdcc.c |
C source file on a 6502 platform | sdcc, mos6502 backend (SDCC 4 only; experimental, devel-6502) |
.cpp |
C source file | Oscar64 (6502) |
.s .ca65 |
Assembler file | ca65 (6502) |
.s |
Assembler file | sdasz80 (Z80) |
.sgb |
Assembler file | sdasgb (Game Boy) |
.dasm .a |
Assembler file | dasm (6502) |
.z |
Assembler file | zmac (Z80) |
.bas |
BASIC file | BASIC, batariBASIC (vcs), FastBASIC (atari8) |
.bb |
batariBASIC file | batariBASIC (vcs) |
.v |
Verilog file | Verilator |
See Toolchains & Platforms for the full platform × extension → tool table.