Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/sdk-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,19 @@ wasm32posix-cc -shared -fPIC plugin.c -o plugin.so
# (removed in commit 9 of the fork-instrument mega-PR, 2026-05-14).
-fno-trapping-math # Non-trapping FP (Wasm requirement)
--sysroot=<path> # musl sysroot
-ffile-prefix-map=<glue>=/usr/src/kandelo-sdk/libc/glue
-fdebug-prefix-map=<glue>=/usr/src/kandelo-sdk/libc/glue
-fmacro-prefix-map=<glue>=/usr/src/kandelo-sdk/libc/glue
# The same three maps use /usr/src/kandelo-sdk/sysroot for the wasm32
# <sysroot>, or /usr/src/kandelo-sdk/sysroot64 for the wasm64 <sysroot>.
```

The file, debug, and macro prefix maps cover paths owned and injected by the
SDK. Linked Wasm debug information and `__FILE__` strings therefore do not
depend on the checkout containing `libc/glue` or the target sysroot. Build
systems remain responsible for mapping their own source trees and explicit
dependency prefixes when those paths must also be reproducible.

The musl objects in the SDK sysroot are compiled with the same Wasm exception
handling and SjLj lowering flags, so libc calls to `setjmp`/`longjmp` do not
leave unresolved host imports in linked programs.
Expand Down
26 changes: 26 additions & 0 deletions sdk/src/bin/cc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ import { runPassthrough } from '../lib/exec.ts';
import { isMain } from '../lib/is-main.ts';
import { type WasmArch, detectArch, targetTriple } from '../lib/arch.ts';

const STABLE_SDK_SOURCE_ROOT = '/usr/src/kandelo-sdk';

function sourcePrefixMapFlags(source: string, destination: string): string[] {
return [
`-ffile-prefix-map=${source}=${destination}`,
`-fdebug-prefix-map=${source}=${destination}`,
`-fmacro-prefix-map=${source}=${destination}`,
];
}

function sdkSourcePrefixMapFlags(toolchain: Toolchain, arch: WasmArch): string[] {
const sysrootName = arch === 'wasm64' ? 'sysroot64' : 'sysroot';
return [
...sourcePrefixMapFlags(toolchain.glueDir, `${STABLE_SDK_SOURCE_ROOT}/libc/glue`),
...sourcePrefixMapFlags(toolchain.sysroot, `${STABLE_SDK_SOURCE_ROOT}/${sysrootName}`),
];
}

export function buildClangArgs(userArgs: string[], toolchain: Toolchain, arch: WasmArch = 'wasm32'): string[] {
const { filtered, warnings } = filterArgs(userArgs, arch);
for (const w of warnings) console.error(w);
Expand Down Expand Up @@ -45,6 +63,14 @@ export function buildClangArgs(userArgs: string[], toolchain: Toolchain, arch: W
if (parsed.outputFile) args.push('-o', parsed.outputFile);
args.push(...parsed.otherArgs);

// The SDK compiles its glue sources during each executable link. Keep those
// files and sysroot headers independent of the checkout used for the build.
// Append these after caller flags so a broader caller-owned mapping cannot
// retain a less-specific host path in DWARF.
if (hasSourceFiles || parsed.compileOnly || parsed.preprocessOnly || parsed.assemblyOnly || linking) {
args.push(...sdkSourcePrefixMapFlags(toolchain, arch));
}

args.push(...parsed.sourceFiles);
args.push(...parsed.objectFiles);
args.push(...parsed.archiveFiles);
Expand Down
20 changes: 20 additions & 0 deletions sdk/test/cc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,26 @@ describe('buildClangArgs', () => {
expect(args.join(' ')).toContain('channel_syscall.c');
});

it('maps SDK-owned glue and sysroot paths to stable debug identities', () => {
const args = buildClangArgs(
['-ffile-prefix-map=/tmp=/caller-source', 'foo.c', '-o', 'foo.wasm'],
toolchain,
);

for (const kind of ['file', 'debug', 'macro']) {
expect(args).toContain(`-f${kind}-prefix-map=/tmp/glue=/usr/src/kandelo-sdk/libc/glue`);
expect(args).toContain(`-f${kind}-prefix-map=/tmp/sysroot=/usr/src/kandelo-sdk/sysroot`);
}
expect(args.indexOf('-ffile-prefix-map=/tmp/glue=/usr/src/kandelo-sdk/libc/glue'))
.toBeGreaterThan(args.indexOf('-ffile-prefix-map=/tmp=/caller-source'));
});

it('uses an architecture-specific stable identity for the wasm64 sysroot', () => {
const args = buildClangArgs(['-c', 'foo.c', '-o', 'foo.o'], toolchain, 'wasm64');

expect(args).toContain('-ffile-prefix-map=/tmp/sysroot=/usr/src/kandelo-sdk/sysroot64');
});

it('preprocess-only: no link flags', () => {
const args = buildClangArgs(['-E', 'foo.c'], toolchain);
expect(args).not.toContain('-Wl,--entry=_start');
Expand Down
58 changes: 58 additions & 0 deletions sdk/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,64 @@ describe('integration: compile C program', () => {
try { unlinkSync(outFile); } catch {}
}, 30_000);

it('keeps SDK checkout paths out of linked debug information', async () => {
const toolchain = await resolveToolchain();
mkdirSync(TMP_DIR, { recursive: true });

const srcFile = join(TMP_DIR, 'debug-paths.c');
const outFile = join(TMP_DIR, 'debug-paths.wasm');
writeFileSync(srcFile, '#include <stdio.h>\nint main(void) { return puts("debug paths"); }\n');

try {
const userArgs = ['-g', srcFile, '-o', outFile];
await prepareExecutableLinker(userArgs, toolchain);
const args = buildClangArgs(userArgs, toolchain);
const result = await run(toolchain.cc, args);
if (result.exitCode !== 0) {
console.error('clang stderr:', result.stderr);
}
expect(result.exitCode).toBe(0);

const artifact = readFileSync(outFile);
expect(artifact.includes(Buffer.from(toolchain.glueDir))).toBe(false);
expect(artifact.includes(Buffer.from(toolchain.sysroot))).toBe(false);
expect(artifact.includes(Buffer.from('/usr/src/kandelo-sdk/libc/glue'))).toBe(true);
expect(artifact.includes(Buffer.from('/usr/src/kandelo-sdk/sysroot'))).toBe(true);
} finally {
try { unlinkSync(srcFile); } catch {}
try { unlinkSync(outFile); } catch {}
}
}, 30_000);

it('maps sysroot paths in compile-only debug information', async () => {
const toolchain = await resolveToolchain();
mkdirSync(TMP_DIR, { recursive: true });

const srcFile = join(TMP_DIR, 'compile-debug-paths.c');
const outFile = join(TMP_DIR, 'compile-debug-paths.o');
const syntheticHeader = join(toolchain.sysroot, 'include', 'debug-paths.h');
writeFileSync(
srcFile,
`#line 1 ${JSON.stringify(syntheticHeader)}\nconst char *debug_path = __FILE__;\n`,
);

try {
const args = buildClangArgs(['-g', '-c', srcFile, '-o', outFile], toolchain);
const result = await run(toolchain.cc, args);
if (result.exitCode !== 0) {
console.error('clang stderr:', result.stderr);
}
expect(result.exitCode).toBe(0);

const artifact = readFileSync(outFile);
expect(artifact.includes(Buffer.from(toolchain.sysroot))).toBe(false);
expect(artifact.includes(Buffer.from('/usr/src/kandelo-sdk/sysroot/include/debug-paths.h'))).toBe(true);
} finally {
try { unlinkSync(srcFile); } catch {}
try { unlinkSync(outFile); } catch {}
}
}, 30_000);

it('links timer_create without fictional raw setjmp imports', async () => {
const toolchain = await resolveToolchain();
mkdirSync(TMP_DIR, { recursive: true });
Expand Down
Loading