Skip to content

Commit 77925e1

Browse files
committed
Add WebAssembly build infrastructure
This enables Emscripten/WebAssembly compilation with complete toolchain integration and browser-based testing workflow. The build system now automatically detects Emscripten and configures appropriate compiler flags, backend selection, and post-build artifact management. Run './scripts/serve-wasm.py --open' to test in browser.
1 parent 9934440 commit 77925e1

File tree

7 files changed

+733
-37
lines changed

7 files changed

+733
-37
lines changed

Makefile

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ ifeq ($(filter $(check_goal),config defconfig),)
1010
endif
1111
endif
1212

13+
# Detect Emscripten early (before including toolchain.mk)
14+
CC_VERSION := $(shell $(CC) --version 2>/dev/null)
15+
ifneq ($(findstring emcc,$(CC_VERSION)),)
16+
CC_IS_EMCC := 1
17+
endif
18+
19+
# Enforce SDL backend for Emscripten builds (skip during config targets)
20+
ifeq ($(filter $(check_goal),config defconfig),)
21+
ifeq ($(CC_IS_EMCC),1)
22+
ifneq ($(CONFIG_BACKEND_SDL),y)
23+
$(error Emscripten (WebAssembly) builds require SDL backend. Please run: env CC=emcc make defconfig)
24+
endif
25+
endif
26+
endif
27+
1328
# Target variables initialization
1429

1530
target-y :=
@@ -23,6 +38,8 @@ target.a-y += libtwin.a
2338
# Core library
2439

2540
libtwin.a_cflags-y :=
41+
# Emscripten size optimization
42+
libtwin.a_cflags-$(CC_IS_EMCC) += -Oz
2643

2744
libtwin.a_files-y = \
2845
src/box.c \
@@ -85,14 +102,26 @@ endif
85102

86103
ifeq ($(CONFIG_LOADER_JPEG), y)
87104
libtwin.a_files-y += src/image-jpeg.c
105+
ifneq ($(CC_IS_EMCC), 1)
88106
libtwin.a_cflags-y += $(shell pkg-config --cflags libjpeg)
89107
TARGET_LIBS += $(shell pkg-config --libs libjpeg)
108+
else
109+
# Emscripten libjpeg port - flags needed for both compile and link
110+
libtwin.a_cflags-y += -sUSE_LIBJPEG=1
111+
TARGET_LIBS += -sUSE_LIBJPEG=1
112+
endif
90113
endif
91114

92115
ifeq ($(CONFIG_LOADER_PNG), y)
93116
libtwin.a_files-y += src/image-png.c
117+
ifneq ($(CC_IS_EMCC), 1)
94118
libtwin.a_cflags-y += $(shell pkg-config --cflags libpng)
95119
TARGET_LIBS += $(shell pkg-config --libs libpng)
120+
else
121+
# Emscripten libpng port (includes zlib) - flags needed for both compile and link
122+
libtwin.a_cflags-y += -sUSE_LIBPNG=1 -sUSE_ZLIB=1
123+
TARGET_LIBS += -sUSE_LIBPNG=1 -sUSE_ZLIB=1
124+
endif
96125
endif
97126

98127
ifeq ($(CONFIG_LOADER_GIF), y)
@@ -116,6 +145,8 @@ libapps.a_files-$(CONFIG_DEMO_ANIMATION) += apps/animation.c
116145
libapps.a_files-$(CONFIG_DEMO_IMAGE) += apps/image.c
117146

118147
libapps.a_includes-y := include
148+
# Emscripten size optimization
149+
libapps.a_cflags-$(CC_IS_EMCC) += -Oz
119150

120151
# Graphical backends
121152

@@ -124,8 +155,15 @@ BACKEND := none
124155
ifeq ($(CONFIG_BACKEND_SDL), y)
125156
BACKEND = sdl
126157
libtwin.a_files-y += backend/sdl.c
158+
# Emscripten uses ports system for SDL2
159+
ifneq ($(CC_IS_EMCC), 1)
127160
libtwin.a_cflags-y += $(shell sdl2-config --cflags)
128161
TARGET_LIBS += $(shell sdl2-config --libs)
162+
else
163+
# Emscripten SDL2 port - flags needed for both compile and link
164+
libtwin.a_cflags-y += -sUSE_SDL=2
165+
TARGET_LIBS += -sUSE_SDL=2
166+
endif
129167
endif
130168

131169
ifeq ($(CONFIG_BACKEND_FBDEV), y)
@@ -149,7 +187,6 @@ libtwin.a_files-y += backend/headless.c
149187
endif
150188

151189
# Performance tester
152-
153190
ifeq ($(CONFIG_PERF_TEST), y)
154191
target-$(CONFIG_PERF_TEST) += mado-perf
155192
mado-perf_depends-y += $(target.a-y)
@@ -167,21 +204,34 @@ target-y += demo-$(BACKEND)
167204
demo-$(BACKEND)_depends-y += $(target.a-y)
168205
demo-$(BACKEND)_files-y = apps/main.c
169206
demo-$(BACKEND)_includes-y := include
207+
# Emscripten size optimization
208+
demo-$(BACKEND)_cflags-$(CC_IS_EMCC) += -Oz
170209
demo-$(BACKEND)_ldflags-y := \
171210
$(target.a-y) \
172211
$(TARGET_LIBS)
173212

174-
# Emscripten-specific linker flags to avoid "section too large" errors
213+
# Emscripten-specific linker flags for WebAssembly builds
175214
ifeq ($(CC_IS_EMCC), 1)
176215
demo-$(BACKEND)_ldflags-y += \
177216
-sINITIAL_MEMORY=33554432 \
178217
-sALLOW_MEMORY_GROWTH=1 \
179-
-sSTACK_SIZE=1048576
218+
-sSTACK_SIZE=1048576 \
219+
-sUSE_SDL=2 \
220+
-sMINIMAL_RUNTIME=0 \
221+
-sDYNAMIC_EXECUTION=0 \
222+
-sASSERTIONS=0 \
223+
-sEXPORTED_FUNCTIONS=_main,_malloc,_free \
224+
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap \
225+
-sNO_EXIT_RUNTIME=1 \
226+
-Oz \
227+
--preload-file assets \
228+
--exclude-file assets/web
180229
endif
181230
endif
182231

183232
# Font editor tool
184-
233+
# Tools should not be built for WebAssembly
234+
ifneq ($(CC_IS_EMCC), 1)
185235
ifeq ($(CONFIG_TOOLS), y)
186236
target-$(CONFIG_TOOL_FONTEDIT) += font-edit
187237
font-edit_files-y = \
@@ -202,6 +252,7 @@ headless-ctl_files-y = tools/headless-ctl.c
202252
headless-ctl_includes-y := include
203253
headless-ctl_ldflags-y := # -lrt
204254
endif
255+
endif
205256

206257
# Build system integration
207258

@@ -245,3 +296,26 @@ defconfig: $(KCONFIGLIB)
245296
config: $(KCONFIGLIB) configs/Kconfig
246297
@tools/kconfig/menuconfig.py configs/Kconfig
247298
@tools/kconfig/genconfig.py configs/Kconfig
299+
300+
# WebAssembly post-build: Copy artifacts to assets/web/
301+
.PHONY: wasm-install
302+
wasm-install:
303+
@if [ "$(CC_IS_EMCC)" = "1" ]; then \
304+
echo "Installing WebAssembly artifacts to assets/web/..."; \
305+
mkdir -p assets/web; \
306+
cp -f .demo-$(BACKEND)/demo-$(BACKEND) .demo-$(BACKEND)/demo-$(BACKEND).wasm .demo-$(BACKEND)/demo-$(BACKEND).data assets/web/ 2>/dev/null || true; \
307+
echo "✓ WebAssembly build artifacts copied to assets/web/"; \
308+
echo ""; \
309+
echo "\033[1;32m========================================\033[0m"; \
310+
echo "\033[1;32mWebAssembly build complete!\033[0m"; \
311+
echo "\033[1;32m========================================\033[0m"; \
312+
echo ""; \
313+
echo "To test in browser, run:"; \
314+
echo " \033[1;34m./scripts/serve-wasm.py --open\033[0m"; \
315+
echo ""; \
316+
fi
317+
318+
# Override all target to add post-build hook for Emscripten
319+
ifeq ($(CC_IS_EMCC), 1)
320+
all: wasm-install
321+
endif

assets/web/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# WebAssembly runtime files (generated by build)
2+
demo-sdl
3+
demo-sdl.wasm
4+
demo-sdl.data

assets/web/index.html

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Mado Window System (WebAssembly)</title>
7+
<style>
8+
body {
9+
margin: 0;
10+
padding: 20px;
11+
background: #1a1a1a;
12+
color: #fff;
13+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
14+
display: flex;
15+
flex-direction: column;
16+
align-items: center;
17+
}
18+
h1 {
19+
margin-bottom: 10px;
20+
}
21+
.info {
22+
margin-bottom: 20px;
23+
color: #aaa;
24+
text-align: center;
25+
}
26+
#canvas {
27+
border: 1px solid #444;
28+
background: #000;
29+
image-rendering: pixelated;
30+
image-rendering: crisp-edges;
31+
}
32+
#status {
33+
margin-top: 10px;
34+
color: #888;
35+
}
36+
#console {
37+
margin-top: 20px;
38+
padding: 15px;
39+
background: #000;
40+
border: 1px solid #444;
41+
border-radius: 4px;
42+
max-width: 800px;
43+
max-height: 300px;
44+
overflow-y: auto;
45+
font-family: 'Courier New', monospace;
46+
font-size: 12px;
47+
text-align: left;
48+
display: none;
49+
}
50+
#console.error {
51+
display: block;
52+
border-color: #f44;
53+
}
54+
.console-line {
55+
margin: 2px 0;
56+
padding: 2px 0;
57+
}
58+
.console-error {
59+
color: #f44;
60+
}
61+
.console-warn {
62+
color: #fa0;
63+
}
64+
.console-info {
65+
color: #4af;
66+
}
67+
.spinner {
68+
display: inline-block;
69+
width: 20px;
70+
height: 20px;
71+
border: 3px solid rgba(255,255,255,.3);
72+
border-radius: 50%;
73+
border-top-color: #fff;
74+
animation: spin 1s ease-in-out infinite;
75+
}
76+
@keyframes spin {
77+
to { transform: rotate(360deg); }
78+
}
79+
</style>
80+
</head>
81+
<body>
82+
<h1>Mado Window System</h1>
83+
<div class="info">
84+
WebAssembly build running in your browser<br>
85+
</div>
86+
<canvas id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas>
87+
<div id="status">
88+
<span class="spinner"></span> Loading...
89+
</div>
90+
<div id="console"></div>
91+
92+
<script type='text/javascript'>
93+
var statusElement = document.getElementById('status');
94+
var consoleElement = document.getElementById('console');
95+
var canvas = document.getElementById('canvas');
96+
97+
function addConsoleLog(message, type) {
98+
consoleElement.classList.add('error');
99+
var line = document.createElement('div');
100+
line.className = 'console-line console-' + type;
101+
line.textContent = message;
102+
consoleElement.appendChild(line);
103+
consoleElement.scrollTop = consoleElement.scrollHeight;
104+
}
105+
106+
var Module = {
107+
// SDL2 environment variables for Emscripten
108+
preRun: [function() {
109+
ENV.SDL_EMSCRIPTEN_KEYBOARD_ELEMENT = '#canvas';
110+
}],
111+
postRun: [],
112+
print: function(text) {
113+
console.log(text);
114+
addConsoleLog(text, 'info');
115+
},
116+
printErr: function(text) {
117+
console.error(text);
118+
addConsoleLog(text, 'error');
119+
},
120+
canvas: canvas,
121+
setStatus: function(text) {
122+
if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
123+
if (text === Module.setStatus.last.text) return;
124+
var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
125+
var now = Date.now();
126+
if (m && now - Module.setStatus.last.time < 30) return;
127+
Module.setStatus.last.time = now;
128+
Module.setStatus.last.text = text;
129+
if (m) {
130+
text = m[1];
131+
statusElement.innerHTML = text + ' (' + m[2] + '/' + m[4] + ')';
132+
} else {
133+
statusElement.innerHTML = text;
134+
}
135+
},
136+
totalDependencies: 0,
137+
monitorRunDependencies: function(left) {
138+
this.totalDependencies = Math.max(this.totalDependencies, left);
139+
Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
140+
}
141+
};
142+
Module.setStatus('Downloading...');
143+
window.onerror = function(message, source, lineno, colno, error) {
144+
var errorMsg = 'Exception: ' + message;
145+
if (error && error.stack) {
146+
errorMsg += '\n' + error.stack;
147+
}
148+
if (source) {
149+
errorMsg += '\nSource: ' + source + ':' + lineno + ':' + colno;
150+
}
151+
Module.setStatus('Exception thrown - see error details below');
152+
statusElement.style.color = '#f44';
153+
addConsoleLog(errorMsg, 'error');
154+
Module.setStatus = function(text) {
155+
if (text) {
156+
Module.printErr('[post-exception status] ' + text);
157+
}
158+
};
159+
return true;
160+
};
161+
162+
// Catch unhandled promise rejections
163+
window.addEventListener('unhandledrejection', function(event) {
164+
var errorMsg = 'Unhandled Promise Rejection: ' + event.reason;
165+
if (event.reason && event.reason.stack) {
166+
errorMsg += '\n' + event.reason.stack;
167+
}
168+
addConsoleLog(errorMsg, 'error');
169+
});
170+
</script>
171+
<script async type="text/javascript" src="demo-sdl"></script>
172+
</body>
173+
</html>

0 commit comments

Comments
 (0)