Skip to content

Commit 656f54d

Browse files
NathanFlurryclaude
andcommitted
feat(wasm-gui M8.3): libfm (LXDE data layer) cross-compiles + lists the kernel VFS, all wasm
libfm-extra -> menu-cache -> libfm-gtk3 all cross-compile from UNMODIFIED upstream and libfm enumerates a kernel-VFS directory headless (19 entries listed, clean exit). Proof: ~/tmp/gui-progress/proof-m8.3-libfm-lists-vfs.txt. Per constraint #5 every gap is platform/ toolchain-layer: - toolchain/openbox-compat.c: execv/execve stubs (no process-image replacement in the sandbox) + ns_get16/ns_get32 (gio resolver DNS field readers, implemented). - toolchain/wasi-compat.c: strsignal made weak (libwasi-emulated-signal owns the real one; strong caused a duplicate-symbol link error when libfm pulls both). - toolchain/host-bin/: fake intltool tools (intltool unavailable; only needed for translations, which we --disable-nls) + a stub perl XML::Parser to satisfy intltool's configure check. - scripts/build-libfm.sh + test-m8-libfm.sh: reproducible build (libfm-extra --with-extra-only, menu-cache lib-only since menu-cache-gen needs fork/exec, libfm --with-gtk=3 --disable-old-actions to skip the Vala component) + the headless dir-listing acceptance test. The menu-cache freedesktop enumeration (menu-cache-gen via fork/exec) is a separate wasi concern; the libfm folder/listing path is proven. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d3b4edf commit 656f54d

10 files changed

Lines changed: 173 additions & 1 deletion

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* M8.3 headless test: drive libfm (the LXDE data layer) to list a directory of the kernel VFS, proving
2+
* the cross-compiled libfm + glib/gio stack runs end to end in wasm. No X / no GUI: libfm's FmFolder
3+
* loads asynchronously on the GLib main loop, so we run a GMainLoop until "finish-loading", then print
4+
* the entries. Diagnostics to stderr (the host captures it). Usage: libfm-test [dir] (default /usr/share). */
5+
#include <libfm/fm.h>
6+
#include <glib.h>
7+
8+
static GMainLoop *loop;
9+
static int rc = 1;
10+
11+
static void on_finish(FmFolder *folder, gpointer data) {
12+
(void) data;
13+
FmFileInfoList *files = fm_folder_get_files(folder);
14+
guint n = files ? fm_file_info_list_get_length(files) : 0;
15+
g_printerr("LIBFM-TEST: folder loaded, %u entries:\n", n);
16+
GList *l = files ? fm_file_info_list_peek_head_link(files) : NULL;
17+
int i = 0;
18+
for (; l && i < 25; l = l->next, ++i) {
19+
FmFileInfo *fi = (FmFileInfo *) l->data;
20+
g_printerr(" - %s\n", fm_file_info_get_name(fi));
21+
}
22+
if (n > 0) { g_printerr("LIBFM-TEST: PASS (listed %u entries via libfm)\n", n); rc = 0; }
23+
else g_printerr("LIBFM-TEST: FAIL (folder loaded but empty)\n");
24+
g_main_loop_quit(loop);
25+
}
26+
27+
static gboolean timeout_fail(gpointer d) {
28+
(void) d;
29+
g_printerr("LIBFM-TEST: FAIL (timeout — folder never finished loading)\n");
30+
g_main_loop_quit(loop);
31+
return G_SOURCE_REMOVE;
32+
}
33+
34+
int main(int argc, char **argv) {
35+
g_printerr("LIBFM-TEST: fm_init\n");
36+
fm_init(NULL);
37+
loop = g_main_loop_new(NULL, FALSE);
38+
const char *dir = argc > 1 ? argv[1] : "/";
39+
g_printerr("LIBFM-TEST: listing %s\n", dir);
40+
FmFolder *folder = fm_folder_from_path_name(dir);
41+
if (!folder) { g_printerr("LIBFM-TEST: FAIL (no folder)\n"); return 1; }
42+
if (fm_folder_is_loaded(folder))
43+
on_finish(folder, NULL);
44+
else
45+
g_signal_connect(folder, "finish-loading", G_CALLBACK(on_finish), NULL);
46+
g_timeout_add(20000, timeout_fail, NULL);
47+
g_main_loop_run(loop);
48+
return rc;
49+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env bash
2+
# M8.3: cross-compile the LXDE data layer (libfm-extra -> menu-cache -> libfm-gtk3) to
3+
# wasm32-wasip1-threads, from UNMODIFIED upstream. Dep order is circular-ish: menu-cache needs
4+
# libfm-extra, full libfm needs menu-cache. All wasi gaps fixed in the platform/toolchain layer
5+
# (constraint #5): see toolchain/openbox-compat.c (getpwuid/exec*/ns_get*), compat-include/grp.h,
6+
# wasi-compat.c (weak strsignal), libhostcompat.a.
7+
#
8+
# Host build-tool shims (intltool is unavailable; only needed for translations, which we --disable-nls):
9+
# toolchain/host-bin/{intltool-update,intltoolize,intltool-merge,intltool-extract} + a stub
10+
# perl5/XML/Parser.pm. The real gettext tools need libxml2.so.2 on LD_LIBRARY_PATH.
11+
set -uo pipefail
12+
cd "$(dirname "$0")/.."
13+
EXP="$(pwd)"; REPO="$(cd ../.. && pwd)"
14+
export SECURE_EXEC_WASM_THREADS=1
15+
source "$EXP/toolchain/cross-env.sh"
16+
TP="$EXP/third_party"
17+
export PATH="$EXP/toolchain/host-bin:/home/linuxbrew/.linuxbrew/bin:$PATH"
18+
export PERL5LIB="$EXP/toolchain/host-bin/perl5:${PERL5LIB:-}"
19+
# the host gettext tools (xgettext/msgfmt) need libxml2.so.2; point at the nix copy if present
20+
XML2=$(find /nix/store -maxdepth 2 -name "libxml2.so.2" 2>/dev/null | head -1)
21+
[ -n "$XML2" ] && export LD_LIBRARY_PATH="$(dirname "$XML2"):${LD_LIBRARY_PATH:-}"
22+
newest_config_sub() { echo "$TP/libX11-threads/config.sub"; }
23+
LDADD_HOST="-lhostcompat -Wl,--allow-undefined"
24+
25+
# 0. libhostcompat.a must exist (build-openbox.sh makes it; rebuild here too in case)
26+
"$CC" $CFLAGS -c "$EXP/toolchain/openbox-compat.c" -o "$EXP/toolchain/openbox-compat.o" 2>/dev/null
27+
"$AR" rcs "$PREFIX/lib/libhostcompat.a" "$EXP"/toolchain/threads-libs/{host_socket,host_pipe_dup,override_fcntl}.o \
28+
"$EXP/toolchain/wasi-compat-threads.o" "$EXP/toolchain/openbox-compat.o"
29+
30+
cfg() { # cfg <dir> <extra configure args...>
31+
local d="$1"; shift
32+
cp "$(newest_config_sub)" "$(dirname "$(newest_config_sub)")/config.guess" "$TP/$d/" 2>/dev/null
33+
( cd "$TP/$d" && export LDFLAGS="$LDFLAGS $LDADD_HOST" && make distclean >/dev/null 2>&1
34+
./configure $CROSS_CONFIGURE_ARGS --disable-maintainer-mode --disable-nls --disable-gtk-doc "$@" \
35+
> "/tmp/conf-$d.log" 2>&1 && touch aclocal.m4 configure config.h.in Makefile.in */Makefile.in 2>/dev/null )
36+
}
37+
38+
# 1. libfm-extra (the non-GTK utility lib; menu-cache requires it)
39+
if [ ! -f "$PREFIX/lib/libfm-extra.a" ]; then
40+
cd "$TP"; [ -d libfm ] || { mkdir -p libfm && tar xf libfm.tar -C libfm --strip-components=1; }
41+
[ -d libfm-threads ] || cp -r libfm libfm-threads
42+
cfg libfm-threads --with-extra-only
43+
( cd "$TP/libfm-threads" && export LDFLAGS="$LDFLAGS $LDADD_HOST" && make -j4 && make install ) >/tmp/make-libfmextra.log 2>&1 \
44+
&& echo " OK libfm-extra" || { echo " FAIL libfm-extra"; exit 1; }
45+
fi
46+
47+
# 2. menu-cache (library only; menu-cache-gen needs fork/exec at runtime, not built)
48+
if [ ! -f "$PREFIX/lib/libmenu-cache.a" ]; then
49+
cd "$TP"; [ -d menu-cache ] || { mkdir -p menu-cache && tar xf menu-cache.tar -C menu-cache --strip-components=1; }
50+
[ -d menu-cache-threads ] || cp -r menu-cache menu-cache-threads
51+
cfg menu-cache-threads
52+
( cd "$TP/menu-cache-threads" && export LDFLAGS="$LDFLAGS $LDADD_HOST" \
53+
&& make -C libmenu-cache -j4 && make -C libmenu-cache install \
54+
&& cp libmenu-cache.pc "$PREFIX/lib/pkgconfig/" 2>/dev/null ) >/tmp/make-menucache.log 2>&1 \
55+
&& echo " OK menu-cache (lib)" || { echo " FAIL menu-cache"; exit 1; }
56+
fi
57+
58+
# 3. full libfm (gtk3). --disable-old-actions skips the Vala 'custom actions' (no valac here).
59+
cd "$TP/libfm-threads"; cfg libfm-threads --with-gtk=3 --disable-old-actions
60+
( cd "$TP/libfm-threads" && export LDFLAGS="$LDFLAGS $LDADD_HOST" && make -j4 && make install ) >/tmp/make-libfm.log 2>&1 \
61+
&& echo "OK: libfm built — $(ls $PREFIX/lib/libfm*.a | xargs -n1 basename | tr '\n' ' ')" \
62+
|| { echo "FAIL libfm"; tail -12 /tmp/make-libfm.log; exit 1; }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
# M8.3 acceptance: build the libfm-test guest and run it headless (no X) to prove the cross-compiled
3+
# libfm + glib/gio data layer enumerates a kernel-VFS directory in wasm. PASS = libfm lists entries
4+
# and exits 0. (The menu-cache freedesktop enumeration goes through menu-cache-gen which needs
5+
# fork/exec at runtime — tracked separately; this proves the libfm folder/listing path.)
6+
set -uo pipefail
7+
cd "$(dirname "$0")/.."
8+
EXP="$(pwd)"; REPO="$(cd ../.. && pwd)"
9+
export SECURE_EXEC_WASM_THREADS=1
10+
source "$EXP/toolchain/cross-env.sh"
11+
HOST="$REPO/target/debug/wasm-gui-host"; SIDECAR="$REPO/target/debug/secure-exec-sidecar"
12+
[ -f "$PREFIX/lib/libfm.a" ] || bash "$EXP/scripts/build-libfm.sh" || { echo "libfm build failed"; exit 1; }
13+
14+
WASMSUB="wasm32-wasip1-threads"
15+
SETJMP="$WSDK/share/wasi-sysroot/lib/$WASMSUB/libsetjmp.a"; LIBC="$THREADS_SYSROOT/lib/$WASMSUB/libc.a"
16+
HOSTOBJS="$EXP/toolchain/threads-libs/host_socket.o $EXP/toolchain/threads-libs/host_pipe_dup.o $EXP/toolchain/threads-libs/override_fcntl.o"
17+
PCCF=$(PKG_CONFIG_LIBDIR="$PREFIX/lib/pkgconfig" pkg-config --cflags libfm gio-2.0)
18+
PCLIBS=$(PKG_CONFIG_LIBDIR="$PREFIX/lib/pkgconfig" pkg-config --libs libfm gio-2.0)
19+
"$CC" $CFLAGS $PCCF -I"$PREFIX/include" -c "$EXP/guest-xclient/libfm-test.c" -o /tmp/libfm-test.o || exit 1
20+
"$CC" $LDFLAGS -Wl,--allow-undefined -o "$EXP/libfm-test.wasm" /tmp/libfm-test.o \
21+
"$EXP/toolchain/wasi-compat-threads.o" $HOSTOBJS $PCLIBS -lhostcompat -lwasi-emulated-signal "$SETJMP" "$LIBC" || exit 1
22+
export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
23+
wasm-opt --fpcast-emu --enable-bulk-memory --enable-threads -O0 "$EXP/libfm-test.wasm" -o /tmp/lf.fp && mv /tmp/lf.fp "$EXP/libfm-test.wasm"
24+
echo "built libfm-test.wasm ($(stat -c%s "$EXP/libfm-test.wasm")b)"
25+
26+
out="$(env -u DISPLAY "$HOST" --exec --guest "$EXP/libfm-test.wasm" --timeout 35 --sidecar "$SIDECAR" 2>&1)"
27+
echo "$out" | grep -E "LIBFM-TEST" | grep -v NETWRITE
28+
if echo "$out" | grep -q "LIBFM-TEST: PASS"; then echo "M8.3 LIBFM: PASS"; exit 0; else echo "M8.3 LIBFM: FAIL"; exit 1; fi
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
exit 0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
args=("$@"); n=${#args[@]}
3+
in="${args[$((n-2))]}"; out="${args[$((n-1))]}"
4+
if [ -f "$in" ]; then sed -E 's/^_([A-Za-z0-9]+=)/\1/' "$in" > "$out"; else exit 0; fi
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
case "$1" in --version|-V) echo "intltool-update (intltool) 0.51.0";; *) exit 0;; esac
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
exit 0
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Stub XML::Parser: intltool's configure requires the module to exist, but we build with --disable-nls
2+
# and our intltool-extract/-merge shims do not parse XML, so a no-op package satisfies the check.
3+
package XML::Parser;
4+
sub new { return bless {}, shift }
5+
sub setHandlers { }
6+
sub parse { }
7+
sub parsefile { }
8+
1;

experiments/wasm-gui/toolchain/openbox-compat.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,16 @@ int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct p
4141
*result = pwd;
4242
return 0;
4343
}
44+
45+
/* libfm/gio reference these. exec* isn't supported in the sandbox (no process-image replacement);
46+
* stub to failure. ns_get16/ns_get32 are trivial big-endian DNS field readers (gio's resolver);
47+
* implement them (harmless, correct). */
48+
int execv(const char *p, char *const a[]) { (void)p; (void)a; return -1; }
49+
int execve(const char *p, char *const a[], char *const e[]) { (void)p; (void)a; (void)e; return -1; }
50+
unsigned ns_get16(const unsigned char *cp) {
51+
return cp ? (((unsigned)cp[0] << 8) | (unsigned)cp[1]) : 0u;
52+
}
53+
unsigned long ns_get32(const unsigned char *cp) {
54+
return cp ? (((unsigned long)cp[0] << 24) | ((unsigned long)cp[1] << 16) |
55+
((unsigned long)cp[2] << 8) | (unsigned long)cp[3]) : 0ul;
56+
}

experiments/wasm-gui/toolchain/wasi-compat.c

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ char *inet_ntoa(unsigned in) {
8585
int seteuid(unsigned u) { (void)u; return 0; }
8686
int setuid(unsigned u) { (void)u; return 0; }
8787
int setgid(unsigned g) { (void)g; return 0; }
88-
char *strsignal(int s) { (void)s; return (char *)"signal"; }
88+
/* weak: libwasi-emulated-signal provides a real strsignal; ours is only a fallback when that lib
89+
* isn't linked. Strong here caused a duplicate-symbol link error in guests (libfm) that pull both. */
90+
__attribute__((weak)) char *strsignal(int s) { (void)s; return (char *)"signal"; }
8991
/* twm (WM) references these; stubbed: the WM runs in a single sandboxed process with no
9092
subprocess exec, no real uid, and uses tempnam only for a session id. */
9193
int execlp(const char *f, const char *a, ...) { (void)f; (void)a; return -1; }

0 commit comments

Comments
 (0)