Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Real, unmodified software compiled to WebAssembly:
| Vim | 9.1 | Full editor with ncurses terminal UI |
| NetHack | 3.6.7 | Classic roguelike with curses UI |
| fbDOOM | (maximevince) | id Software's DOOM via the kernel's `/dev/fb0` Linux fbdev surface |
| espeak-ng | 1.52 | Speech synthesis; plays through upstream pcaudiolib's OSS backend on `/dev/dsp` |
| Perl | 5.40 | Interpreter with core modules |
| Ruby | 3.3 | Interpreter with core stdlib |
| SpiderMonkey | 140 ESR | JavaScript engine backing the Node.js-compatible runtime with Intl, SharedArrayBuffer, worker_threads, and npm package installs. |
Expand Down Expand Up @@ -349,6 +350,7 @@ bash packages/registry/nano/build-nano.sh # GNU nano 8.3
bash packages/registry/curl/build-curl.sh # curl
bash packages/registry/netcat/build-netcat.sh # GNU Netcat 0.7.1
bash packages/registry/make/build-make.sh # GNU make
bash packages/registry/espeak-ng/build-espeak-ng.sh # espeak-ng 1.52
```

See [docs/porting-guide.md](docs/porting-guide.md) for how to port your own software.
Expand Down
57 changes: 57 additions & 0 deletions abi/snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,12 @@
}
},
"ioctl_request_contracts": {
"1074021776": {
"argKind": "scalar-i32",
"direction": "none",
"wasm32Size": 0,
"wasm64Size": 0
},
"1074024452": {
"argKind": "pointer",
"direction": "in",
Expand Down Expand Up @@ -1326,6 +1332,12 @@
"wasm32Size": 0,
"wasm64Size": 0
},
"2147763457": {
"argKind": "pointer",
"direction": "out",
"wasm32Size": 4,
"wasm64Size": 4
},
"2147766274": {
"argKind": "pointer",
"direction": "out",
Expand Down Expand Up @@ -1380,6 +1392,12 @@
"wasm32Size": 4,
"wasm64Size": 4
},
"2148025602": {
"argKind": "pointer",
"direction": "out",
"wasm32Size": 8,
"wasm64Size": 8
},
"2148028435": {
"argKind": "pointer",
"direction": "out",
Expand Down Expand Up @@ -1759,6 +1777,35 @@
"wasm64Size": null
}
},
"ioctl_request_families": [
{
"dir": 2,
"direction": "out",
"fixedSize": null,
"magic": 69,
"maxCallerSize": 256,
"nrFirst": 6,
"nrLast": 6
},
{
"dir": 2,
"direction": "out",
"fixedSize": null,
"magic": 69,
"maxCallerSize": 256,
"nrFirst": 32,
"nrLast": 63
},
{
"dir": 2,
"direction": "out",
"fixedSize": 24,
"magic": 69,
"maxCallerSize": null,
"nrFirst": 64,
"nrLast": 127
}
],
"kernel_exports": [
{
"kind": "func",
Expand Down Expand Up @@ -2405,6 +2452,11 @@
"name": "kernel_inject_mouse_event",
"signature": "(i32,i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_input_event",
"signature": "(i32,i32,i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_ioctl",
Expand Down Expand Up @@ -2970,6 +3022,11 @@
"name": "kernel_set_fork_fd_action",
"signature": "(i32,i32,i32) -> (i32)"
},
{
"kind": "func",
"name": "kernel_set_input_canvas_dims",
"signature": "(i32,i32) -> ()"
},
{
"kind": "func",
"name": "kernel_set_max_addr",
Expand Down
162 changes: 161 additions & 1 deletion apps/browser-demos/pages/kandelo/kernel-host/live-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
bindImageOwnedRuntimeUrls,
type ImageOwnedRuntimeLazyAssets,
} from "../../../lib/init/image-owned-runtime-urls";
import { BrowserInputSource } from "../../../../../host/src/input/browser-input-source";
import {
WORDPRESS_CONFIG_INIT_SCRIPT,
WORDPRESS_URL_MU_PLUGIN,
Expand All @@ -23,6 +24,10 @@ import {
WORDPRESS_MARIADB_SOCKET_PATH,
} from "../../../lib/init/wordpress-mariadb-readiness";
import { MemoryFileSystem } from "../../../../../host/src/vfs/memory-fs";
import {
extractZipEntry,
parseZipCentralDirectory,
} from "../../../../../host/src/vfs/zip";
import {
resolveBrowserCorsProxyConfig,
} from "../../../lib/browser-cors-proxy";
Expand Down Expand Up @@ -170,6 +175,26 @@ const OPTIONAL_BINARY_URLS = {
import: "default",
},
),
...import.meta.glob("../../../../../local-binaries/programs/wasm32/evdev_demo.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/evdev_demo.wasm", {
query: "?url", import: "default",
}),
// espeak-ng publishes a wasm output plus a runtime file, so the resolver
// mirrors its whole closure under the package directory.
...import.meta.glob("../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng.wasm", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip", {
query: "?url", import: "default",
}),
...import.meta.glob("../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip", {
query: "?url", import: "default",
}),
} as Record<string, () => Promise<string>>;

async function optionalBinaryUrl(
Expand All @@ -180,7 +205,11 @@ async function optionalBinaryUrl(
const loader = OPTIONAL_BINARY_URLS[relPath];
if (loader) return loader();
}
throw new Error(`${label} is not built. Run: ./run.sh build programs`);
throw new Error(
`${label} is not built. Run: ./run.sh build programs, ` +
`or for package-owned binaries: ` +
`cargo xtask build-deps resolve <package>`,
);
}

const HTTP_PORT = 8080;
Expand Down Expand Up @@ -303,6 +332,8 @@ const LIVE_DEMO_IDS = [
"wordpress-mariadb",
"doom",
"modeset",
"evdev",
"espeak",
] as const;

type LiveDemoId = (typeof LIVE_DEMO_IDS)[number];
Expand Down Expand Up @@ -401,6 +432,12 @@ const LIVE_DEMO_SPECS: Record<LiveDemoId, LiveDemoSpec> = {
image: "shell",
features: ["kms"],
},
evdev: {
image: "shell",
},
espeak: {
image: "shell",
},
};

const DEFAULT_DEMO_FOR_VFS_IMAGE: Record<LiveVfsImage, LiveDemoId> = {
Expand Down Expand Up @@ -458,6 +495,22 @@ interface LiveProfile {
};
};
framebufferTest: boolean;
/**
* Stage `evdev_demo` into `/usr/local/bin`, attach a `BrowserInputSource`
* to the window so keyboard/pointer events flow into the kernel's
* `/dev/input/event{0,1}`, and run the binary from bash so its event
* log streams to the user's Shell pane.
*/
evdevDemo: boolean;
/**
* Spawn `espeak-ng "..."` from the booted shell. espeak-ng links
* upstream pcaudiolib built with only its OSS backend, so
* `create_audio_device_object` falls through to `/dev/dsp` and a
* single binary invocation produces audible synthesised speech
* without any host-side pipeline. The binary + data dir are baked
* into the image via `stageEspeakRuntime`.
*/
espeakDemo: boolean;
}

interface WebReadinessState {
Expand Down Expand Up @@ -896,6 +949,8 @@ function customVfsProfile(
shell: "default",
maxVfsByteLength: CUSTOM_VFS_PROFILE_MAX_BYTES,
framebufferTest: fb === "test",
evdevDemo: false,
espeakDemo: false,
};
}

Expand Down Expand Up @@ -944,6 +999,8 @@ function profileFor(id: string, fb?: FbDemo): LiveProfile {
},
},
framebufferTest: fb === "test",
evdevDemo: normalized === "evdev",
espeakDemo: normalized === "espeak",
};
}

Expand Down Expand Up @@ -1220,6 +1277,18 @@ async function bootProfile(
ensureDirRecursive(buildFs, dirname(profile.init.argv[0]));
writeVfsBinary(buildFs, profile.init.argv[0], new Uint8Array(bytes), 0o755);
}
// Both demos run their binary from a path, so the bytes have to be in the
// image before the worker takes exclusive ownership of the VFS.
if (profile.espeakDemo) {
tick("staging espeak-ng...");
await stageEspeakRuntime(buildFs);
assertCurrent();
}
if (profile.evdevDemo) {
tick("staging evdev_demo...");
await stageEvdevDemo(buildFs);
assertCurrent();
}
ensureDemoHomes(buildFs);
}
assertImageTerminalProgram(buildFs, terminalSession.initial);
Expand Down Expand Up @@ -1452,6 +1521,49 @@ async function bootProfile(
tick,
assertCurrent,
);
} else if (profile.espeakDemo) {
// The binary and its voice data are already in the image; see
// stageEspeakRuntime. Playback rides the /dev/dsp path every other
// sound demo uses.
void (async () => {
try {
tick("running espeak-ng...");
await host.runShellCommand(
`/usr/bin/espeak-ng "Welcome to Kandelo, the WebAssembly POSIX kernel"`,
);
tick("espeak-ng exited");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
tick(`espeak-ng failed: ${msg}`);
}
})();
} else if (profile.evdevDemo) {
// autoCommand can't run this: the InputSource must be attached before
// the binary starts polling /dev/input/event{0,1}. The binary itself is
// already in the image; see stageEvdevDemo.
const kernelForEvdev = kernel;
void (async () => {
try {
tick("attaching input source...");
kernelForEvdev.attachInputSource(new BrowserInputSource(window), {
width: window.innerWidth,
height: window.innerHeight,
});
tick("running evdev_demo...");
// evdev_demo runs forever; runShellCommand resolves when the
// bash prompt reappears (it never will) or rejects after its
// internal 5-minute timeout. Both are expected — log neutrally.
await host.runShellCommand("/usr/local/bin/evdev_demo");
tick("evdev_demo exited");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (/timed out waiting for PTY prompt/.test(msg)) {
tick("evdev_demo running (long-tail; no further status updates)");
} else {
tick(`evdev_demo failed: ${msg}`);
}
}
})();
} else if (presentation?.autoCommand) {
tick("starting configured command from the default shell...");
void host.runShellCommand(presentation.autoCommand).catch((err) => {
Expand Down Expand Up @@ -1527,6 +1639,54 @@ function stageShellUtilities(
}
}

/**
* Bake espeak-ng and its voice data into the image.
*
* Both come from the espeak-ng package closure, so the demo consumes the same
* bytes the resolver published. libespeak-ng's PATH_ESPEAK_DATA is fixed to
* /usr/share at build time, so the data tree has to land unpacked there.
*/
async function stageEspeakRuntime(fs: MemoryFileSystem): Promise<void> {
const binaryUrl = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng.wasm",
"../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng.wasm",
], "espeak-ng.wasm");
const binary = await fetch(binaryUrl)
.then(failOn("espeak-ng.wasm"))
.then((r) => r.arrayBuffer());
ensureDirRecursive(fs, "/usr/bin");
writeVfsBinary(fs, "/usr/bin/espeak-ng", new Uint8Array(binary), 0o755);

const dataUrl = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip",
"../../../../../binaries/programs/wasm32/espeak-ng/espeak-ng-data.zip",
], "espeak-ng-data.zip");
const data = await fetch(dataUrl)
.then(failOn("espeak-ng-data.zip"))
.then((r) => r.arrayBuffer());
const zipBytes = new Uint8Array(data);
const root = "/usr/share/espeak-ng-data";
ensureDirRecursive(fs, root);
for (const entry of parseZipCentralDirectory(zipBytes)) {
if (entry.isDirectory) continue;
const target = `${root}/${entry.fileName}`;
ensureDirRecursive(fs, target.slice(0, target.lastIndexOf("/")));
writeVfsBinary(fs, target, extractZipEntry(zipBytes, entry), 0o644);
}
}

async function stageEvdevDemo(fs: MemoryFileSystem): Promise<void> {
const url = await optionalBinaryUrl([
"../../../../../local-binaries/programs/wasm32/evdev_demo.wasm",
"../../../../../binaries/programs/wasm32/evdev_demo.wasm",
], "evdev_demo.wasm");
const bytes = await fetch(url)
.then(failOn("evdev_demo.wasm"))
.then((r) => r.arrayBuffer());
ensureDirRecursive(fs, "/usr/local/bin");
writeVfsBinary(fs, "/usr/local/bin/evdev_demo", new Uint8Array(bytes), 0o755);
}

function ensureDemoHomes(fs: MemoryFileSystem): void {
ensureDirRecursive(fs, "/home");
ensureOwnedDir(fs, DEMO_HOME, 0o755, DEMO_UID, DEMO_GID);
Expand Down
22 changes: 22 additions & 0 deletions apps/browser-demos/pages/kandelo/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,26 @@ export const PRESET_LIBRARY: Preset[] = [
bootCommand: ["/usr/local/bin/modeset"],
estimatedUrlBytes: 612,
},
{
id: "evdev",
title: "Evdev input log",
summary: "Keystrokes + pointer motion captured from the DOM and replayed through /dev/input/event{0,1}.",
base: SHELL_BASE,
packages: ["bash@local", "coreutils@local"],
accent: "#7e57c2",
glyph: "E",
bootCommand: ["bash", "-l", "-i"],
estimatedUrlBytes: 612,
},
{
id: "espeak",
title: "OSS - Espeak-NG",
summary: "The kernel speaks: espeak-ng synthesises text directly through libpcaudio's OSS backend.",
base: SHELL_BASE,
packages: ["bash@local", "coreutils@local"],
accent: "#f48fb1",
glyph: "T",
bootCommand: ["bash", "-l", "-i"],
estimatedUrlBytes: 612,
},
];
Loading