mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
sdcc: support hybrid 3.x and 4.x compilers, 3.x is the default
This commit is contained in:
1 parent
01a47873ae
commit
1327bc5bef
19 files changed
+594
-156
No files matched your search
@@ -63,6 +63,7 @@ See [extension/CHANGELOG.md](extension/CHANGELOG.md).
|
||||
- cc65 toolchain (cc65/ca65/ld65 WASI builds and the per-platform include/lib zips) updated from the 2021 build (`6ac4aa4`) to cc65 `d8a486a` (V2.19-3867, 2026-09-26). The compiler runs with `--disable-opt OptLoadStore1` (the new step drops stores after loads of the same address, which breaks hardware registers and inline asm) and `-unreachable-code`.
|
||||
- cc65 `a2.lo.s` (Apple II lo-res TGI) patched in `8bitworkshop-compilers/patches/cc65-apple2-lo-tgi.patch`: a stray `bpl :+` in `INSTALL` made `tgi_install(a2_lo_tgi)` crash on a non-enhanced Apple II.
|
||||
- Presets adjusted for the newer cc65: NES `chase` closes its zero-page `#pragma` region, C64 `side_scroller` uses an unsigned fixed-point constant, and the Atari vector presets no longer recurse into `main()`. `neslib.h` no longer redefines `NULL` if `stddef.h` already did.
|
||||
- SDCC: both 3.6.5 and 4.x are bundled; 3.6.5 is the default (several times faster to build) and `//#tooldef c sdcc=4` opts in to 4.x (the default is `SDCC_DEFAULT_VERSION` in `toolmeta.ts`). Programs without an `opt_code*` pragma now build with `--no-peep --nolospre --max-allocs-per-node 500`, including Game Boy, which previously always got the full optimizer. See *SDCC versions* in the build directives docs.
|
||||
- Build is now only `esbuild` through `scripts/build.mjs`; code split into a headless `src/common/projectcore.ts` / `toolselect.ts`.
|
||||
- New `buildpresets` script that builds and runs every preset.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ void putstring(byte x, byte y, const char* str) {
|
||||
CHPUT(*str++);
|
||||
}
|
||||
}
|
||||
byte getchar(byte x, byte y) {
|
||||
byte getchar_at(byte x, byte y) {
|
||||
word addr = 0x1800 | (x+1) | y*32; // TODO: use variable for base address
|
||||
byte result;
|
||||
result = RDVRM(addr);
|
||||
@@ -130,7 +130,7 @@ void move_player(Player* p) {
|
||||
cputcxy(p->x, p->y, p->tail_attr);
|
||||
p->x += DIR_X[p->dir];
|
||||
p->y += DIR_Y[p->dir];
|
||||
if (getchar(p->x, p->y) != CHAR(' '))
|
||||
if (getchar_at(p->x, p->y) != CHAR(' '))
|
||||
p->collided = 1;
|
||||
draw_player(p);
|
||||
}
|
||||
@@ -154,7 +154,7 @@ byte ai_try_dir(Player* p, dir_t dir, byte shift) {
|
||||
dir &= 3;
|
||||
x = p->x + (DIR_X[dir] << shift);
|
||||
y = p->y + (DIR_Y[dir] << shift);
|
||||
if (x < 29 && y < 27 && getchar(x, y) == CHAR(' ')) {
|
||||
if (x < 29 && y < 27 && getchar_at(x, y) == CHAR(' ')) {
|
||||
p->dir = dir;
|
||||
return 1;
|
||||
} else {
|
||||
|
||||
+33
-8
@@ -268,6 +268,31 @@ const TOOL_PLATFORM_HELPURL: { [tool: string]: { [platform: string]: string } }
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Which SDCC a program gets unless it says `//#tooldef c sdcc=3|4`. To make
|
||||
* 4.x the default, change this to 4: nothing else needs editing.
|
||||
* 3 = 3.6.5, Emscripten, preloadFS 'sdcc' (builds several times faster)
|
||||
* 4 = 4.x, WASI, sdcc-fs.zip (smaller code)
|
||||
*/
|
||||
export const SDCC_DEFAULT_VERSION: 3 | 4 = 3;
|
||||
|
||||
/**
|
||||
* 'default' lists both filesystems; the one for SDCC_DEFAULT_VERSION leads
|
||||
* (getSharedFileSystemName prefers preloadFS). Platforms whose prebuilt
|
||||
* libraries were compiled by SDCC 3.x (libcv, gb.lib, cpctelera) have only
|
||||
* preloadFS: 4.x's default calling convention, sdcccall(1), can't call them.
|
||||
*/
|
||||
const SDCC_FS: { [platform: string]: PlatformToolConfig } = {
|
||||
'default': SDCC_DEFAULT_VERSION === 3
|
||||
? { preloadFS: 'sdcc', wasiFSZip: 'sdcc-fs.zip' }
|
||||
: { wasiFSZip: 'sdcc-fs.zip' },
|
||||
'coleco': { preloadFS: 'sdcc' },
|
||||
'msx-libcv': { preloadFS: 'sdcc' },
|
||||
'sms': { preloadFS: 'sdcc' },
|
||||
'gb': { preloadFS: 'sdcc' },
|
||||
'cpc': { preloadFS: 'sdcc' },
|
||||
};
|
||||
|
||||
const CC65_WASIFS: { [platform: string]: PlatformToolConfig } = {
|
||||
'apple2': { wasiFSZip: 'cc65-fs-apple2.zip' },
|
||||
'c64': { wasiFSZip: 'cc65-fs-c64.zip' },
|
||||
@@ -386,13 +411,13 @@ export const TOOL_META: { [id: string]: ToolMeta } = {
|
||||
sdcc: {
|
||||
id: 'sdcc', name: 'SDCC', kind: 'compiler', arch: 'z80',
|
||||
extensions: ['.c', '.h'],
|
||||
includeDirs: ['/include'],
|
||||
includeDirs: ['/share/sdcc/include', '/include'],
|
||||
editorStyle: 'text/x-csrc',
|
||||
helpURL: 'http://sdcc.sourceforge.net/doc/sdccman.pdf',
|
||||
wasmModule: 'sdcc',
|
||||
version: '3.6.5',
|
||||
wasmModule: 'sdcc4',
|
||||
version: SDCC_DEFAULT_VERSION === 3 ? '3.6.5' : '4.6.0',
|
||||
defineFlag: '-D', defineInline: true,
|
||||
platforms: { default: { preloadFS: 'sdcc' } },
|
||||
platforms: SDCC_FS,
|
||||
includePatterns: SHARED_INCLUDE_PATTERNS,
|
||||
linkPatterns: SHARED_LINK_PATTERNS,
|
||||
},
|
||||
@@ -400,11 +425,11 @@ export const TOOL_META: { [id: string]: ToolMeta } = {
|
||||
sdasz80: {
|
||||
id: 'sdasz80', name: 'sdasz80', kind: 'assembler', arch: 'z80',
|
||||
extensions: ['.s'],
|
||||
includeDirs: ['/include'],
|
||||
includeDirs: ['/share/sdcc/include', '/include'],
|
||||
editorStyle: 'z80',
|
||||
wasmModule: 'sdasz80',
|
||||
wasmModule: 'sdasz80-4',
|
||||
version: '02.00',
|
||||
platforms: { default: { preloadFS: 'sdcc' } },
|
||||
platforms: SDCC_FS,
|
||||
includePatterns: SHARED_INCLUDE_PATTERNS,
|
||||
linkPatterns: SHARED_LINK_PATTERNS,
|
||||
},
|
||||
@@ -423,7 +448,7 @@ export const TOOL_META: { [id: string]: ToolMeta } = {
|
||||
sdldz80: {
|
||||
id: 'sdldz80', name: 'sdldz80', kind: 'linker', arch: 'z80',
|
||||
extensions: [],
|
||||
wasmModule: 'sdldz80',
|
||||
wasmModule: 'sdld4',
|
||||
version: '03.00',
|
||||
linkSymbolFlag: '-g', linkSymbolInline: false,
|
||||
},
|
||||
|
||||
@@ -336,6 +336,18 @@ export class WASIMemoryFilesystem implements WASIFilesystem {
|
||||
if (this.dirs.has(name)) return WASIErrors.ISDIR;
|
||||
return this.files.delete(name) ? WASIErrors.SUCCESS : WASIErrors.NOENT;
|
||||
}
|
||||
// Rename a file within this (writable) layer, replacing any file at newName.
|
||||
renameFile(oldName: string, newName: string) {
|
||||
oldName = normalizeWASIPath(oldName);
|
||||
newName = normalizeWASIPath(newName);
|
||||
if (this.dirs.has(oldName) || this.dirs.has(newName)) return WASIErrors.ISDIR;
|
||||
const file = this.files.get(oldName);
|
||||
if (!file) return WASIErrors.NOENT;
|
||||
this.files.delete(oldName);
|
||||
file.name = newName;
|
||||
this.files.set(newName, file);
|
||||
return WASIErrors.SUCCESS;
|
||||
}
|
||||
removeDirectory(name: string) {
|
||||
name = normalizeWASIPath(name);
|
||||
if (name === '/') return WASIErrors.BUSY; // can't remove the root
|
||||
@@ -732,6 +744,17 @@ export class WASIRunner {
|
||||
debug("path_unlink_file", dir+"", path);
|
||||
return this.fs.removeFile(path);
|
||||
}
|
||||
path_rename(old_dirfd: number, old_path_ptr: number, old_path_len: number,
|
||||
new_dirfd: number, new_path_ptr: number, new_path_len: number) {
|
||||
const olddir = this.fds[old_dirfd];
|
||||
const newdir = this.fds[new_dirfd];
|
||||
if (olddir == null || newdir == null) return WASIErrors.BADF;
|
||||
if (olddir.type !== FDType.DIRECTORY || newdir.type !== FDType.DIRECTORY) return WASIErrors.NOTDIR;
|
||||
const oldpath = olddir.name + '/' + this.peekUTF8(old_path_ptr, old_path_len);
|
||||
const newpath = newdir.name + '/' + this.peekUTF8(new_path_ptr, new_path_len);
|
||||
debug("path_rename", oldpath, newpath);
|
||||
return this.fs.renameFile(oldpath, newpath);
|
||||
}
|
||||
path_remove_directory(dirfd: number, path_ptr: number, path_len: number) {
|
||||
const dir = this.fds[dirfd];
|
||||
if (dir == null) return WASIErrors.BADF;
|
||||
@@ -778,6 +801,7 @@ export class WASIRunner {
|
||||
path_readlink: this.path_readlink.bind(this),
|
||||
path_unlink_file: this.path_unlink_file.bind(this),
|
||||
path_remove_directory: this.path_remove_directory.bind(this),
|
||||
path_rename: this.path_rename.bind(this),
|
||||
path_create_directory: this.path_create_directory.bind(this),
|
||||
clock_time_get: this.clock_time_get.bind(this),
|
||||
fd_fdstat_set_flags() { warning("TODO: fd_fdstat_set_flags"); return WASIErrors.NOTSUP; },
|
||||
|
||||
@@ -129,16 +129,18 @@ Sets a named build parameter that the build system understands:
|
||||
//#tooldef <phase> NAME=VALUE
|
||||
~~~
|
||||
|
||||
For the linker phase, the supported parameters are:
|
||||
The supported parameters are:
|
||||
|
||||
| Name | Meaning |
|
||||
| --- | --- |
|
||||
| `cfgfile` | linker configuration file to use (e.g. for CC65's ld65) |
|
||||
| `libargs` | comma-separated list of library/symbol arguments |
|
||||
| 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 |
|
||||
| `c` | `sdcc` | SDCC version, `3` or `4` (see *SDCC versions* below) |
|
||||
|
||||
~~~c
|
||||
//#tooldef ld cfgfile=apple2-hgr2.cfg
|
||||
//#tooldef ld libargs=,nes.lib
|
||||
//#tooldef c sdcc=3
|
||||
~~~
|
||||
|
||||
### Commenting out a directive
|
||||
@@ -202,6 +204,63 @@ 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:
|
||||
|
||||
~~~c
|
||||
//#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:
|
||||
|
||||
~~~c
|
||||
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
|
||||
@@ -212,11 +271,13 @@ the top of the file:
|
||||
#pragma opt_code_speed // for faster code
|
||||
~~~
|
||||
|
||||
Either pragma slows builds but produces faster and smaller 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
|
||||
|
||||
On the Game Boy, `#pragma bank N` at the top of a file puts its code and
|
||||
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:
|
||||
|
||||
@@ -466,7 +466,8 @@ export function applyAsmProjectParams(params) {
|
||||
*
|
||||
* //#symbol [<phase>] NAME[=VALUE] phase in c|as|ld (default c)
|
||||
* //#flag <phase> <args...> raw argv for one phase
|
||||
* //#tooldef <phase> NAME=VALUE typed knob (linker: cfgfile, libargs)
|
||||
* //#tooldef <phase> NAME=VALUE typed knob (linker: cfgfile, libargs;
|
||||
* compiler: sdcc=3|4)
|
||||
*
|
||||
* Symbols are phase-scoped because they are different things: a preprocessor
|
||||
* macro (`c`) takes text, while an assembler/linker global (`as`/`ld`) is an
|
||||
@@ -732,6 +733,10 @@ export function applyBuildDirectives(dir: SourceDirectives, params) {
|
||||
params.cfgfile = td.value;
|
||||
} else if (td.phase === 'linker' && td.name === 'libargs') {
|
||||
params.libargs = td.value.split(',').filter((s) => s !== '');
|
||||
} else if (td.phase === 'compiler' && td.name === 'sdcc') {
|
||||
// SDCC major version; every sdcc/sdas/sdld step of the build reads it
|
||||
if (td.value === '3' || td.value === '4') params.sdcc_version = parseInt(td.value);
|
||||
else dir.errors.push(`#tooldef c sdcc: expected 3 or 4, got '${td.value}'`);
|
||||
} else {
|
||||
dir.errors.push(`#tooldef: unknown ${td.phase} param '${td.name}'`);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,12 +1,10 @@
|
||||
|
||||
import { defineArgs, extraArgsFor, linkSymbolArgs, getSharedFileSystemName } from "../../common/toolmeta";
|
||||
import { CodeListingMap, WorkerError } from "../../common/workertypes";
|
||||
import { WASIRunner } from "../../common/wasi/wasishim";
|
||||
import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, fixParamsWithDefines, applyAsmProjectParams, putWorkFile, populateExtraFiles, store, populateEntry, anyTargetChanged, processEmbedDirective } from "../builder";
|
||||
import { re_crlf, makeErrorMatcher } from "../listingutils";
|
||||
import { parseCC65DbgSizes } from "./cc65dbg";
|
||||
import { loadWASMBinary, ensureWasiFilesystem } from "../wasmutils";
|
||||
import { wasiFSAdapter } from "../wasiutils";
|
||||
import { wasiFSAdapter, runWASITool, checkExitCode, readWASIOutput, readWASIOutputString } from "../wasiutils";
|
||||
|
||||
// the WASI builds look for their data under share/cc65 in the per-platform
|
||||
// cc65-fs-<platform>.zip, preopened at '.'
|
||||
@@ -29,44 +27,9 @@ async function runCC65Tool(step: BuildStep, tool: string, args: string[],
|
||||
const fsname = getSharedFileSystemName('cc65', step.platform);
|
||||
if (!fsname || !fsname.startsWith('wasi:'))
|
||||
throw new Error("No cc65 filesystem for platform " + step.platform);
|
||||
const sharefs = await ensureWasiFilesystem(fsname.substring(5));
|
||||
if (!sharefs)
|
||||
throw new Error("Could not load cc65 filesystem " + fsname);
|
||||
if (!wasiModules[tool]) {
|
||||
wasiModules[tool] = new WebAssembly.Module(loadWASMBinary(tool));
|
||||
}
|
||||
const wasi = new WASIRunner();
|
||||
wasi.initSync(wasiModules[tool]);
|
||||
wasi.fs.setParent(sharefs);
|
||||
populate(wasiFSAdapter(wasi));
|
||||
wasi.addPreopenDirectory(".");
|
||||
wasi.setArgs([tool, ...args]);
|
||||
const errno = wasi.run();
|
||||
console.log('exec', tool, args.join(' '));
|
||||
const stdout = wasi.fds[1].getBytesAsString();
|
||||
if (stdout) console.log(stdout);
|
||||
const stderr = wasi.fds[2].getBytesAsString().split(re_crlf).filter(s => s != '');
|
||||
return { wasi, errno, stderr };
|
||||
return runWASITool(tool, args, { sharedFS: fsname.substring(5), populate });
|
||||
}
|
||||
|
||||
/** Report a failed tool run that printed no parseable error message. */
|
||||
function checkExitCode(tool: string, errno: number, stderr: string[], errors: WorkerError[]) {
|
||||
if (errno && !errors.length) {
|
||||
errors.push({ line: 0, msg: tool + " exited with code " + errno + (stderr.length ? ": " + stderr.join('\n') : '') });
|
||||
}
|
||||
}
|
||||
|
||||
function readWASIOutput(wasi: WASIRunner, path: string): Uint8Array {
|
||||
const fd = wasi.fs.getFile(path);
|
||||
if (!fd) throw new Error("Missing output file " + path);
|
||||
return fd.getBytes().slice();
|
||||
}
|
||||
|
||||
function readWASIOutputString(wasi: WASIRunner, path: string): string {
|
||||
return new TextDecoder().decode(readWASIOutput(wasi, path));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
000000r 1 .segment "CODE"
|
||||
000000r 1 .proc _rasterWait: near
|
||||
|
||||
@@ -4,12 +4,46 @@ import { BuildStep, populateFiles, populateExtraFiles, errorResult, processEmbed
|
||||
import { makeErrorMatcher, extractErrors } from "../listingutils";
|
||||
import { PLATFORM_PARAMS } from "../platforms";
|
||||
import { load, print_fn, setupFS, execMain, emglobal, EmscriptenModule } from "../wasmutils";
|
||||
import type { WASIMemoryFilesystem } from "../../common/wasi/wasishim";
|
||||
|
||||
function makeCPPSafe(s: string): string {
|
||||
return s.replace(/[^A-Za-z0-9_]/g, '_');
|
||||
}
|
||||
|
||||
export function preprocessMCPP(step: BuildStep, filesys: string) {
|
||||
/** Headers in a WASI filesystem zip, under `dir`, for mcpp's /share/include. */
|
||||
export interface MCPPIncludeSource {
|
||||
fs: WASIMemoryFilesystem;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
/** Copy the headers under src.dir into an Emscripten FS at /share/include. */
|
||||
function copyWASIIncludes(FS, src: MCPPIncludeSource) {
|
||||
const prefix = src.dir + '/';
|
||||
const made = new Set<string>();
|
||||
const mkdirs = (path: string) => {
|
||||
let parts = path.split('/');
|
||||
for (let i = 2; i < parts.length; i++) {
|
||||
let dir = parts.slice(0, i).join('/');
|
||||
if (!made.has(dir)) {
|
||||
if (!FS.analyzePath(dir).exists) FS.mkdir(dir);
|
||||
made.add(dir);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (let file of src.fs.getFiles()) {
|
||||
if (file.name.startsWith(prefix)) {
|
||||
let path = '/share/include/' + file.name.substring(prefix.length);
|
||||
mkdirs(path);
|
||||
FS.writeFile(path, file.getBytes(), { encoding: 'binary' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess the step's main source. `filesys` names the Emscripten FS
|
||||
* package mounted at /share, or gives headers to copy into /share/include.
|
||||
*/
|
||||
export function preprocessMCPP(step: BuildStep, filesys: string | MCPPIncludeSource, extraArgs: string[] = []) {
|
||||
load("mcpp");
|
||||
var platform = step.platform;
|
||||
var params = PLATFORM_PARAMS[platform] || PLATFORM_PARAMS[getBasePlatform(platform)];
|
||||
@@ -24,7 +58,8 @@ export function preprocessMCPP(step: BuildStep, filesys: string) {
|
||||
printErr: match_fn,
|
||||
});
|
||||
var FS = MCPP.FS;
|
||||
if (filesys) setupFS(FS, filesys);
|
||||
if (typeof filesys === 'string') setupFS(FS, filesys);
|
||||
else if (filesys) copyWASIIncludes(FS, filesys);
|
||||
populateFiles(step, FS, {
|
||||
mainFilePath: step.path,
|
||||
processFn: (path, code) => {
|
||||
@@ -49,6 +84,7 @@ export function preprocessMCPP(step: BuildStep, filesys: string) {
|
||||
}
|
||||
let platform_def = (platform.toUpperCase() as any).replaceAll(/[^a-zA-Z0-9]/g, '_');
|
||||
args.unshift.apply(args, ["-D", `__PLATFORM_${platform_def}__`]);
|
||||
args.unshift(...extraArgs);
|
||||
if (params.extra_preproc_args) {
|
||||
args.push.apply(args, params.extra_preproc_args);
|
||||
}
|
||||
|
||||
+226
-85
@@ -1,11 +1,54 @@
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { defineArgs, extraArgsFor, linkSymbolArgs } from "../../common/toolmeta";
|
||||
import { defineArgs, extraArgsFor, linkSymbolArgs, getPlatformToolConfig, SDCC_DEFAULT_VERSION } from "../../common/toolmeta";
|
||||
import { CodeListingMap, WorkerError } from "../../common/workertypes";
|
||||
import { BuildStep, BuildStepResult, gatherFiles, staleFiles, populateFiles, putWorkFile, populateExtraFiles, anyTargetChanged, getWorkFileAsString, fixParamsWithDefines, applyAsmProjectParams } from "../builder";
|
||||
import { parseListing, parseSourceLines, msvcErrorMatcher } from "../listingutils";
|
||||
import { EmscriptenModule, emglobal, execMain, loadNative, moduleInstFn, print_fn, setupFS, setupStdin } from "../wasmutils";
|
||||
import { EmscriptenModule, emglobal, ensureFilesystem, ensureWasiFilesystem, execMain, loadNative, moduleInstFn, print_fn, setupFS, setupStdin } from "../wasmutils";
|
||||
import { runWASITool, checkExitCode, readWASIOutputString } from "../wasiutils";
|
||||
import { preprocessMCPP } from "./mcpp";
|
||||
|
||||
// SDCC 4.x runs as WASI over sdcc-fs.zip, which holds share/sdcc/{include,lib/<port>}.
|
||||
// Platforms with libraries built by SDCC 3.x stay on the 3.6.5 Emscripten build
|
||||
// (see SDCC_FS in toolmeta).
|
||||
const SDCC4_INCLUDE = 'share/sdcc/include';
|
||||
const SDCC4_LIB = 'share/sdcc/lib';
|
||||
|
||||
// what the sdcc driver predefines when it runs sdcpp; mcpp must supply them
|
||||
const SDCC4_DEFINES = [
|
||||
'-D', '__SDCC=4_6_3',
|
||||
'-D', '__SDCC_VERSION_MAJOR=4',
|
||||
'-D', '__SDCC_VERSION_MINOR=6',
|
||||
'-D', '__SDCC_VERSION_PATCH=3',
|
||||
'-D', '__SDCCCALL=1',
|
||||
'-D', '__STDC_NO_COMPLEX__=1',
|
||||
'-D', '__STDC_NO_THREADS__=1',
|
||||
'-D', '__STDC_NO_ATOMICS__=1',
|
||||
'-D', '__STDC_NO_VLA__=1',
|
||||
];
|
||||
|
||||
/**
|
||||
* The SDCC 4.x filesystem zip for this build, or null for SDCC 3.6.5. The
|
||||
* version is `//#tooldef c sdcc=3|4` (params.sdcc_version) or else
|
||||
* SDCC_DEFAULT_VERSION. Platforms with 3.x-only libraries always get 3.x,
|
||||
* and asking for 4.x there is an error.
|
||||
*/
|
||||
function sdcc4FS(step: BuildStep): string | null {
|
||||
const requested = step.params.sdcc_version;
|
||||
if ((requested ?? SDCC_DEFAULT_VERSION) !== 4) return null;
|
||||
const zip = getPlatformToolConfig('sdcc', step.platform)?.wasiFSZip;
|
||||
if (!zip && requested === 4) {
|
||||
throw new Error(`SDCC 4 can't build for ${step.platform}: its libraries were compiled by SDCC 3. Remove "//#tooldef c sdcc=4".`);
|
||||
}
|
||||
return zip || null;
|
||||
}
|
||||
|
||||
/** True if the step's cached asm output came from the other SDCC version. */
|
||||
function builtByOtherSDCC(outpath: string, fs4: string | null): boolean {
|
||||
const asm = getWorkFileAsString(outpath);
|
||||
if (typeof asm !== 'string') return false;
|
||||
const m = /^; Version (\d+)\./m.exec(asm);
|
||||
return !!m && (m[1] === '4') !== !!fs4;
|
||||
}
|
||||
|
||||
function hexToArray(s, ofs) {
|
||||
var buf = new ArrayBuffer(s.length / 2);
|
||||
var arr = new Uint8Array(buf);
|
||||
@@ -41,6 +84,19 @@ export function bankedAreaArgs(rels: string[], banking: ROMBanking): string[] {
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if any of the object files declares the area, or if one of them can't
|
||||
* be read (so the caller keeps its default).
|
||||
*/
|
||||
export function objectsDefineArea(objfiles: string[], area: string): boolean {
|
||||
for (let fn of objfiles) {
|
||||
if (!fn.endsWith('.rel')) continue;
|
||||
let rel = getWorkFileAsString(fn);
|
||||
if (typeof rel !== 'string' || new RegExp(`^A ${area} `, 'm').test(rel)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Intel HEX to a ROM image of rom_size bytes. With banking, records
|
||||
* above 64 KB are placed by bank, and the image grows to the next power of 2
|
||||
@@ -157,8 +213,6 @@ function errorMatcherSDASZ80(path: string, errors: WorkerError[]) {
|
||||
}
|
||||
|
||||
async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promise<BuildStepResult> {
|
||||
loadNative(tool);
|
||||
var objout, lstout, symout;
|
||||
var errors = [];
|
||||
gatherFiles(step, { mainFilePath: "main.asm" });
|
||||
var objpath = step.prefix + ".rel";
|
||||
@@ -169,24 +223,41 @@ async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promis
|
||||
}
|
||||
if (staleFiles(step, [objpath, lstpath])) {
|
||||
const match_asm_fn = errorMatcherSDASZ80(step.path, errors);
|
||||
var AS: EmscriptenModule = emglobal[tool]({
|
||||
instantiateWasm: moduleInstFn(tool),
|
||||
noInitialRun: true,
|
||||
//logReadFiles:true,
|
||||
print: match_asm_fn,
|
||||
printErr: match_asm_fn,
|
||||
});
|
||||
// old-style Emscripten modules return the Module object, whose .then()
|
||||
// only resolves after main() runs; newer MODULARIZE factories return a Promise
|
||||
if (AS instanceof Promise) AS = await AS;
|
||||
var FS = AS.FS;
|
||||
populateFiles(step, FS);
|
||||
execMain(step, AS, ['-plosgffwy', step.path]);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
const args = ['-plosgffwy', step.path];
|
||||
var objout, lstout;
|
||||
if (tool == 'sdasz80' && sdcc4FS(step)) {
|
||||
const { wasi, errno, stdout, stderr } = await runWASITool(tool, args, {
|
||||
module: 'sdasz80-4',
|
||||
populate: (fs) => populateFiles(step, fs),
|
||||
});
|
||||
stdout.concat(stderr).forEach(match_asm_fn);
|
||||
checkExitCode(tool, errno, stderr, errors);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
objout = readWASIOutputString(wasi, objpath);
|
||||
lstout = readWASIOutputString(wasi, lstpath);
|
||||
} else {
|
||||
loadNative(tool);
|
||||
var AS: EmscriptenModule = emglobal[tool]({
|
||||
instantiateWasm: moduleInstFn(tool),
|
||||
noInitialRun: true,
|
||||
//logReadFiles:true,
|
||||
print: match_asm_fn,
|
||||
printErr: match_asm_fn,
|
||||
});
|
||||
// old-style Emscripten modules return the Module object, whose .then()
|
||||
// only resolves after main() runs; newer MODULARIZE factories return a Promise
|
||||
if (AS instanceof Promise) AS = await AS;
|
||||
var FS = AS.FS;
|
||||
populateFiles(step, FS);
|
||||
execMain(step, AS, args);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
objout = FS.readFile(objpath, { encoding: 'utf8' });
|
||||
lstout = FS.readFile(lstpath, { encoding: 'utf8' });
|
||||
}
|
||||
objout = FS.readFile(objpath, { encoding: 'utf8' });
|
||||
lstout = FS.readFile(lstpath, { encoding: 'utf8' });
|
||||
putWorkFile(objpath, objout);
|
||||
putWorkFile(lstpath, lstout);
|
||||
}
|
||||
@@ -195,7 +266,6 @@ async function assembleSDAS(step: BuildStep, tool: 'sdasz80' | 'sdasgb'): Promis
|
||||
files: [objpath, lstpath],
|
||||
args: [objpath]
|
||||
};
|
||||
//symout = FS.readFile("main.sym", {encoding:'utf8'});
|
||||
}
|
||||
|
||||
export function assembleSDASZ80(step: BuildStep): Promise<BuildStepResult> {
|
||||
@@ -206,9 +276,9 @@ export function assembleSDASGB(step: BuildStep): Promise<BuildStepResult> {
|
||||
return assembleSDAS(step, 'sdasgb');
|
||||
}
|
||||
|
||||
export function linkSDLDZ80(step: BuildStep) {
|
||||
loadNative("sdldz80");
|
||||
export async function linkSDLDZ80(step: BuildStep) {
|
||||
const arch = step.params.arch || 'z80';
|
||||
const fs4 = sdcc4FS(step);
|
||||
var errors = [];
|
||||
gatherFiles(step);
|
||||
var binpath = "main.ihx";
|
||||
@@ -225,28 +295,16 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
}
|
||||
}
|
||||
var params = step.params;
|
||||
var LDZ80: EmscriptenModule = emglobal.sdldz80({
|
||||
instantiateWasm: moduleInstFn('sdldz80'),
|
||||
noInitialRun: true,
|
||||
//logReadFiles:true,
|
||||
print: match_aslink_fn,
|
||||
printErr: match_aslink_fn,
|
||||
});
|
||||
var FS = LDZ80.FS;
|
||||
setupFS(FS, 'sdcc');
|
||||
populateFiles(step, FS);
|
||||
populateExtraFiles(step, FS, params.extra_link_files);
|
||||
// TODO: coleco hack so that -u flag works
|
||||
if (step.platform.startsWith("coleco")) {
|
||||
FS.writeFile('crt0.rel', FS.readFile('/share/lib/coleco/crt0.rel', { encoding: 'utf8' }));
|
||||
FS.writeFile('crt0.lst', '\n'); // TODO: needed so -u flag works
|
||||
var libdir = fs4 ? `${SDCC4_LIB}/${arch}` : arch === 'z80' ? '/share/lib/z80' : '.'; // sm83.lib copied to current (.) directory
|
||||
var args = ['-mjwxyu', '-i', 'main.ihx'];
|
||||
// sdld 4.x fails on -b for an area no module defines (an asm-only
|
||||
// program may have no _DATA)
|
||||
var bases = { _CODE: params.codeseg_start || params.code_start, _DATA: params.data_start };
|
||||
for (let area in bases) {
|
||||
if (!fs4 || objectsDefineArea(step.args.concat(params.extra_link_args || []), area))
|
||||
args.push('-b', `${area}=0x${bases[area].toString(16)}`);
|
||||
}
|
||||
var args = ['-mjwxyu',
|
||||
'-i', 'main.ihx',
|
||||
'-b', '_CODE=0x' + (params.codeseg_start||params.code_start).toString(16),
|
||||
'-b', '_DATA=0x' + params.data_start.toString(16),
|
||||
'-k', arch === 'z80' ? '/share/lib/z80' : '.', // sm83.lib copied to current (.) directory
|
||||
'-l', arch];
|
||||
args.push('-k', libdir, '-l', arch);
|
||||
if (params.extra_link_args)
|
||||
args.push.apply(args, params.extra_link_args);
|
||||
// //#symbol ld (sdldz80 uses -g sym=expr) and //#flag ld
|
||||
@@ -274,13 +332,50 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
}
|
||||
objargs = withStartupObjects(params.startup_objs, objargs);
|
||||
args.push.apply(args, objargs);
|
||||
//console.log(args);
|
||||
execMain(step, LDZ80, args);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
var readText: (path: string) => string;
|
||||
if (fs4) {
|
||||
// one sdld binary for all targets: it picks z80 from argv[0]
|
||||
const { wasi, errno, stdout, stderr } = await runWASITool('sdldz80', args, {
|
||||
module: 'sdld4',
|
||||
sharedFS: fs4,
|
||||
populate: (fs) => {
|
||||
populateFiles(step, fs);
|
||||
populateExtraFiles(step, fs, params.extra_link_files);
|
||||
},
|
||||
});
|
||||
stdout.concat(stderr).forEach(match_aslink_fn);
|
||||
checkExitCode('sdldz80', errno, stderr, errors);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
readText = (path) => readWASIOutputString(wasi, path);
|
||||
} else {
|
||||
loadNative("sdldz80");
|
||||
var LDZ80: EmscriptenModule = emglobal.sdldz80({
|
||||
instantiateWasm: moduleInstFn('sdldz80'),
|
||||
noInitialRun: true,
|
||||
//logReadFiles:true,
|
||||
print: match_aslink_fn,
|
||||
printErr: match_aslink_fn,
|
||||
});
|
||||
var FS = LDZ80.FS;
|
||||
ensureFilesystem('sdcc');
|
||||
setupFS(FS, 'sdcc');
|
||||
populateFiles(step, FS);
|
||||
populateExtraFiles(step, FS, params.extra_link_files);
|
||||
// TODO: coleco hack so that -u flag works
|
||||
if (step.platform.startsWith("coleco")) {
|
||||
FS.writeFile('crt0.rel', FS.readFile('/share/lib/coleco/crt0.rel', { encoding: 'utf8' }));
|
||||
FS.writeFile('crt0.lst', '\n'); // TODO: needed so -u flag works
|
||||
}
|
||||
execMain(step, LDZ80, args);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
readText = (path) => FS.readFile(path, { encoding: 'utf8' });
|
||||
}
|
||||
var hexout = FS.readFile("main.ihx", { encoding: 'utf8' });
|
||||
var noiout = FS.readFile("main.noi", { encoding: 'utf8' });
|
||||
var hexout = readText("main.ihx");
|
||||
var noiout = readText("main.noi");
|
||||
putWorkFile("main.ihx", hexout);
|
||||
putWorkFile("main.noi", noiout);
|
||||
// return unchanged if no files changed
|
||||
@@ -295,17 +390,9 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
var listings: CodeListingMap = {};
|
||||
for (var fn of step.files) {
|
||||
if (fn.endsWith('.lst')) {
|
||||
var rstout = FS.readFile(fn.replace('.lst', '.rst'), { encoding: 'utf8' });
|
||||
// 0000 21 02 00 [10] 52 ld hl, #2
|
||||
var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,6})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3);
|
||||
var srclines = parseSourceLines(rstout, /^\s+\d+ ;<stdin>:(\d+):/i, /^\s*([0-9A-F]{4,6})/i);
|
||||
var rstout = readText(fn.replace('.lst', '.rst'));
|
||||
putWorkFile(fn, rstout);
|
||||
// TODO: you have to get rid of all source lines to get asm listing
|
||||
listings[fn] = {
|
||||
asmlines: srclines.length ? asmlines : null,
|
||||
lines: srclines.length ? srclines : asmlines,
|
||||
text: rstout
|
||||
};
|
||||
listings[fn] = parseRSTListing(rstout, fn.replace(/\.lst$/, ''));
|
||||
}
|
||||
}
|
||||
// parse symbol map
|
||||
@@ -361,6 +448,27 @@ export function linkSDLDZ80(step: BuildStep) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a linked (.rst) listing into asm lines and, when the compiler left
|
||||
* source comments, source lines. `srcprefix` is the source file name without
|
||||
* its extension: SDCC 3.6.5 writes `;<stdin>:N:` comments, SDCC 4.x writes
|
||||
* `;file.c:N: <source>` (with mcpp's `//` path prefix), and 4.x addresses
|
||||
* have 8 hex digits instead of 4.
|
||||
*/
|
||||
export function parseRSTListing(rstout: string, srcprefix: string) {
|
||||
// 0000 21 02 00 [10] 52 ld hl, #2
|
||||
var asmlines = parseListing(rstout, /^\s*([0-9A-F]{4,8})\s+([0-9A-F][0-9A-F r]*[0-9A-F])\s+\[([0-9 ]+)\]?\s+(\d+) (.*)/i, 4, 1, 2, 3);
|
||||
const name = srcprefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const srcre = new RegExp(`^\\s+\\d+ ;(?:<stdin>|/*${name}\\.[^:\\s]+):(\\d+):`, 'i');
|
||||
var srclines = parseSourceLines(rstout, srcre, /^\s*([0-9A-F]{4,8})/i);
|
||||
// TODO: you have to get rid of all source lines to get asm listing
|
||||
return {
|
||||
asmlines: srclines.length ? asmlines : null,
|
||||
lines: srclines.length ? srclines : asmlines,
|
||||
text: rstout
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* sdcc 3.6.5 compiles a __banked call to `call banked_call; .dw _fn; .dw 0`,
|
||||
* with a "PENDING: bank support" comment where the bank number belongs. Supply
|
||||
@@ -392,7 +500,7 @@ export function withStartupObjects(startup: string[] | undefined, objargs: strin
|
||||
return startup.filter((fn) => !own.has(fn)).concat(objargs);
|
||||
}
|
||||
|
||||
export function compileSDCC(step: BuildStep): BuildStepResult {
|
||||
export async function compileSDCC(step: BuildStep): Promise<BuildStepResult> {
|
||||
|
||||
gatherFiles(step, {
|
||||
mainFilePath: "main.c" // not used
|
||||
@@ -401,29 +509,25 @@ export function compileSDCC(step: BuildStep): BuildStepResult {
|
||||
var isGBZ80 = params.arch === 'gbz80';
|
||||
var outpath = step.prefix + ".asm";
|
||||
fixParamsWithDefines(step.path, params); // //#symbol, //#flag, //#tooldef
|
||||
if (staleFiles(step, [outpath])) {
|
||||
const fs4 = sdcc4FS(step); // after the directives: //#tooldef c sdcc=3|4
|
||||
if (staleFiles(step, [outpath]) || builtByOtherSDCC(outpath, fs4)) {
|
||||
var errors = [];
|
||||
loadNative('sdcc');
|
||||
var SDCC: EmscriptenModule = emglobal.sdcc({
|
||||
instantiateWasm: moduleInstFn('sdcc'),
|
||||
noInitialRun: true,
|
||||
noFSInit: true,
|
||||
print: print_fn,
|
||||
printErr: msvcErrorMatcher(errors),
|
||||
//TOTAL_MEMORY:256*1024*1024,
|
||||
});
|
||||
var FS = SDCC.FS;
|
||||
populateFiles(step, FS);
|
||||
// load source file and preprocess
|
||||
var code = getWorkFileAsString(step.path);
|
||||
var preproc = preprocessMCPP(step, 'sdcc');
|
||||
var preproc;
|
||||
if (fs4) {
|
||||
const sharefs = await ensureWasiFilesystem(fs4);
|
||||
if (!sharefs) throw new Error("Could not load SDCC filesystem " + fs4);
|
||||
preproc = preprocessMCPP(step, { fs: sharefs, dir: SDCC4_INCLUDE }, SDCC4_DEFINES);
|
||||
} else {
|
||||
ensureFilesystem('sdcc');
|
||||
preproc = preprocessMCPP(step, 'sdcc');
|
||||
}
|
||||
if (preproc.errors) {
|
||||
return { errors: preproc.errors };
|
||||
}
|
||||
else code = preproc.code;
|
||||
// pipe file to stdin
|
||||
setupStdin(FS, code);
|
||||
setupFS(FS, 'sdcc');
|
||||
// mcpp keeps a UTF-8 byte order mark, which 4.x rejects
|
||||
else code = preproc.code.replace(/\uFEFF/g, '');
|
||||
const machineFlags = isGBZ80 ? '-mgbz80' : '-mz80';
|
||||
var args = ['--vc', '--std-sdcc99', machineFlags, //'-Wall',
|
||||
'--c1mode',
|
||||
@@ -458,13 +562,50 @@ export function compileSDCC(step: BuildStep): BuildStepResult {
|
||||
// //#symbol c and //#flag c
|
||||
args.push.apply(args, defineArgs('sdcc', params.symbols && params.symbols.compiler));
|
||||
args.push.apply(args, extraArgsFor('sdcc', params.buildArgs));
|
||||
execMain(step, SDCC, args);
|
||||
// TODO: preprocessor errors w/ correct file
|
||||
if (errors.length /* && nwarnings < msvc_errors.length*/) {
|
||||
return { errors: errors };
|
||||
var asmout: string;
|
||||
if (fs4) {
|
||||
// --c1mode reads the preprocessed source from stdin
|
||||
const { wasi, errno, stdout, stderr } = await runWASITool('sdcc', args, {
|
||||
module: 'sdcc4',
|
||||
stdin: code,
|
||||
populate: (fs) => populateFiles(step, fs),
|
||||
});
|
||||
// 4.x warns more than 3.6.5 did (e.g. `int main(int argc)`), and
|
||||
// WorkerError has no severity: only a failed run reports them
|
||||
stderr.forEach(msvcErrorMatcher(errors));
|
||||
if (!errno) {
|
||||
errors.forEach((e) => console.log('sdcc warning:', e.path + ':' + e.line, e.msg));
|
||||
errors = [];
|
||||
}
|
||||
checkExitCode('sdcc', errno, stderr, errors);
|
||||
if (errors.length) {
|
||||
return { errors: errors };
|
||||
}
|
||||
asmout = readWASIOutputString(wasi, outpath);
|
||||
} else {
|
||||
loadNative('sdcc');
|
||||
var SDCC: EmscriptenModule = emglobal.sdcc({
|
||||
instantiateWasm: moduleInstFn('sdcc'),
|
||||
noInitialRun: true,
|
||||
noFSInit: true,
|
||||
print: print_fn,
|
||||
printErr: msvcErrorMatcher(errors),
|
||||
//TOTAL_MEMORY:256*1024*1024,
|
||||
});
|
||||
var FS = SDCC.FS;
|
||||
populateFiles(step, FS);
|
||||
// pipe file to stdin
|
||||
setupStdin(FS, code);
|
||||
ensureFilesystem('sdcc'); // not preloaded on a 4.x platform built with sdcc=3
|
||||
setupFS(FS, 'sdcc');
|
||||
execMain(step, SDCC, args);
|
||||
// TODO: preprocessor errors w/ correct file
|
||||
if (errors.length /* && nwarnings < msvc_errors.length*/) {
|
||||
return { errors: errors };
|
||||
}
|
||||
asmout = FS.readFile(outpath, { encoding: 'utf8' });
|
||||
}
|
||||
// massage the asm output
|
||||
var asmout = FS.readFile(outpath, { encoding: 'utf8' });
|
||||
asmout = " .area _HOME\n .area _CODE\n .area _INITIALIZER\n .area _DATA\n .area _INITIALIZED\n .area _BSEG\n .area _BSS\n .area _HEAP\n" + asmout;
|
||||
if (isGBZ80) asmout = fixBankedCalls(asmout);
|
||||
putWorkFile(outpath, asmout);
|
||||
|
||||
@@ -2,6 +2,8 @@ import JSZip from 'jszip';
|
||||
import { WASIRunner, WASIMemoryFilesystem } from "../common/wasi/wasishim";
|
||||
import { WorkerError } from "../common/workertypes";
|
||||
import { BuildStep, store } from "./builder";
|
||||
import { re_crlf } from "./listingutils";
|
||||
import { ensureWasiFilesystem, loadWASMBinary } from "./wasmutils";
|
||||
|
||||
export function loadBlobSync(path: string) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
@@ -73,3 +75,63 @@ export function wasiFSAdapter(wasi: WASIRunner) {
|
||||
utime: () => { },
|
||||
};
|
||||
}
|
||||
|
||||
const wasiModules: { [module: string]: WebAssembly.Module } = {};
|
||||
|
||||
export interface WASIToolOptions {
|
||||
module?: string; // wasm module name, if not the same as argv[0]
|
||||
sharedFS?: string; // filesystem zip layered under the runner (parent layer)
|
||||
stdin?: string; // text fed to the tool's stdin
|
||||
populate?: (fs: ReturnType<typeof wasiFSAdapter>) => void; // copy inputs in
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a WASI tool on a fresh runner with '.' preopened, optionally layered over
|
||||
* a shared filesystem zip. argv[0] is `tool` (some tools, like sdld, pick
|
||||
* their target from it). Returns the runner (for reading outputs), its exit
|
||||
* code and its stdout and stderr lines.
|
||||
*/
|
||||
export async function runWASITool(tool: string, args: string[], opts: WASIToolOptions = {}) {
|
||||
const module = opts.module || tool;
|
||||
const wasi = new WASIRunner();
|
||||
if (opts.sharedFS) {
|
||||
const sharefs = await ensureWasiFilesystem(opts.sharedFS);
|
||||
if (!sharefs)
|
||||
throw new Error("Could not load filesystem " + opts.sharedFS);
|
||||
wasi.fs.setParent(sharefs);
|
||||
}
|
||||
if (!wasiModules[module]) {
|
||||
wasiModules[module] = new WebAssembly.Module(loadWASMBinary(module) as Uint8Array<ArrayBuffer>);
|
||||
}
|
||||
wasi.initSync(wasiModules[module]);
|
||||
if (opts.populate) opts.populate(wasiFSAdapter(wasi));
|
||||
if (opts.stdin != null) {
|
||||
wasi.stdin.write(new TextEncoder().encode(opts.stdin));
|
||||
wasi.stdin.offset = 0; // so fd_read starts at the beginning
|
||||
}
|
||||
wasi.addPreopenDirectory(".");
|
||||
wasi.setArgs([tool, ...args]);
|
||||
const errno = wasi.run();
|
||||
console.log('exec', tool, args.join(' '));
|
||||
const stdout = wasi.fds[1].getBytesAsString().split(re_crlf).filter(s => s != '');
|
||||
if (stdout.length) console.log(stdout.join('\n'));
|
||||
const stderr = wasi.fds[2].getBytesAsString().split(re_crlf).filter(s => s != '');
|
||||
return { wasi, errno, stdout, stderr };
|
||||
}
|
||||
|
||||
/** Report a failed tool run that printed no parseable error message. */
|
||||
export function checkExitCode(tool: string, errno: number, stderr: string[], errors: WorkerError[]) {
|
||||
if (errno && !errors.length) {
|
||||
errors.push({ line: 0, msg: tool + " exited with code " + errno + (stderr.length ? ": " + stderr.join('\n') : '') });
|
||||
}
|
||||
}
|
||||
|
||||
export function readWASIOutput(wasi: WASIRunner, path: string): Uint8Array {
|
||||
const fd = wasi.fs.getFile(path);
|
||||
if (!fd) throw new Error("Missing output file " + path);
|
||||
return fd.getBytes().slice();
|
||||
}
|
||||
|
||||
export function readWASIOutputString(wasi: WASIRunner, path: string): string {
|
||||
return new TextDecoder().decode(readWASIOutput(wasi, path));
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
+67
-9
@@ -18,6 +18,7 @@ global.onmessage({data:{preload:'ca65', platform:'atari2600'}});
|
||||
global.onmessage({data:{preload:'cc65', platform:'pce'}});
|
||||
global.onmessage({data:{preload:'ca65', platform:'pce'}});
|
||||
global.onmessage({data:{preload:'sdcc'}});
|
||||
global.onmessage({data:{preload:'sdcc', platform:'coleco'}}); // SDCC 3.6.5 (Emscripten) filesystem
|
||||
global.onmessage({data:{preload:'inform6'}});
|
||||
|
||||
// TODO: check msg against spec
|
||||
@@ -183,11 +184,45 @@ describe('Worker', function() {
|
||||
it('should NOT link SDASZ80', function(done) {
|
||||
compile('sdasz80', '\tcall divxxx\n', 'mw8080bw', done, 0, 0, 1, {ignoreErrorPath:true});
|
||||
});
|
||||
it('should compile SDCC', function(done) {
|
||||
compile('sdcc', 'int foo=0; // comment\n#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main(int argc) {\nint x=1;\nint y=2+argc;\nreturn x+y+argc;\n}\n#endif\n', 'mw8080bw', done, 8192, 3, 0);
|
||||
// 3.6.5 marks fewer source lines than 4.x (3 here, vs 4)
|
||||
const SDCC_TEST_SRC = 'int foo=0; // comment\n#if defined(__8BITWORKSHOP__) && defined(__MAIN__)\nint main(int argc) {\nint x=1;\nint y=2+argc;\nreturn x+y+argc;\n}\n#endif\n';
|
||||
const SDCC_DEFAULT = +/SDCC_DEFAULT_VERSION[^=]*= *(\d)/.exec(fs.readFileSync('src/common/toolmeta.ts', 'utf8'))[1];
|
||||
const sdccLines = (n3, n4) => SDCC_DEFAULT == 4 ? n4 : n3; // listing line counts differ by version
|
||||
it('should compile SDCC with the default version', function(done) {
|
||||
compile('sdcc', SDCC_TEST_SRC, 'mw8080bw', done, 8192, sdccLines(3, 4), 0);
|
||||
});
|
||||
it('should compile SDCC 3 with //#tooldef c sdcc=3', function(done) {
|
||||
compile('sdcc', '//#tooldef c sdcc=3\n' + SDCC_TEST_SRC, 'mw8080bw', done, 8192, 3, 0);
|
||||
});
|
||||
it('should compile SDCC 4 with //#tooldef c sdcc=4', function(done) {
|
||||
compile('sdcc', '//#tooldef c sdcc=4\n' + SDCC_TEST_SRC, 'mw8080bw', done, 8192, 4, 0);
|
||||
});
|
||||
it('should pass --max-allocs-per-node unless #pragma opt_code', async function() {
|
||||
for (const platform of ['mw8080bw', 'gb']) {
|
||||
for (const ver of platform == 'gb' ? [3] : [3, 4]) {
|
||||
for (const pragma of ['', '#pragma opt_code_speed\n']) {
|
||||
var result;
|
||||
global.postMessage = (msg) => { result = msg; };
|
||||
await global.onmessage({data:{reset:true}});
|
||||
const logs = []; const ol = console.log; console.log = (...a) => logs.push(a.join(' '));
|
||||
try {
|
||||
await global.onmessage({data:{code:'//#tooldef c sdcc=' + ver + '\n' + pragma + 'void main() {\n}\n',
|
||||
platform:platform, tool:'sdcc', path:'src.sdcc', mainfile:true}});
|
||||
} finally { console.log = ol; }
|
||||
const what = [platform, 'sdcc', ver, JSON.stringify(pragma)].join(' ');
|
||||
// the test worker has no gb filesystem package, so only the compile command is checked there
|
||||
if (platform != 'gb') assert.ok(!result.errors || !result.errors.length, what + ': ' + JSON.stringify(result.errors));
|
||||
const cmd = logs.find(l => /^exec sdcc --vc/.test(l)) || '';
|
||||
assert.equal(/--no-peep --nolospre --max-allocs-per-node 500/.test(cmd), !pragma, what + ': ' + cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
it('should NOT compile SDCC 4 for a 3.x-library platform', function(done) {
|
||||
compile('sdcc', '//#tooldef c sdcc=4\nvoid main() {\n}\n', 'coleco', done, 0, 0, 1, {ignoreErrorPath:true});
|
||||
});
|
||||
it('should compile SDCC w/ include', function(done) {
|
||||
compile('sdcc', '#include <string.h>\nvoid main() {\nstrlen(0);\n}\n', 'mw8080bw', done, 8192, 2, 0);
|
||||
compile('sdcc', '#include <string.h>\nvoid main() {\nstrlen(0);\n}\n', 'mw8080bw', done, 8192, sdccLines(2, 3), 0);
|
||||
});
|
||||
it('should compile oscar64 and return listings/symbols/segments', async function() {
|
||||
var msgs = [{code:'#include <stdio.h>\nint main() { printf("FOO"); return 0; }', platform:'c64', tool:'oscar64', path:'main.c', mainfile:true}];
|
||||
@@ -276,23 +311,23 @@ describe('Worker', function() {
|
||||
|
||||
it('should compile mw8080 skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/mw8080bw/skeleton.sdcc'));
|
||||
compile('sdcc', csource, 'mw8080bw', done, 8192, 84, 0);
|
||||
compile('sdcc', csource, 'mw8080bw', done, 8192, sdccLines(84, 97), 0);
|
||||
});
|
||||
it('should compile galaxian skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/galaxian-scramble/skeleton.sdcc'));
|
||||
compile('sdcc', csource, 'galaxian-scramble', done, 20512, 28, 0);
|
||||
compile('sdcc', csource, 'galaxian-scramble', done, 20512, sdccLines(28, 36), 0);
|
||||
});
|
||||
it('should compile vector skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/vector-z80color/skeleton.sdcc'));
|
||||
compile('sdcc', csource, 'vector-z80color', done, 32768, 23, 0);
|
||||
compile('sdcc', csource, 'vector-z80color', done, 32768, sdccLines(23, 26), 0);
|
||||
});
|
||||
it('should compile williams skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/williams-z80/skeleton.sdcc'));
|
||||
compile('sdcc', csource, 'williams-z80', done, 38912, 40, 0);
|
||||
compile('sdcc', csource, 'williams-z80', done, 38912, sdccLines(40, 43), 0);
|
||||
});
|
||||
it('should compile williams_sound skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/sound_williams-z80/skeleton.sdcc'));
|
||||
compile('sdcc', csource, 'sound_williams-z80', done, 16384, 6, 0);
|
||||
compile('sdcc', csource, 'sound_williams-z80', done, 16384, sdccLines(6, 7), 0);
|
||||
});
|
||||
it('should compile coleco skeleton', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/coleco/cursorsmooth.c'));
|
||||
@@ -370,6 +405,29 @@ describe('Worker', function() {
|
||||
var msgs = [m, m, m2];
|
||||
doBuild(msgs, done, 8192, [1,1], 0);
|
||||
});
|
||||
it('should rebuild unchanged files when //#tooldef c sdcc changes', async function() {
|
||||
var build = (main) => ({
|
||||
"updates":[
|
||||
{"path":"main.c", "data":main + "extern int mul2(int x);\nint main() { return mul2(2); }\n"},
|
||||
{"path":"fn.c", "data":"int mul2(int x) { return x*x; }\n"}
|
||||
],
|
||||
"buildsteps":[
|
||||
{"path":"main.c", "platform":"mw8080bw", "tool":"sdcc"},
|
||||
{"path":"fn.c", "platform":"mw8080bw", "tool":"sdcc"}
|
||||
]
|
||||
});
|
||||
// fn.c has no directive, and doesn't change: its asm banner shows which SDCC built it
|
||||
var fnVersion = async (main) => {
|
||||
var result;
|
||||
global.postMessage = (msg) => { result = msg; };
|
||||
await global.onmessage({data:build(main)});
|
||||
assert.ok(!result.errors || !result.errors.length, JSON.stringify(result.errors));
|
||||
return /; Version (\d+)\./.exec(result.listings['fn.lst'].text)[1];
|
||||
};
|
||||
await global.onmessage({data:{reset:true}});
|
||||
assert.equal(await fnVersion("//#tooldef c sdcc=4\n"), '4');
|
||||
assert.equal(await fnVersion("//#tooldef c sdcc=3\n"), '3');
|
||||
});
|
||||
it('should include filename in compile errors', function(done) {
|
||||
var m = {
|
||||
"updates":[
|
||||
@@ -387,7 +445,7 @@ describe('Worker', function() {
|
||||
});
|
||||
it('should compile vicdual skeleton', function(done) {
|
||||
var files = ['skeleton.sdcc', 'cp437.c'];
|
||||
compileFiles('sdcc', files, 'vicdual', done, 16416, [0,45], 0); // TODO?
|
||||
compileFiles('sdcc', files, 'vicdual', done, 16416, [0, sdccLines(45, 55)], 0); // TODO?
|
||||
});
|
||||
it('should compile apple2 skeleton with CC65', function(done) {
|
||||
var csource = ab2str(fs.readFileSync('presets/apple2/skeleton.cc65'));
|
||||
|
||||
@@ -248,6 +248,15 @@ describe("explicit build directives (//#symbol, //#flag, //#tooldef)", function
|
||||
assert.deepStrictEqual(params.libargs, ["a.lib", "b.lib"]);
|
||||
});
|
||||
|
||||
it("#tooldef c sdcc picks the SDCC version", function () {
|
||||
const params: any = {};
|
||||
applyDirectives("//#tooldef c sdcc=3\n", params);
|
||||
assert.strictEqual(params.sdcc_version, 3);
|
||||
const dir = applyDirectives("//#tooldef c sdcc=5\n", params);
|
||||
assert.strictEqual(dir.errors.length, 1);
|
||||
assert.strictEqual(params.sdcc_version, 3);
|
||||
});
|
||||
|
||||
it("#tooldef rejects unknown knobs", function () {
|
||||
const params = c64Params();
|
||||
const dir = applyDirectives("//#tooldef ld nonsense=1\n", params);
|
||||
@@ -298,7 +307,8 @@ describe("platform tool config resolution", function () {
|
||||
it("resolves per-platform, root-base, and default configs", function () {
|
||||
assert.strictEqual(getPlatformToolConfig('cc65', 'nes').wasiFSZip, 'cc65-fs-nes.zip');
|
||||
assert.strictEqual(getPlatformToolConfig('cc65', 'atari8-800').wasiFSZip, 'cc65-fs-atari8.zip');
|
||||
assert.strictEqual(getPlatformToolConfig('sdcc').preloadFS, 'sdcc');
|
||||
assert.strictEqual(getPlatformToolConfig('sdcc').wasiFSZip, 'sdcc-fs.zip');
|
||||
assert.strictEqual(getPlatformToolConfig('sdcc', 'sms-sms-libcv').preloadFS, 'sdcc'); // root base 'sms'
|
||||
assert.strictEqual(getPlatformToolConfig('dasm', 'nes'), undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import { parseRSTListing } from "../../src/worker/tools/sdcc";
|
||||
|
||||
// linked listings of `clrscr();` at skeleton.sdcc line 172, then a call from
|
||||
// a header the file includes
|
||||
const RST_SDCC3 = [
|
||||
" 1274 _main::",
|
||||
" 1275 ;<stdin>:172: clrscr();",
|
||||
" 056C CD 3C 00 [17] 1276 \tcall\t_clrscr",
|
||||
].join("\n");
|
||||
|
||||
const RST_SDCC4 = [
|
||||
" 0000056C 1274 _main::",
|
||||
" 1275 ;//skeleton.sdcc:172: clrscr();",
|
||||
" 0000056C CD 3C 00 [17] 1276 \tcall\t_clrscr",
|
||||
" 1277 ;//defs.h:9: inline_fn();",
|
||||
" 0000056F CD 40 00 [17] 1278 \tcall\t_inline_fn",
|
||||
].join("\n");
|
||||
|
||||
describe("sdcc listings", function () {
|
||||
|
||||
it("maps SDCC 3.6.5 <stdin> source comments", function () {
|
||||
const l = parseRSTListing(RST_SDCC3, "skeleton");
|
||||
assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]);
|
||||
assert.strictEqual(l.asmlines[0].offset, 0x56c);
|
||||
});
|
||||
|
||||
it("maps SDCC 4.x file source comments with 8-digit addresses", function () {
|
||||
const l = parseRSTListing(RST_SDCC4, "skeleton");
|
||||
// the defs.h line is not attributed to skeleton.sdcc
|
||||
assert.deepStrictEqual(l.lines.map((x) => [x.line, x.offset]), [[172, 0x56c]]);
|
||||
assert.deepStrictEqual(l.asmlines.map((x) => x.offset), [0x56c, 0x56f]);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -51,7 +51,12 @@ describe('Tool metadata registry', function () {
|
||||
assert.strictEqual(getSharedFileSystemName('cc65', 'nes'), 'wasi:cc65-fs-nes.zip');
|
||||
assert.strictEqual(getSharedFileSystemName('cc65', 'c64'), 'wasi:cc65-fs-c64.zip');
|
||||
assert.strictEqual(getSharedFileSystemName('cc65', 'nonexistent'), undefined);
|
||||
assert.strictEqual(getPreloadFSName('sdcc'), 'sdcc'); // default config
|
||||
// SDCC 4.x (WASI zip) by default; 3.x-library platforms keep the 3.6.5 preloadFS
|
||||
assert.strictEqual(getPreloadFSName('sdcc'), undefined);
|
||||
assert.strictEqual(getSharedFileSystemName('sdcc', 'mw8080bw'), 'wasi:sdcc-fs.zip');
|
||||
assert.strictEqual(getPreloadFSName('sdcc', 'gb'), 'sdcc');
|
||||
assert.strictEqual(getPreloadFSName('sdcc', 'msx-libcv'), 'sdcc');
|
||||
assert.strictEqual(getSharedFileSystemName('sdcc', 'msx'), 'wasi:sdcc-fs.zip');
|
||||
assert.strictEqual(getPreloadFSName('dasm'), undefined);
|
||||
});
|
||||
|
||||
|
||||
@@ -192,6 +192,17 @@ describe('WASIMemoryFilesystem path handling', function () {
|
||||
assert.strictEqual(fsys.removeDirectory('./sub'), WASIErrors.SUCCESS);
|
||||
assert.strictEqual(fsys.removeDirectory('/'), WASIErrors.BUSY);
|
||||
});
|
||||
it('renames files, replacing the target', function () {
|
||||
const fsys = new WASIMemoryFilesystem();
|
||||
fsys.putFile('./a.tmp', 'new');
|
||||
fsys.putFile('./a.s', 'old');
|
||||
fsys.putDirectory('./sub');
|
||||
assert.strictEqual(fsys.renameFile('./a.tmp', './a.s'), WASIErrors.SUCCESS);
|
||||
assert.strictEqual(fsys.getFile('./a.s').getBytesAsString(), 'new');
|
||||
assert.strictEqual(fsys.getFile('./a.tmp'), undefined);
|
||||
assert.strictEqual(fsys.renameFile('./a.tmp', './b.s'), WASIErrors.NOENT);
|
||||
assert.strictEqual(fsys.renameFile('./a.s', './sub'), WASIErrors.ISDIR);
|
||||
});
|
||||
it('deletes only from the writable layer, not the parent', function () {
|
||||
const parent = new WASIMemoryFilesystem();
|
||||
parent.putFile('./shared.h', 'shared');
|
||||
|
||||
Reference in new issue
Block a user