Merge master (ff69f7e4, integ/2026-10-09b) into merge/unity-seed

Conflict: web/src/runtime.js (lazy kept beside symlinks).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
velzieandClaude Opus 5.5 committed 2026-10-09 12:21:30 -04:00
commit 7bcb4fbe7c
39 files changed
+2589 -146

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
/* AAPCS64 (aarch64 Linux), ahead of an x86-64 provider's body: the `gp64_`
* helpers it calls mean aarch64's convention here. Integer and pointer
* arguments come in x0-x7, then the stack at SP (8 bytes each); the result
* goes in x0 (a floating-point one in v0, the same context slot as xmm0);
* the return is to x30, and SP is the caller's throughout. The SysV
* register names are gone, so a body that reaches for one does not build. */
GEX_INLINE uint64_t a64_arg(int ctx, unsigned i) {
return i < 8u ? GEX_C64(GEX_CTX_XN(i)) : GW64(GEX_C64(GEX_CTX_SP) + 8u * (i - 8u));
}
GEX_INLINE int a64_return(int ctx, uint64_t ret) {
uint64_t ra = GEX_C64(GEX_CTX_XN(30));
GEX_C64(GEX_CTX_PC) = ra;
return ra == ret;
}
GEX_INLINE int a64_return_x0(int ctx, uint64_t ret, uint64_t x0) {
GEX_C64(GEX_CTX_XN(0)) = x0;
return a64_return(ctx, ret);
}
#define gp64_arg a64_arg
#define gp64_return a64_return
#define gp64_return_rax a64_return_x0
#define gp64_call6 gp64_call6_is_sysv_only
#undef R_RAX
#undef R_RCX
#undef R_RDX
#undef R_RSP
#undef R_RSI
#undef R_RDI
#undef R_R8
#undef R_R9
@@ -0,0 +1,73 @@
/* aarch64: glibc's strcmp (sysdeps/aarch64/strcmp.S) step for step. Its
* result is not the difference of the first differing bytes: the end takes
* the top byte of each word shifted to the highest set bit of the syndrome
* (difference | first string's NUL marks), so 'a' against 'b' or a NUL in
* the second string where the strings are mutually misaligned gives another
* value of the same sign. Programs see this one, so the provider returns
* it (a later GEX_PROVIDE of an export replaces the earlier one). */
#define A64_ONES 0x0101010101010101ull
#define A64_7F 0x7f7f7f7f7f7f7f7full
#define A64_NUL(v) (((v) - A64_ONES) & ~((v) | A64_7F))
GEX_INLINE uint64_t a64_strcmp_end(uint64_t syn, uint64_t d1, uint64_t d2) {
syn = __builtin_bswap64(syn), d1 = __builtin_bswap64(d1), d2 = __builtin_bswap64(d2);
unsigned sh = (syn ? (unsigned)__builtin_clzll(syn) : 64u) & 63u;
return ((d1 << sh) >> 56) - ((d2 << sh) >> 56);
}
static GEX_PROVIDER(p_strcmp_a64) {
(void)depth, (void)need;
uint64_t s1 = gp64_arg(ctx, 0), s2 = gp64_arg(ctx, 1);
uint64_t off = s2 - s1, d1, d2, nul;
if (!(off & 7u)) {
if (s1 & 7u) {
unsigned sh = (unsigned)(-(s2 << 3)) & 63u;
uint64_t pre = ~0ull >> sh;
s1 &= ~7ull;
d2 = GW64(s1 + off) | pre, d1 = GW64(s1) | pre;
s1 += 8;
} else {
d2 = GW64(s1 + off), d1 = GW64(s1);
s1 += 8;
}
while (!(nul = A64_NUL(d1)) && d1 == d2) {
d2 = GW64(s1 + off), d1 = GW64(s1);
s1 += 8;
}
return gp64_return_rax(ctx, ret, a64_strcmp_end((d1 ^ d2) | nul, d1, d2));
}
while (s1 & 7u) {
uint64_t c1 = B(s1), c2 = B(s2);
++s1, ++s2;
if (!c1 || c1 != c2) {
return gp64_return_rax(ctx, ret, c1 - c2);
}
}
/* s1 aligned, s2 not: the aligned word of s2 ahead says where its NUL
* is before an unaligned load could cross a page. */
unsigned sh = (unsigned)(-(s2 << 3)) & 63u;
uint64_t a2 = s2 & ~7ull, w2 = GW64(a2) | (A64_ONES >> sh);
a2 += 8;
uint64_t n2 = A64_NUL(w2);
if (!n2) {
uint64_t ahead = a2 - s1;
for (;;) {
w2 = GW64(s1 + ahead);
d2 = GW64(s1 + off);
n2 = A64_NUL(w2);
d1 = GW64(s1);
s1 += 8;
if (n2 || d1 != d2) {
break;
}
}
uint64_t syn = (d1 ^ d2) | (n2 << sh);
if (syn) {
return gp64_return_rax(ctx, ret, a64_strcmp_end(syn, d1, d2));
}
}
unsigned rs = (unsigned)(-sh) & 63u;
d1 = GW64(s1);
d2 = w2 >> rs;
return gp64_return_rax(ctx, ret, a64_strcmp_end((d1 ^ d2) | (n2 >> rs), d1, d2));
}
GEX_PROVIDE(p_strcmp_a64, "strcmp")
+62 -12
View File
@@ -54,11 +54,47 @@ const SOURCES: &[(&str, &str)] = &[
("glibc_mem", include_str!("../providers/glibc_mem.c")),
("glibc_printf", include_str!("../providers/glibc_printf.c")),
("glibc_math", include_str!("../providers/glibc_math.c")),
// The same functions for aarch64's glibc: the x86-64 bodies under
// AAPCS64 (`aapcs64.inc` makes the `gp64_` helpers mean its registers).
(
"glibc_mem_a64",
concat!(
"/* PROVIDES libc.so.6 aarch64 */\n",
include_str!("../providers/aapcs64.inc"),
include_str!("../providers/glibc_mem.c"),
include_str!("../providers/glibc_mem_a64.inc")
),
),
(
"glibc_math_a64",
concat!("/* PROVIDES libm.so.6 aarch64 */\n", include_str!("../providers/aapcs64.inc"), include_str!("../providers/glibc_math.c")),
),
];
/// Files a stateful provider may `#include` (written beside it when built).
const INCLUDES: &[(&str, &str)] = &[("dlmalloc.inc", include_str!("../providers/dlmalloc.inc"))];
/// The guest architecture a provider's calling convention is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Arch {
I386,
X86_64,
Aarch64,
}
/// The provider architecture for an image of this build's guest: i386 or
/// x86-64 on an x86 build, aarch64's 64-bit images on an aarch64 build
/// (its 32-bit ones are wasm32 programs, which have no providers); none on
/// a PPC build.
pub fn guest_arch(class32: bool) -> Option<Arch> {
match (gex_abi::X86, gex_abi::PPC, class32) {
(true, _, true) => Some(Arch::I386),
(true, _, false) => Some(Arch::X86_64),
(false, false, false) => Some(Arch::Aarch64),
_ => None,
}
}
/// One provider source, parsed.
#[derive(Clone, Debug)]
pub struct Provider {
@@ -66,6 +102,8 @@ pub struct Provider {
text: &'static str,
/// The libraries, lower case (`msvcr120.dll`).
pub dlls: Vec<String>,
pub arch: Arch,
/// `arch` is i386.
pub class32: bool,
/// Has data and a stack (`stateful` on the first line).
pub stateful: bool,
@@ -80,10 +118,16 @@ pub struct Provider {
pub build_ids: Vec<String>,
}
/// Every provider gex carries (`gexc cgen-image --providers` builds them
/// all into a unit cache, for a host that cannot build them at run time).
/// Every provider gex carries for this build's guest (`gexc cgen-image
/// --providers` builds them all into a unit cache, for a host that cannot
/// build them at run time).
pub fn all() -> Vec<Provider> {
SOURCES.iter().map(|(n, t)| parse(n, t).expect("a provider source gex carries is well formed")).collect()
let archs = [guest_arch(true), guest_arch(false)];
SOURCES
.iter()
.map(|(n, t)| parse(n, t).expect("a provider source gex carries is well formed"))
.filter(|p| archs.contains(&Some(p.arch)))
.collect()
}
/// Whether providers are on (`GEX_PROVIDERS=0` turns them off).
@@ -95,7 +139,7 @@ fn parse(name: &'static str, text: &'static str) -> Result<Provider> {
let first = text.lines().next().unwrap_or("");
let w: Vec<&str> = first.split_whitespace().collect();
if w.len() < 4 || w[0] != "/*" || w[1] != "PROVIDES" {
bail!("provider {name}: the first line must be `/* PROVIDES <dll>[,<dll>...] <i386|x86_64> [stateful] [nowine]`");
bail!("provider {name}: the first line must be `/* PROVIDES <dll>[,<dll>...] <i386|x86_64|aarch64> [stateful] [nowine]`");
}
let stateful = w[4..].contains(&"stateful");
let nowine = w[4..].contains(&"nowine");
@@ -106,11 +150,13 @@ fn parse(name: &'static str, text: &'static str) -> Result<Provider> {
.filter(|x| !x.is_empty())
.map(|x| x.to_ascii_lowercase())
.collect();
let class32 = match w[3] {
"i386" => true,
"x86_64" => false,
let arch = match w[3] {
"i386" => Arch::I386,
"x86_64" => Arch::X86_64,
"aarch64" => Arch::Aarch64,
a => bail!("provider {name}: unknown architecture {a}"),
};
let class32 = arch == Arch::I386;
let mut binds = Vec::new();
let mut groups = Vec::new();
for l in text.lines() {
@@ -124,6 +170,9 @@ fn parse(name: &'static str, text: &'static str) -> Result<Provider> {
.split_once(',')
.ok_or_else(|| anyhow!("provider {name}: malformed {l:?}"))?;
let e = e.trim().trim_end_matches(')').trim().trim_matches('"');
// A later binding of an export replaces an earlier one (an
// architecture's own body after a shared source's).
binds.retain(|(_, x)| x != e);
binds.push((f.trim().to_string(), e.to_string()));
}
if binds.is_empty() {
@@ -134,20 +183,21 @@ fn parse(name: &'static str, text: &'static str) -> Result<Provider> {
// `memcpy@@GLIBC_2.14`).
binds.retain(|(_, e)| !skip.split(',').any(|s| s == e || e.split('@').next() == Some(s)));
let dlls = w[2].split(',').map(|d| d.to_ascii_lowercase()).collect();
Ok(Provider { name, text, dlls, class32, stateful, nowine, binds, groups, build_ids })
Ok(Provider { name, text, dlls, arch, class32, stateful, nowine, binds, groups, build_ids })
}
/// The providers for library `dll` (its export directory's name, any case)
/// of the given class, when providers are on.
/// of the given class, for this build's guest ([`guest_arch`]), when
/// providers are on.
pub fn for_dll(dll: &str, class32: bool) -> Vec<Provider> {
if !enabled() || !gex_abi::X86 {
let Some(arch) = guest_arch(class32).filter(|_| enabled()) else {
return Vec::new();
}
};
let dll = dll.to_ascii_lowercase();
SOURCES
.iter()
.map(|(n, t)| parse(n, t).expect("a provider source gex carries is well formed"))
.filter(|p| p.dlls.contains(&dll) && p.class32 == class32)
.filter(|p| p.dlls.contains(&dll) && p.arch == arch)
.collect()
}
+14
View File
@@ -157,6 +157,20 @@ impl FuncMap {
n
}
/// [`FuncMap::publish`] at `base` when nothing has taken it yet: a fork's
/// child of a wasm32 program installs the parent's module where the
/// parent had it, because the function pointers in the data it copied
/// are `__table_base + i` for the parent's base (an exec'd program's base
/// is past its predecessor's range, which is never reused). Returns
/// whether the install got that base.
pub fn publish_at(&mut self, base: u32, len: u32, module: u32, tier: Tier, funcs: &[(u64, u32)]) -> (u32, bool) {
if base >= self.next {
self.next = base;
}
let n = self.publish(len, module, tier, funcs);
(n, self.installs[n as usize].base == base)
}
/// Map more addresses to slots of install `install`: one module serving
/// several images (Wine's PE side as one module, docs/wine-native.md
/// §3.14, bound DLL by DLL as each maps). Nothing changes in any table
+94 -1
View File
@@ -214,6 +214,8 @@ pub mod raw {
pub const EPOLL_PWAIT: u64 = 319;
pub const UTIMENSAT: u64 = 320;
pub const TIMERFD_CREATE: u64 = 322;
pub const TIMERFD_SETTIME: u64 = 325;
pub const TIMERFD_GETTIME: u64 = 326;
pub const FALLOCATE: u64 = 324;
pub const EVENTFD2: u64 = 328;
pub const EPOLL_CREATE1: u64 = 329;
@@ -279,6 +281,9 @@ pub enum Post {
/// `getrlimit`'s 32-bit rlimit: the canonical `prlimit64` wrote 16 bytes
/// at `scratch`; each half saturates to the 32-bit `RLIM_INFINITY`.
Rlimit32 { user: u64, scratch: u64 },
/// A `struct itimerspec` with 32-bit fields (`timerfd_settime`'s old
/// value, `timerfd_gettime`): two timespecs at `scratch`, narrowed.
Itimerspec32 { user: u64, scratch: u64 },
}
impl Post {
@@ -305,6 +310,15 @@ impl Post {
}
r
}
Post::Itimerspec32 { user, scratch } => {
if ok && user != 0 {
for i in 0..4 {
let v = m.read_u64(scratch + i * 8).unwrap_or(0);
let _ = m.write_u32(user + i * 4, v as u32);
}
}
r
}
Post::Timespec32 { user, scratch } | Post::Timeval32 { user, scratch } => {
if ok && user != 0 {
let s = m.read_u64(scratch).unwrap_or(0);
@@ -643,6 +657,35 @@ pub fn translate(
nr::NANOSLEEP
}
x::TIMERFD_CREATE => nr::TIMERFD_CREATE,
// The 32-bit pair is what musl's time64 `timerfd_settime` issues when
// the times fit and no old value is asked for -- foot's delayed-render
// timers. Unmapped, it was ENOSYS and those timers never armed, so
// foot drew its first frames and then never again.
x::TIMERFD_SETTIME => {
a[0] = sext(a[0]);
// Both timespecs widened into scratch (interval, then value); the
// old value, if asked for, comes back through the scratch's
// second half and is narrowed afterwards.
let new = a[2];
if new != 0 {
widen_time(m, new, scratch, 1);
widen_time(m, new + 8, scratch + 16, 1);
a[2] = scratch;
}
if a[3] != 0 {
post = Post::Itimerspec32 { user: a[3], scratch: scratch + 32 };
a[3] = scratch + 32;
}
nr::TIMERFD_SETTIME
}
x::TIMERFD_GETTIME => {
a[0] = sext(a[0]);
if a[1] != 0 {
post = Post::Itimerspec32 { user: a[1], scratch };
a[1] = scratch;
}
nr::TIMERFD_GETTIME
}
x::TIMERFD_SETTIME64 => {
a[0] = sext(a[0]);
nr::TIMERFD_SETTIME
@@ -987,7 +1030,20 @@ pub fn translate(
// multiplexes calls glibc no longer makes through it.
x::RECVMMSG | x::SOCKETCALL => UNKNOWN,
x::REBOOT => nr::REBOOT,
x::STATFS | x::FSTATFS | x::STATFS64 | x::FSTATFS64 => UNKNOWN,
// statfs64(path, size, buf) / fstatfs64(fd, size, buf): the 64-bit
// layout (musl's `struct statfs`), written in its 32-bit form under
// `compat` (apt checks for room before it downloads). The old
// statfs/fstatfs with 32-bit counts are not made by musl.
x::STATFS64 => {
a[1] = a[2];
nr::STATFS
}
x::FSTATFS64 => {
a[0] = sext(a[0]);
a[1] = a[2];
nr::FSTATFS
}
x::STATFS | x::FSTATFS => UNKNOWN,
x::TRUNCATE | x::TRUNCATE64 | x::GETDENTS => UNKNOWN,
_ => UNKNOWN,
};
@@ -1048,4 +1104,41 @@ mod tests {
let (n, a) = tr(raw::PREAD64, [3, 0x3000, 16, 0x2000, 1, 0], &mut buf);
assert_eq!((n, a[3]), (nr::PREAD64, 0x1_0000_2000));
}
#[test]
fn timerfd_settime_widens_a_32_bit_itimerspec() {
let mut buf = vec![0u8; 0x4000];
{
let mut m = GuestMem::new(&mut buf);
// { interval {0, 0}, value {0, 500000} }: foot's lower render timer.
for (i, v) in [0u32, 0, 0, 500_000].iter().enumerate() {
m.write_u32(0x3000 + i as u64 * 4, *v).unwrap();
}
}
let (n, a) = tr(raw::TIMERFD_SETTIME, [5, 0, 0x3000, 0, 0, 0], &mut buf);
assert_eq!(n, nr::TIMERFD_SETTIME);
assert_eq!(a[..4], [5, 0, SCRATCH, 0]);
let m = GuestMem::new(&mut buf);
let words: Vec<u64> = (0..4).map(|i| m.read_u64(SCRATCH + i * 8).unwrap()).collect();
assert_eq!(words, vec![0, 0, 0, 500_000]);
}
#[test]
fn timerfd_gettime_narrows_into_the_callers_struct() {
let mut buf = vec![0u8; 0x4000];
let (n, a, post) = {
let mut m = GuestMem::new(&mut buf);
let mut a = [5, 0x3000, 0, 0, 0, 0];
let (n, post) = translate(raw::TIMERFD_GETTIME, &mut a, &mut m, SCRATCH, 0x1000);
(n, a, post)
};
assert_eq!((n, a[1]), (nr::TIMERFD_GETTIME, SCRATCH));
let mut m = GuestMem::new(&mut buf);
for (i, v) in [1u64, 2, 3, 4].iter().enumerate() {
m.write_u64(SCRATCH + i as u64 * 8, *v).unwrap();
}
assert_eq!(post.finish(&mut m, 0), 0);
let words: Vec<u32> = (0..4).map(|i| m.read_u32(0x3000 + i * 4).unwrap()).collect();
assert_eq!(words, vec![1, 2, 3, 4]);
}
}
+28 -14
View File
@@ -5205,6 +5205,19 @@ impl Kernel {
put(&mut out, 48, 1 << 17); // f_ffree
put(&mut out, 72, 255); // f_namelen
put(&mut out, 80, 4096); // f_frsize
if self.compat {
// i386's statfs64 (and musl's wasm32 `struct statfs`): 32-bit
// f_type, f_bsize, f_namelen, f_frsize, 64-bit counts
let mut c = [0u8; 84];
let w32 = |o: &mut [u8; 84], at: usize, v: u32| o[at..at + 4].copy_from_slice(&v.to_le_bytes());
w32(&mut c, 0, 0x0102_1994);
w32(&mut c, 4, 4096);
c[8..48].copy_from_slice(&out[16..56]); // blocks, bfree, bavail, files, ffree
w32(&mut c, 56, 255);
w32(&mut c, 60, 4096);
m.write(buf, &c)?;
return Ok(0);
}
m.write(buf, &out)?;
Ok(0)
}
@@ -6559,23 +6572,24 @@ impl Kernel {
break;
}
}
let is_unix = unix_sock.is_some();
// Who sent it, as recvfrom says: musl's resolver drops a reply whose
// msg_name is not one of its nameservers. A unix peer has no name.
let name = self.msg_rd(m, msg, 0)?;
let namelen_at = msg + if self.compat { 4 } else { 8 };
if name != 0 {
if unix_sock.is_some() {
m.write_u32(namelen_at, 0)?;
} else {
let peer = self.socket_of(fd).ok().and_then(|s| s.last_from().or_else(|| s.peer()));
let (ip, port) = peer.unwrap_or((0, 0));
self.sys_write_sockaddr(m, ip, port, name, namelen_at)?;
}
}
let received = unix_sock.map(|s| s.take_received()).unwrap_or_default();
let written = self.write_scm_rights(m, control, controllen, received)?;
self.msg_wr(m, msg, 5, written)?;
// Where the datagram came from, as `recvfrom` says it. musl's
// resolver (1.2.4 on) reads its answers with `recvmsg` and drops any
// whose source is not a nameserver it asked: with the name left
// unwritten every answer was dropped, and each `getaddrinfo` waited
// out the resolver's 5 s timeout and failed.
let name = self.msg_rd(m, msg, 0)?;
if name != 0 && !is_unix {
if let Ok(sock) = self.socket_of(fd) {
let (ip, port) = sock.last_from().or_else(|| sock.peer()).unwrap_or((0, 0));
let namelen = msg + if self.compat { 4 } else { 8 };
self.sys_write_sockaddr(m, ip, port, name, namelen)?;
}
}
// msg_flags (int): nothing truncated
m.write_u32(msg + if self.compat { 24 } else { 48 }, 0)?;
Ok(total)
}
+7
View File
@@ -114,6 +114,13 @@ addresses under `--mem64`.
These re-key every unit, so they go in **one commit**, and the
coordinator schedules its landing between E/F measurement series. New
provider `.c` sources don't re-key anything.
- **aarch64 (2026-10-08):** a source's first line names its architecture
(`i386`, `x86_64` or `aarch64`), and `provider::guest_arch` picks the
ones for the build's guest. `providers/aapcs64.inc` in front of an
x86-64 body makes its `gp64_` helpers mean AAPCS64 (x0-x7, x0/v0, x30):
`glibc_mem_a64` and `glibc_math_a64` are built that way (no x86
fingerprint moves). A later `GEX_PROVIDE` of an export replaces an
earlier one (aarch64's strcmp, `glibc_mem_a64.inc`).
- Switches: `GEX_PROVIDERS=0` and `GEX_PROVIDER_SKIP`, as for PE.
`GEX_PROVIDER_TRACE` lists the bindings.
+151 -32
View File
@@ -421,10 +421,10 @@ without polls, ABBA, 6 runs each): median total 1.91 s with polls against
to mw.
- Don't edit crates/gex-cgen without asking.
- No game runs on gooner; the corpus is fine here. No timing on gooner.
- **Demo (§6), state 2026-10-08:**
- **Demo (§6), state 2026-10-09:**
- Design: §6.1a (arm64 native from Debian, `musl-linux-wasm32` ours),
approved. Item 1 done (gated as integ/2026-10-08f). Item 2 (aarch64
ELF providers) ready on branch `merge/gexcc-a64prov` (ecf2b251), no
ELF providers) on this branch, `merge/gexcc-a64prov`, no
x86 re-key; the coordinator schedules its landing with U. Item 3
(LP64-layout libraries lowered to wasm32, no memory64) not started:
measure the lowering's cost in V8 on edger first.
@@ -437,27 +437,69 @@ without polls, ABBA, 6 runs each): median total 1.91 s with polls against
(`src/`, `stage/`, `logs/`, `built.txt`, `pool/`); Debian downloads
cached in `scratch/CC/deb`. Whole thing: `web/build-native-debian.sh`
with `GEXCC_PREFIX=~/gex-sandbox/scratch/CC/dtc`, in the build scope.
- Tier 1 rootfs: 44 Debian arm64 packages + 27 of ours, 91.5 MiB
unpacked, 20.4 MB as tar.zst.
- Works (gooner, correctness only): natively (`gexc run --uid 0:0
--sysroot TREE`) and in the browser runtime under Node
(`run-node.mjs --tar`, `GEX_UID=0`, `GEX_NETWORK=1`): bash, the tools,
dpkg (`--audit` clean), `apt-get update` against deb.debian.org
(signatures checked), `apt-get install jq` (arm64 jq, libjq1,
libonig5 translated) and jq runs; the pins keep ours over
`coreutils:arm64`. Test trees in `scratch/CC/rootfs` (`tree` fresh,
`native-cache.tar`: lists and jq's .debs, for `apt-get install
--no-download` without the network).
- Unverified: update and install in one browser-runtime session went
past 900 s once on gooner (each alone passes).
- Page startup, edger (bench.sh, headless Chromium, 8 threads,
`web/test/site.mjs native-debian`): browser launch to the
`root@gex:~#` prompt 5.4-5.6 s (5578, 5408, 5395 ms; 20.4 MB rootfs
from localhost). In the guest (`native-debian-cmd`, each figure
includes two `date` forks, about 0.5 s): `ls /` 1054 then 1041 ms,
`perl -e 1` 3234 then 2204 ms, `dpkg -l` 2658 then 2684 ms,
`apt-cache policy coreutils` 11504 then 7415 ms.
- Next: tier 2; GUI; item 3.
- Rootfs (tiers 1, 2 and pm): 22 Debian arm64 packages + 44 of ours,
175.6 MiB unpacked, 39.3 MB as tar.zst (`native-debian.tar.zst`);
`native-debian-cache.tar.zst` (49.7 MB) adds Debian's lists and jq's
.debs for `apt-get install --no-download jq` without the network.
Rebuild: `mkrootfs.py --pool $GEXCC_DEB/pool --out TREE --tar TAR`
(`--exclude jq` for the cache image), zstd -19 into `web/public/rootfs/`.
- dpkg, apt (one `apt-multi` program) and gpgv are ours, labelled arm64
(§6.1a). Natively: tampered InRelease rejected, `apt-get update` from
deb.debian.org 10.1 MB in 12 s, install/remove, `dpkg --audit` clean.
- In the page (edger, bench.sh, `site.mjs native-debian-run`, real Wisp
to anura.pro): `apt-get update` 9.33 s (10.1 MB at 1633 kB/s),
`apt-get install -y hello` 5.79 s, `hello` runs. The Wisp connection
is the page's, served to Workers over shared memory
(`shims/browser/js/net-bridge.mjs`): a WebSocket in a wasm32 program's
own Worker never delivers, since the program never returns its event
loop.
- `apt-get install --no-download jq` in the page, 26 processes: 92.9 s
with Debian's translated apt/dpkg/debconf, 8.6 s with ours (8.65 and
8.56 s). Before, by process: apt-get 92.65 s, dpkg --unpack 39.5 s,
dpkg-preconfigure 17.4 s (perl; ld.so 7.3 s, translation 2.5 s), each
dpkg-deb 4.6-5.5 s (ld.so ~0.7 s, translation ~0.4 s, compile
~0.37 s), apt-extracttemplates 5.1 s; summed over processes ld.so
19.0 s, translation 14.1 s (452k blocks), compile 9.4 s, Worker
start 3.7 s. After: apt-get 8.4 s, dpkg --unpack 2.8 s, each dpkg-deb
~0.3 s.
- Page startup (browser launch to the prompt): 5441 and 5407 ms.
`native-debian-cmd` (each includes two `date` forks, ~0.5 s), before
then after: `dpkg -l` 2658/2684 then 1084/1030 ms; `apt-cache policy
coreutils` 11504/7415 (first), 5.7/4.9 s (later) then 1330/1167 ms;
`ls /` 954/1028 ms; `perl -e 1` 3297/1855 ms (Debian's perl,
translated).
- GUI tier (2026-10-09), `?gui=1&rootfs=/rootfs/native-debian-gui.tar.zst`
(61.6 MB; 27 Debian packages + 48 of ours, 262 MiB unpacked; rebuild
with `scratch/CC/mkgui.sh`): a compositor canvas above the terminal,
`WAYLAND_DISPLAY=wayland-0` and the page's X server (`DISPLAY=:0`).
- foot (ours, with wayland, xkbcommon, pixman, freetype, fontconfig,
fcft) runs bash in its window, draws colour and bold, and takes
typed keys (`drive.mjs --wltype`). It used to stop drawing after
its first frames: musl's time64 `timerfd_settime` issues the 32-bit
call (325) when the times fit, which the i386/wasm32 personality
did not map, so foot's delayed-render timers never fired.
- Chocolate Doom (ours, on SDL2's X11 driver, SDL2_mixer, SDL2_net,
libX11/libxcb, all static) on Debian's freedoom plays its demo loop
in the page through the page's Xwayland. `-timedemo demo1` stalls
after ST_Init with the process alive and its window gone (open).
No sound (SDL's dummy driver only).
- xkbcommon's Compose tables come from Debian's libx11-data.
- edger scripts: `scratch/CC/pipe/gui.sh` (foot), `doom.sh`,
`doom2.sh` (timedemo); drive.mjs with `GEX_WEBGPU_SOFTWARE=1`.
- python3: `_ctypes` built on the wasm32 libffi
(`tools/gexcc/debian/libffi-wasm32`: generated trampolines, no
closures); `import ctypes`, types, structures and buffers work;
`CDLL(None)` raises OSError (no dlopen), patched so the import
survives it.
- Known: fork inside a C++ catch block traps (asyncify).
- perl 5.40 ours builds (`build_perl`): Debian's options, Configured
natively, every extension static; make_ext's make and compiler run on
the host through gexc's `GEXC_HOST_EXEC` (an opt-in build aid for
build tooling only, never set by a demo or image). POSIX, Digest::MD5,
Storable, Encode and threads load. Not shipped: Debian's arm64
perl-base stays until ours provides perlapi-5.40.1 and Debian's
layout for arm64 Perl packages.
- Next: ship perl, bc's dc, /usr/bin/git duplicate, GTK, item 3.
- **Next (corpus):**
1. Rerun the corpus clean.
2. coreutils, bash, make.
@@ -531,15 +573,70 @@ median of 3, ABBA; `scratch/CC/archbench`, 2026-10-08):
**dpkg's architectures.**
- Native architecture **arm64**: apt fetches Debian's arm64 index and
packages as native, its dependency resolution unmodified. `dpkg
--print-architecture` says arm64. Until our own dpkg and apt are built,
dpkg, apt, perl-base, debconf and sqv are Debian's arm64 builds,
translated.
--print-architecture` says arm64. dpkg, apt and gpgv are ours (below);
perl-base and debconf are Debian's arm64 builds, translated.
- Foreign architecture **`musl-linux-wasm32`** (`dpkg --add-architecture`;
the name dpkg's tables give a musl wasm32 tuple) for every package we
build. Tested with the host's dpkg: accepted, and an arm64 package's
`Depends: coreutils (>= 9.7)` is satisfied by
`coreutils:musl-linux-wasm32`.
**Our dpkg and apt (user, 2026-10-08).** Each translated process in the
page pays about 1.5-2.5 s before it does its own work (Worker, translated
ld.so, retranslation; profile in §6.4), and `apt install` runs dozens of
dpkg, dpkg-deb, dpkg-split and gpgv processes. So dpkg, apt and the
OpenPGP verifier are gexcc builds:
- **dpkg** 1.22.22, C, static (libmd, zlib, xz, zstd, bzip2 from tier 1).
configure asks dpkg-architecture for the host triplet's name; the recipe
answers `arm64`, so `ARCHITECTURE` is compiled in as arm64 and
`dpkg --print-architecture` says arm64 (checked natively).
- **apt** 3.0.3, C++ against gexcc's libc++, CMake. libapt-pkg and
libapt-private are static (linked into each program); no seccomp, udev,
systemd, NLS, docs, tests, apt-ftparchive (Berkeley DB) or dselect.
`COMMON_ARCH=arm64`. Two libc++ patches (`patches/apt/`: no
`char_traits<unsigned char>`, no `deque` of an incomplete type).
Deps: xxhash (new, static only), zlib, bzip2, xz, lz4, zstd, OpenSSL.
- **Packaging.** Both are labelled `Architecture: arm64` (binpkgs.txt
`arch dpkg arm64`), version Debian's + `+gex1`, from our repository,
which now carries `binary-arm64` beside `binary-musl-linux-wasm32`. The
`o=gex` pin (1001) keeps them over Debian's, also after a Debian point
release (1.22.23 > 1.22.22+gex1 still loses to 1001); the `-1` pin list
covers only our wasm32 names, so it does not block them.
- **Essential/Pre-Depends.** Debian's dpkg is `Essential: yes` with
`Pre-Depends: ${shlibs}` (libc6, libbz2, liblzma, libmd0, libselinux1,
libzstd1) and `Depends: tar (>= 1.28-1)`. Ours keeps `Essential: yes`
and has no library Pre-Depends (static); tar's is a Pre-Depends, met by
our tar (Multi-Arch: foreign). Upgrading dpkg is still dpkg's own
unpack-then-configure; nothing in the closure Pre-Depends on a library
that only our dpkg brought. Those libraries stay installed (other arm64
packages need them) but are no longer pulled in for dpkg.
- **apt's dependencies.** Debian's apt Depends on libapt-pkg7.0 (exact
binary version), sqv, base-passwd | adduser and the keyring. Ours:
`base-passwd (>= 3.6.1) | adduser, gpgv, debian-archive-keyring`;
libapt-pkg7.0 and sqv leave the image. Consequence: Debian's
apt-utils (Depends `apt (= 3.0.3)`) is not installable against
3.0.3+gex1; build it here if wanted.
- **OpenPGP.** apt's `sqv` method is built only when the build machine has
sqv; without it apt verifies with its `gpgv` method, which runs
`APT::Key::gpgvcommand` (default `gpgv`). Ours is gnupg2 2.4.7's gpgv,
configured as Debian's gpgv udeb (no npth, gpgsm, dirmngr, ldap, tofu,
sqlite...), with libgcrypt 1.11 (no asm), libgpg-error, libassuan and
libksba static. Debian's keyring files are unchanged
(`/usr/share/keyrings/debian-archive-keyring.gpg`, `Signed-By`).
What translated sqv cost before: in the page it ran three times per
`apt-get update` at 5.9-7.8 s each (Rust, about 20 s per update).
Tampering with a cached InRelease is rejected (BADSIG, exit 100).
- **Hashing.** apt hashes downloads itself (OpenSSL EVP, in-process) and
dpkg checks md5sums in-process; neither execs a hashing program during
install. TLS is apt's http method's OpenSSL, native.
- **debconf.** apt's `DPkg::Pre-Install-Pkgs` hook runs debconf's
dpkg-preconfigure (perl) before every install: about 19 s in the page
for jq. The image clears it (`#clear DPkg::Pre-Install-Pkgs;` in
99gex); postinst scripts still ask (noninteractively) at configure.
- **Later: a Rust target for gexcc** (sqv, ripgrep, fd...): a rustc
target over gexcc's sysroot and wasm32 container. Not needed for apt
once gpgv is native.
**How arm64 packages' dependencies are satisfied.**
- **Tools** (bash, coreutils, dash, grep, sed, tar, gzip, findutils,
diffutils, debianutils, mawk, hostname, ncurses-bin, util-linux,
@@ -617,10 +714,30 @@ arm64 program uninstallable).
tests/gexcc 15/15, a wasm32 bash running Debian's arm64 xz, python3.13
and sqlite3, python's `os.system` running the wasm32 dash) and on the
aarch64 browser shim (tests/wasm32 5/5; tests/gexcc as on x86).
2. **AAPCS64 ELF providers:** the existing ones are SysV x86-64 only. An
aarch64 class (x0-x7, v0-v7, `TPIDR_EL0` TLS for errno) in gex-cgen's
`provider.rs` re-keys every unit: one commit, landed when the
coordinator schedules it.
2. **AAPCS64 ELF providers: ready on `merge/gexcc-a64prov`.**
- `provider.rs` gains an explicit architecture (`PROVIDES ... i386 |
x86_64 | aarch64`, `provider::Arch`, `guest_arch(class32)`):
i386/x86-64 on an x86 build, aarch64 for 64-bit images on an aarch64
build, none on PPC. Before, `for_dll` returned nothing off x86.
- `providers/aapcs64.inc` maps the `gp64_` helpers to AAPCS64 (x0-x7,
then the stack; result in x0/v0; return to x30). `glibc_mem_a64` and
`glibc_math_a64` are the x86-64 bodies with it in front (Rust
`concat!`); `glibc_mem_a64.inc` replaces strcmp with a step-for-step
port of aarch64 glibc's (its result is not the byte difference, and
depends on mutual alignment). A later `GEX_PROVIDE` of an export now
replaces an earlier one.
- **No x86 unit re-keys:** `provider.h`, the x86 sources and the header
are unchanged, so every x86 provider fingerprint (and every unit keyed
on one) is the same. Only arm64 libc/libm translations change.
- Checked on an AArch64-only gexc with tests/elf-prov cross-built for
Debian's arm64 glibc (scratch/CC/a64prov/run-a64.sh): mem, printf,
malloc and fork agree with providers off and on (mem binds 14
addresses in libc, math 8 in libm). math differs only on round()'s
halfway values, where the **translated libm is wrong**: gex-lift
lowers FRINTA (ties away from zero) to wasm `nearest` (ties to even),
so translated `round(0.5)` is 0 and `round(-2.5)` is -2; the provider
gives 1 and -3 as C requires.
- Not ported yet: printf (its va_list code is SysV) and malloc.
3. **arm64-ABI native libraries in the guest's own wasm32 memory** (user,
2026-10-08: no memory64 at run time). Whole-library providers for
arm64 programs need aarch64's LP64 data layout. Approach to evaluate:
@@ -738,6 +855,8 @@ page (web/src/demos.js): fetches `/rootfs/native-debian.tar.zst`
its symbolic links as links (`gex_add_symlink`), and boots `/bin/bash -l`
(`-l -i`) as pid 1 with uid 0, `HOME=/root`, `TERM`, `PATH`. Network: Wisp,
default `wss://anura.pro/`, `?wisp=URL` overrides (`?wisp=` empty: none).
The page holds the one Wisp connection and serves it to every
guest Worker over shared memory (`shims/browser/js/net-bridge.mjs`).
Translated programs run on tier 1 (`?fjit=1` turns the function JIT on).
`?cmd=` runs one command for tests. Under Node the same runtime runs the
tar with `run-node.mjs --tar FILE` (`GEX_UID=0`, `GEX_NETWORK=1` for real
@@ -745,8 +864,8 @@ sockets instead of Wisp); there apt updates from Debian and installs
arm64 packages (§5).
Size budget (zstd, transferred): tier 1 at most 15 MB, tier 1+2 at most
45 MB, everything at most 80 MB. Tier 1 today: 20.4 MB (Debian's arm64
system pieces are most of it: libc6, perl-base, libssl, apt, dpkg).
45 MB, everything at most 80 MB. Tiers 1 and 2 with our dpkg/apt/gpgv
today: 39.3 MB.
If the whole-tar load is too slow, split into a base tar and per-package
tars fetched when first opened (the kernel's demand-fill hook).
+27 -2
View File
@@ -890,6 +890,17 @@ export class Gex {
})() : null,
};
}
/// The program's argv as the shim has it (after any `execve`).
get cmdline() {
const e = this.#shim?.exports;
if (!e?.gex_cmdline) return '';
const max = 160;
const p = e.gex_alloc(max) >>> 0;
const n = e.gex_cmdline(p, max) >>> 0;
const s = utf8(this.#bytes().subarray(p, p + n));
e.gex_free?.(p, max);
return s;
}
/// Which objects this process lifted blocks from, `name=count` separated.
get blocksByObject() {
const e = this.#shim?.exports;
@@ -983,6 +994,12 @@ export class Gex {
const text = utf8(
this.#bytes().subarray(ptr >>> 0, (ptr >>> 0) + (len >>> 0)),
).replace(/\n$/, '');
// `[gex-note]`: worth a line, but nothing went wrong (a large
// allocation that succeeded): a warning, not an error.
if (text.startsWith('[gex-note] ')) {
console.warn(text.slice(11));
return;
}
// `[gex-stack]`: the shim wants to be told who called it (the wasm
// frames are named).
if (text.startsWith('[gex-stack]')) {
@@ -2025,7 +2042,10 @@ export class Gex {
child.brk ?? 0n,
);
if (child.compat) e.gex_set_compat?.(1);
if (child.wasm32) e.gex_wasmprog_adopt?.(BigInt(child.wasm32));
if (child.wasm32) {
if (child.wasm32Base && e.gex_wasmprog_adopt_at) e.gex_wasmprog_adopt_at(BigInt(child.wasm32), child.wasm32Base);
else e.gex_wasmprog_adopt?.(BigInt(child.wasm32));
}
// The parent's block numbering carries on in the child (its low region),
// over slot maps of its own (`dense::child`).
this.#dense = !!(child.opts ?? {}).dense && !!this.#guest.exports.blocks && !!e.gex_set_dense;
@@ -2293,6 +2313,9 @@ export class Gex {
compat: e.gex_compat?.() ?? 0,
// A wasm32 program's entry: the child installs the same module.
wasm32: e.gex_wasmprog_entry?.() ?? 0n,
// And where its module sits in the function table: the child's copy
// of the data holds function pointers for that base.
wasm32Base: e.gex_wasmprog_base?.() ?? 0,
// Where the parent's heap had reached. Without the number the child's
// kernel thinks it has no heap and grows one from zero, over its contexts.
brk: e.gex_fork_brk?.() ?? 0n,
@@ -3836,7 +3859,9 @@ export class Gex {
// own code and stays quiet. A give-up (nothing exited, nothing woke)
// is the runtime stuck; that is 125 -- outside a program's range so
// it is not mistaken for a clean exit -- and it is said out loud.
const stalled = e.gex_exited ? !e.gex_exited() : false;
// (A thread that ended in a process that goes on -- a joined
// worker thread -- is this Worker's end too, not a stall.)
const stalled = (e.gex_exited ? !e.gex_exited() : false) && !(e.gex_ctx_exited?.(ctx) > 0);
if (stalled) {
// Dump every thread's wait-state + pc so a give-up says WHICH
// thread is stuck on WHAT (futex vs socket read vs exited), not
+1
View File
@@ -580,6 +580,7 @@ export class ProcessHost {
// Worker creation, adoption and the wait for a start message. The
// two must be separable -- a cost has been blamed on the wrong side.
`[gex] pid ${pid}: lived ${lived}ms (guest ${m.guestMillis ?? -1}ms)` +
(m.cmdline ? ` {${m.cmdline}}` : '') +
// What the runtime still held on exit. -1 without GEX_HEAP_COUNTERS=1.
(m.heapKib >= 0 ? `, runtime holds ${Math.round(m.heapKib / 1024)} MiB` : '') +
// Dispatch slots the page has left. A budget nothing refills, so
+181
View File
@@ -0,0 +1,181 @@
// The guest's network, reached from a Worker. The page holds the one Wisp
// connection; every guest Worker calls it across a `SharedArrayBuffer` with
// `Atomics.wait`, the way gl-bridge.mjs reaches the page's GL device.
//
// Why not a WebSocket in each Worker, as before: its bytes arrive as messages
// on that Worker's event loop, and a native wasm32 program waiting in a
// syscall never gives its Worker's event loop back (it is polled in place;
// shims/browser/src/lib.rs). Nothing it waited for could ever arrive. The page's
// event loop is free, so the bytes land there, and the seven calls below never
// wait on anything but the page answering.
const STATE = 0; // 0 idle, 1 request, 2 reply
const OP = 1;
const A0 = 2;
const A1 = 3;
const A2 = 4;
const RESULT = 5;
// One channel and every process on it: claimed per call, holding the
// claimant's own number (a stuck claim then says whose it is).
const LOCK = 6;
const IDLE = 0;
const REQUEST = 1;
const REPLY = 2;
const OPEN = 1;
const CONNECT = 2;
const STATUS = 3;
const SEND = 4;
const RECV = 5;
const AVAILABLE = 6;
const CLOSE = 7;
/** Bytes one send or recv carries. A larger send is cut (it returns the count,
* as a socket does); a recv takes at most this much. */
const DATA_BYTES = 1 << 20;
/** The control words and the data buffer a bridge is made of, or null where
* the page cannot serve one (no `Atomics.waitAsync`). */
export function makeNetChannel() {
if (typeof Atomics.waitAsync !== 'function' || typeof SharedArrayBuffer !== 'function') return null;
return {
control: new SharedArrayBuffer(64),
data: new SharedArrayBuffer(DATA_BYTES),
};
}
/** The page's half: run `net` (a `WispNetwork`) when a Worker asks. Returns a
* function that stops it. */
export function serveNet(net, channel) {
const ctl = new Int32Array(channel.control);
const data = new Uint8Array(channel.data);
const dec = new TextDecoder();
let active = true;
const serve = () => {
const op = ctl[OP];
const id = ctl[A0];
let r = 0;
try {
switch (op) {
case OPEN: r = net.open(); break;
case CONNECT:
// `slice`: the decoder refuses a view of shared memory.
net.connect(id, dec.decode(data.slice(0, ctl[A1])), ctl[A2]);
break;
case STATUS: r = net.status(id); break;
case SEND: r = net.send(id, data.subarray(0, ctl[A1])); break;
case RECV: r = net.recv(id, data.subarray(0, Math.min(ctl[A1], DATA_BYTES))); break;
case AVAILABLE: r = net.available(id) ? 1 : 0; break;
case CLOSE: net.close(id); break;
default: r = -1;
}
} catch (e) {
// It must still answer, or the Worker sits in its syscall until the
// timeout.
console.error('[net] the network threw', e);
r = -2;
}
ctl[RESULT] = r;
Atomics.store(ctl, STATE, REPLY);
Atomics.notify(ctl, STATE);
};
(async () => {
while (active) {
const state = Atomics.load(ctl, STATE);
if (state === REQUEST) {
serve();
continue;
}
const waiting = Atomics.waitAsync(ctl, STATE, state, 1000);
if (waiting.async) await waiting.value;
}
})();
return () => {
active = false;
Atomics.notify(ctl, STATE);
};
}
/** The Worker's half: the seven calls `Gex.useNetwork` wants. */
export class NetProxy {
#ctl;
#data;
#enc = new TextEncoder();
#me;
constructor(channel, me = 1) {
this.#ctl = new Int32Array(channel.control);
this.#data = new Uint8Array(channel.data);
this.#me = me || 1;
}
#call(op, a0 = 0, a1 = 0, a2 = 0) {
const ctl = this.#ctl;
// A holder that has not let go in five seconds died inside its call.
const until = Date.now() + 5000;
for (;;) {
const held = Atomics.compareExchange(ctl, LOCK, 0, this.#me);
if (held === 0) break;
Atomics.wait(ctl, LOCK, held, 50);
if (Date.now() > until) {
console.warn(`[net] taking the channel from ${held}`);
Atomics.store(ctl, LOCK, this.#me);
break;
}
}
return { a0, a1, a2, op };
}
#run({ op, a0, a1, a2 }, after) {
const ctl = this.#ctl;
try {
ctl[OP] = op;
ctl[A0] = a0;
ctl[A1] = a1;
ctl[A2] = a2;
Atomics.store(ctl, STATE, REQUEST);
Atomics.notify(ctl, STATE);
while (Atomics.load(ctl, STATE) === REQUEST) {
if (Atomics.wait(ctl, STATE, REQUEST, 30000) === 'timed-out') {
throw new Error('the page did not answer a network call in thirty seconds');
}
}
const r = ctl[RESULT];
after?.(r);
Atomics.store(ctl, STATE, IDLE);
return r;
} finally {
Atomics.store(ctl, LOCK, 0);
Atomics.notify(ctl, LOCK);
}
}
open() { return this.#run(this.#call(OPEN)); }
connect(id, host, port) {
const req = this.#call(CONNECT, id, 0, port);
const b = this.#enc.encode(host);
this.#data.set(b.subarray(0, DATA_BYTES), 0);
req.a1 = Math.min(b.length, DATA_BYTES);
this.#run(req);
}
status(id) { return this.#run(this.#call(STATUS, id)); }
send(id, bytes) {
const n = Math.min(bytes.length, DATA_BYTES);
const req = this.#call(SEND, id, n);
this.#data.set(bytes.subarray(0, n), 0);
return this.#run(req);
}
recv(id, out) {
return this.#run(this.#call(RECV, id, Math.min(out.length, DATA_BYTES)), (r) => {
if (r > 0) out.set(this.#data.subarray(0, r), 0);
});
}
available(id) { return this.#run(this.#call(AVAILABLE, id)) === 1; }
close(id) { this.#run(this.#call(CLOSE, id)); }
}
+8 -1
View File
@@ -567,7 +567,13 @@ async function handle(msg) {
const { NodeNetwork } = await import('./net-node.mjs');
gex.useNetwork(new NodeNetwork());
}
if (msg.wispUrl) {
if (msg.netChannel) {
// The page's Wisp connection, called across shared memory (net-bridge.mjs):
// a wasm32 program never hands this Worker's event loop back, so a
// WebSocket of its own here would never deliver.
const { NetProxy } = await import('./net-bridge.mjs');
gex.useNetwork(new NetProxy(msg.netChannel, msg.child?.pid ?? msg.pid ?? 1));
} else if (msg.wispUrl) {
// The site's Wisp client, imported only when a proxy was named.
const { WispNetwork } = await import('../../../web/src/net.js');
const net = new WispNetwork(msg.wispUrl, (e) => port.post({ kind: 'net', event: e }));
@@ -765,6 +771,7 @@ async function handle(msg) {
instantiateMs: gex.instantiateMillis,
growMs: gex.growMillis,
byObject: gex.blocksByObject,
cmdline: gex.cmdline,
// How this process waited, by kind. See `Gex.waits`.
waits: gex.waits,
loader: gex.loader,
+39 -8
View File
@@ -29,10 +29,13 @@ struct Prog {
}
static PROGS: Mutex<Vec<Prog>> = Mutex::new(Vec::new());
/// The program to publish (entry, whether its data is fresh).
static STAGED: Mutex<Option<(u64, bool)>> = Mutex::new(None);
/// The program to publish (entry, whether its data is fresh, the table base
/// it must have: a fork's child's, the parent's).
static STAGED: Mutex<Option<(u64, bool, Option<u32>)>> = Mutex::new(None);
/// This process's program's entry while it is a wasm32 one, for a fork.
static CURRENT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// And its module's `__table_base`.
static CURRENT_BASE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
pub(crate) const MAGIC: &[u8; 8] = b"GEXWASM\0";
@@ -52,7 +55,7 @@ pub(crate) fn is_wasm32(program: &gex_ld::Program) -> bool {
/// it runs if it is a wasm32 one.
pub(crate) fn stage(program: Option<&gex_ld::Program>) {
CURRENT.store(0, Ordering::Relaxed);
*lock(&STAGED) = program.filter(|p| is_wasm32(p)).map(|p| (p.exe.entry, true));
*lock(&STAGED) = program.filter(|p| is_wasm32(p)).map(|p| (p.exe.entry, true, None));
}
/// A fork's child of a wasm32 program at `entry`: the same module, over the
@@ -60,10 +63,26 @@ pub(crate) fn stage(program: Option<&gex_ld::Program>) {
#[no_mangle]
pub extern "C" fn gex_wasmprog_adopt(entry: u64) {
if entry != 0 {
*lock(&STAGED) = Some((entry, false));
*lock(&STAGED) = Some((entry, false, None));
}
}
/// [`gex_wasmprog_adopt`] with the parent's table base
/// ([`gex_wasmprog_base`]): the data the child copied holds function
/// pointers for that base, so the module goes there.
#[no_mangle]
pub extern "C" fn gex_wasmprog_adopt_at(entry: u64, base: u32) {
if entry != 0 {
*lock(&STAGED) = Some((entry, false, (base != 0).then_some(base)));
}
}
/// The running wasm32 program's `__table_base` (the fork record's).
#[no_mangle]
pub extern "C" fn gex_wasmprog_base() -> u32 {
if CURRENT.load(Ordering::Relaxed) == 0 { 0 } else { CURRENT_BASE.load(Ordering::Relaxed) }
}
/// The running program's entry if it is a wasm32 one (the fork record's).
#[no_mangle]
pub extern "C" fn gex_wasmprog_entry() -> u64 {
@@ -73,8 +92,8 @@ pub extern "C" fn gex_wasmprog_entry() -> u64 {
/// Publish the staged program: its provider module number, or -1.
#[no_mangle]
pub extern "C" fn gex_wasmprog_serve() -> i32 {
let Some((entry, relocate)) = lock(&STAGED).take() else { return -1 };
match publish(entry, relocate) {
let Some((entry, relocate, at)) = lock(&STAGED).take() else { return -1 };
match publish(entry, relocate, at) {
Ok(n) => n as i32,
Err(e) => {
crate::warn_line(&format!("gex: wasm32 program: {e}\n"));
@@ -84,7 +103,7 @@ pub extern "C" fn gex_wasmprog_serve() -> i32 {
}
}
fn publish(entry: u64, relocate: bool) -> Result<u32, String> {
fn publish(entry: u64, relocate: bool, want_base: Option<u32>) -> Result<u32, String> {
let mem = crate::guest_bytes();
let at = entry.wrapping_sub(0x40) as usize;
let h = mem.get(at..at + 36).ok_or("header outside memory")?;
@@ -101,7 +120,19 @@ fn publish(entry: u64, relocate: bool) -> Result<u32, String> {
let slot = |n: &str| names.iter().position(|s| s.as_deref() == Some(n)).map(|k| k as u32).ok_or(format!("{n} is not in its table"));
let pairs = vec![(entry, slot("__gex_start")?), (thread_entry, slot("__gex_thread_start")?)];
let number = lock(&PROVIDERS).len() as u32;
ftab(|m| m.publish(names.len() as u32, PROVIDER | number, Tier::Unit, &pairs));
let install = ftab(|m| match want_base {
Some(base) => {
let (n, ok) = m.publish_at(base, names.len() as u32, PROVIDER | number, Tier::Unit, &pairs);
if !ok {
crate::warn_line(&format!(
"gex: wasm32 fork child: table base {base} is taken, its function pointers will be wrong\n"
));
}
n
}
None => m.publish(names.len() as u32, PROVIDER | number, Tier::Unit, &pairs),
});
CURRENT_BASE.store(ftab(|m| m.install(install).base), Ordering::Relaxed);
let addrs: Vec<u64> = pairs.iter().map(|&(a, _)| a).collect();
for &a in &addrs {
gex_kernel::ftab::fcache_clear(crate::guest_bytes(), a);
+92 -22
View File
@@ -570,7 +570,15 @@ fn signal_note(what: &str, caller: i32, target: i32, signo: u32) {
if signo == 0 || signo == 28 || SAID.fetch_add(1, Ordering::Relaxed) >= 24 {
return;
}
warn_line(&format!("[sig] pid {caller} sends signal {signo} to {what} {target}\n"));
let line = format!("[sig] pid {caller} sends signal {signo} to {what} {target}\n");
// SIGINT, SIGKILL and SIGTERM to another process are a parent ending a
// child on purpose (apt stops each of its methods with SIGINT): noted,
// not warned. A hangup stays a warning; that is a session coming apart.
if matches!(signo, 2 | 9 | 15) && caller != target {
trace_line(&line);
} else {
warn_line(&line);
}
}
extern "C" fn r_kill(caller: i32, target: i32, signo: u32) -> i32 {
@@ -923,8 +931,9 @@ fn pty_note(what: &str, id: i32, master: i32, pid: i32) {
return;
}
let counts = unsafe { k_pty_counts(id) };
// A note (console.warn), not an error: nothing is wrong when it is said.
warn_line(&format!(
"[pty] pid {pid} {what}s the {} of pty {id}: {} master(s), {} slave(s) now\n",
"[gex-note] [pty] pid {pid} {what} the {} of pty {id}: {} master(s), {} slave(s) now\n",
if master != 0 { "master" } else { "slave" },
counts >> 16,
counts & 0xffff
@@ -933,21 +942,21 @@ fn pty_note(what: &str, id: i32, master: i32, pid: i32) {
extern "C" fn rpty_open_master() -> i32 {
let r = unsafe { k_pty_open_master(my_pid()) };
pty_note("open", r >> 16, 1, my_pid());
pty_note("opens", r >> 16, 1, my_pid());
r
}
extern "C" fn rpty_open_slave(ptn: i32) -> i32 {
let r = unsafe { k_pty_open_slave(ptn, my_pid()) };
pty_note("open", ptn, 0, my_pid());
pty_note("opens", ptn, 0, my_pid());
r
}
extern "C" fn rpty_open_end(id: i32, master: i32) {
unsafe { k_pty_open_end(id, master, my_pid()) };
pty_note("inherit", id, master, my_pid());
pty_note("inherits", id, master, my_pid());
}
extern "C" fn rpty_close_end(id: i32, master: i32) {
unsafe { k_pty_close_end(id, master, my_pid()) };
pty_note("close", id, master, my_pid());
pty_note("closes", id, master, my_pid());
}
extern "C" fn rpty_read(id: i32, master: i32, at: gex_kernel::PAddr, len: u32) -> i32 {
let r = unsafe { k_pty_read(id, master, at, len) };
@@ -1913,6 +1922,18 @@ pub extern "C" fn gex_dump_threads() {
});
}
/// Whether the thread whose context is at `ctx` has exited: a Worker whose
/// own thread ended while the process goes on (a wasm32 program's worker
/// thread, joined) is done, not stuck.
#[no_mangle]
pub extern "C" fn gex_ctx_exited(ctx: u32) -> i32 {
with_host(|h| {
h.threads
.iter()
.any(|t| t.kstate.ctx_addr == ctx as u64 && matches!(t.waiting, Waiting::Exited)) as i32
})
}
/// How many of an object's stored units one dispatch miss may take, merged
/// into a single module. See the use site in the miss path.
static PER_MISS_N: std::sync::atomic::AtomicUsize =
@@ -2272,25 +2293,29 @@ unsafe impl<const BIG: bool> talc::source::Source for ShimRegion<BIG> {
// The first few, and only well above a chunk: this is a real event worth
// a line, not a stream. A run that made one of these per miss printed
// two hundred of them and drowned everything else in the console.
use core::fmt::Write as _;
struct Buf([u8; 192], usize);
impl core::fmt::Write for Buf {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let room = self.0.len() - self.1;
let n = s.len().min(room);
self.0[self.1..self.1 + n].copy_from_slice(&s.as_bytes()[..n]);
self.1 += n;
Ok(())
}
}
static SAID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
// Said once the span is in hand: a large single allocation that
// succeeds is legitimate (apt maps its 42 MiB package list), so it is
// a note; one the arena cannot serve is an error.
let mut said = None;
if _layout.size() as u64 > 2 * CHUNK
&& SAID.fetch_add(1, std::sync::atomic::Ordering::Relaxed) < 4
{
use core::fmt::Write as _;
struct Buf([u8; 160], usize);
impl core::fmt::Write for Buf {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
let room = self.0.len() - self.1;
let n = s.len().min(room);
self.0[self.1..self.1 + n].copy_from_slice(&s.as_bytes()[..n]);
self.1 += n;
Ok(())
}
}
let mut b = Buf([0; 160], 0);
let mut b = Buf([0; 192], 0);
let _ = write!(
b,
"[gex:{}] a single {} MiB allocation takes a span of its own from \
"[gex-note] [gex:{}] a single {} MiB allocation takes a span of its own from \
the arena, which never gives one back (in {}, syscall {}, \
{} refills so far)\n",
my_pid(),
@@ -2299,8 +2324,7 @@ unsafe impl<const BIG: bool> talc::source::Source for ShimRegion<BIG> {
LAST_SYSCALL.load(std::sync::atomic::Ordering::Relaxed) as i32,
HEAP_REFILLS.load(std::sync::atomic::Ordering::Relaxed),
);
// SAFETY: `warn` reads the bytes and keeps none of them.
unsafe { warn(b.0.as_ptr() as u32, b.1 as u32) };
said = Some(b);
}
arena_note(1, len);
// Count of refills, plus the biggest single layout that forced one --
@@ -2349,7 +2373,21 @@ unsafe impl<const BIG: bool> talc::source::Source for ShimRegion<BIG> {
);
}
}
let base = arena_take(len).ok_or(())? as *mut u8;
let Some(base) = arena_take(len) else {
if let Some(mut b) = said {
// Not a note any more: the arena could not serve it. Drop the
// `[gex-note] ` prefix (11 bytes) so it reaches console.error.
let _ = write!(b, " ... and the arena could not serve it\n");
// SAFETY: `warn` reads the bytes and keeps none of them.
unsafe { warn(b.0.as_ptr().add(11) as u32, (b.1 - 11) as u32) };
}
return Err(());
};
let base = base as *mut u8;
if let Some(b) = said {
// SAFETY: as above.
unsafe { warn(b.0.as_ptr() as u32, b.1 as u32) };
}
memmap::claim(memmap::Kind::Heap, 0, base as u64, base as u64 + len, if BIG { "heap span (big)" } else { "heap span" });
// Extend the last span when this one abuts it, rather than claiming a
// separate one: talc cannot serve a request larger than any single span
@@ -5794,6 +5832,25 @@ pub extern "C" fn gex_recent_syscalls(out: u32, max: u32) -> u32 {
n as u32
}
/// The running program's argv, space-separated and capped, for the
/// per-process line (which program a cost belongs to).
#[no_mangle]
pub extern "C" fn gex_cmdline(out: u32, max: u32) -> u32 {
let text = with_host(|h| {
let mut t = h.kernel.argv.join(" ");
if t.is_empty() {
t = h.kernel.exe_path.clone();
}
t
});
let mut n = text.len().min(max as usize);
while n > 0 && !text.is_char_boundary(n) {
n -= 1;
}
shim_slice_mut(out, n as u32).copy_from_slice(&text.as_bytes()[..n]);
n as u32
}
/// Nanoseconds spent inside syscalls, and how many there were. Always kept.
static SYS_TOTAL_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static SYS_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
@@ -6384,11 +6441,13 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
// Who sent it, if the global kernel knows: "killed by signal 1"
// is true of a hangup, a terminal's line discipline and any
// guest's `kill`, and they are not the same bug.
let mut sent = None;
let from = if unsafe { k_present() } != 0 {
let v = unsafe { k_last_kill() };
let caller = (v >> 40) as i32;
let target = ((v >> 8) & 0xffff_ffff) as i32;
let signo = (v & 0xff) as u32;
sent = Some((caller, target, signo));
format!(
" (signal #{}: {signo} from pid {caller} to {target})",
unsafe { k_last_kill_seq() }
@@ -6396,6 +6455,16 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
} else {
String::new()
};
// Ended on purpose by another process (SIGINT, SIGKILL or
// SIGTERM it sent this one): one quiet line, not the report.
// apt ends every method this way after a clean run.
let pid = h.kernel.pid;
if matches!(sent, Some((caller, target, signo))
if signo == signal as u32 && matches!(signo, 2 | 9 | 15)
&& caller != pid && (target == pid || target == -pid || target == 0))
{
trace_line(&format!("[gex] pid {pid} ended by signal {signal}{from}\n"));
} else {
warn_line(&format!(
"[gex] pid {} killed by signal {}{from} at pc={at:#x}\n stderr: {}\n threads:{}\n \
last calls:{}\n address space:{}\n futexes:{}",
@@ -6424,6 +6493,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
mem_recent(),
ftx_recent(),
));
}
exit_process(h, -(signal as i32))
}
SyscallOutcome::SpawnThread {
+150
View File
@@ -2175,6 +2175,153 @@ fn take_kernel(cx: &SpawnContext) -> Kernel {
std::mem::replace(&mut *guard, Kernel::new(0, 0, String::new()))
}
/// `GEXC_HOST_EXEC` (gexc's own environment, `:`-separated): programs a
/// guest's `execve` runs on the host instead -- an exact path, or a
/// directory ending in `/` for everything in it. For builds whose own tools
/// are guest programs that start the build machine's tools: perl's make_ext
/// is miniperl (a wasm32 program) running `make` and the C compiler, which
/// translated are either far too slow or do not load at all (clang's 100 MB
/// library). Paths are the guest's, so this means something only when the
/// guest sees the host's tree (no sysroot), as gexcc's runner does.
/// Opt-in and for build tooling only: no demo, image or test sets it, and
/// the browser shim has no equivalent.
///
/// The process stays: it waits for the host program and exits as it did.
/// Descriptors 0-2 are bridged: a stream the guest got from gexc's own stdio
/// is the host's, anything else (a pipe, a file) is copied through a host
/// pipe. Nothing above 2 is passed (make's jobserver falls back to -j1).
fn host_exec(req: &ExecRequest, kernel: &Kernel) -> Result<Option<Exit>> {
use std::io::{Read, Write};
use std::process::{Command, Stdio};
let Ok(list) = std::env::var("GEXC_HOST_EXEC") else {
return Ok(None);
};
let path = if req.path.starts_with('/') {
req.path.clone()
} else {
format!("{}/{}", kernel.vfs.cwd.trim_end_matches('/'), req.path)
};
let listed = |p: &str| {
list.split(':').filter(|e| !e.is_empty()).any(|e| {
if e.ends_with('/') {
p.starts_with(e) && !p[e.len()..].contains('/')
} else {
p == e
}
})
};
// The kernel has followed a `#!` line by now, so a listed script (gexcc
// is Python) arrives as its interpreter with the script's path among the
// first arguments: run the script itself, so its interpreter is the
// host's too.
let script = (1..req.argv.len().min(3)).find(|&i| {
let a = &req.argv[i];
a.starts_with('/') && listed(a) && kernel.read_file(a).is_some_and(|b| b.starts_with(b"#!"))
});
let (path, argv): (String, &[String]) = match script {
Some(i) => (req.argv[i].clone(), &req.argv[i..]),
None if listed(&path) => (path, &req.argv[..]),
None => return Ok(None),
};
let mut cmd = Command::new(&path);
if let Some(a0) = argv.first() {
use std::os::unix::process::CommandExt;
cmd.arg0(a0);
}
cmd.args(argv.iter().skip(1));
cmd.env_clear();
for kv in &req.envp {
if let Some((k, v)) = kv.split_once('=') {
cmd.env(k, v);
}
}
cmd.current_dir(&kernel.vfs.cwd);
// For a launcher that limits how many gexcs run at once (gexcc-exec): a
// gexc started below this one must not wait for a slot this one holds.
cmd.env("GEXC_HOSTED", "1");
// A guest stream that is gexc's own stdio is passed as is; the rest piped.
let files: Vec<Option<Arc<gex_kernel::file::OpenFile>>> = (0..3).map(|fd| kernel.fds.get(fd).ok()).collect();
let is_std = |f: &Option<Arc<gex_kernel::file::OpenFile>>, want: &str| {
f.as_ref().is_some_and(|f| format!("{:?}", f.handle).contains(want))
};
let std_names = ["stream: In", "stream: Out", "stream: Err"];
let mut piped = [false; 3];
for (i, f) in files.iter().enumerate() {
let st = if f.is_none() {
Stdio::null()
} else if is_std(f, std_names[i]) {
Stdio::inherit()
} else {
piped[i] = true;
Stdio::piped()
};
match i {
0 => cmd.stdin(st),
1 => cmd.stdout(st),
_ => cmd.stderr(st),
};
}
let mut child = cmd
.spawn()
.with_context(|| format!("execve: running {path} on the host (GEXC_HOST_EXEC)"))?;
let mut threads = Vec::new();
if piped[0] {
let (mut to, from) = (child.stdin.take().unwrap(), files[0].clone().unwrap());
// Detached: a guest stdin that never ends must not hold the exit up.
std::thread::spawn(move || {
let mut buf = vec![0u8; 65536];
loop {
match from.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if to.write_all(&buf[..n as usize]).is_err() {
break;
}
}
Err(gex_kernel::errno::Errno::EAGAIN) => std::thread::sleep(std::time::Duration::from_millis(2)),
Err(_) => break,
}
}
});
}
let out: [Option<Box<dyn Read + Send>>; 2] = [
child.stdout.take().map(|r| Box::new(r) as Box<dyn Read + Send>),
child.stderr.take().map(|r| Box::new(r) as Box<dyn Read + Send>),
];
for (k, r) in out.into_iter().enumerate() {
let (Some(mut r), Some(to)) = (r, files[k + 1].clone()) else { continue };
if !piped[k + 1] {
continue;
}
threads.push(std::thread::spawn(move || {
let mut buf = vec![0u8; 65536];
while let Ok(n) = r.read(&mut buf) {
if n == 0 {
break;
}
let mut at = 0;
while at < n {
match to.write(&buf[at..n]) {
Ok(w) => at += w as usize,
Err(gex_kernel::errno::Errno::EAGAIN) => std::thread::sleep(std::time::Duration::from_millis(2)),
Err(_) => return,
}
}
}
}));
}
let status = child.wait()?;
for t in threads {
let _ = t.join();
}
use std::os::unix::process::ExitStatusExt;
Ok(Some(match (status.code(), status.signal()) {
(Some(c), _) => Exit::Status(c),
(None, Some(sig)) => Exit::Signal { signal: sig, pc: 0, detail: format!("{path} on the host"), internal: false },
_ => Exit::Status(1),
}))
}
/// Load and run the program `execve` asked for, keeping the process.
#[allow(clippy::too_many_arguments)]
fn exec_program(
@@ -2185,6 +2332,9 @@ fn exec_program(
proc_state: Arc<Mutex<ProcState>>,
wayland: Option<wayland::Wayland>,
) -> Result<(Exit, Stats)> {
if let Some(exit) = host_exec(&req, &kernel)? {
return Ok((exit, Stats::default()));
}
// The file comes from the guest's namespace, not the host's, so a sysroot
// is honoured and `/bin/sh` means the guest's.
let bytes = kernel
+10
View File
@@ -111,6 +111,16 @@ int main(void) {
y[i] = (char)0xf0;
mix((unsigned)strcmp(x, y));
mix((unsigned)strcmp(y, x));
/* Low bits only (aarch64 glibc's result is then not the
* bytes' difference), at another mutual alignment too. */
y[i] = 'b';
mix((unsigned)strcmp(x, y));
mix((unsigned)strcmp(y, x));
if (i > 0) {
mix((unsigned)strcmp(x + 1, y + 1));
mix((unsigned)strcmp(x + i, y + 1));
mix((unsigned)strcmp(y + 1, x + i));
}
y[i] = 'a';
}
if (len > 0) {
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
"""apt's programs and methods as one gexcc program (docs/gexcc.md §6.1a).
aptmulti.py BUILDDIR OUT
Each of apt's 20 programs links all of libapt-pkg (its static registries
reach most of it): about 10 MB of wasm each, 100 MB for the set, and a
separate module for the page to compile per program. Here each program's
main source is compiled again with `main` renamed, a dispatcher picks one by
the name it was run as (busybox's way), and the lot links once. BUILDDIR is
apt's CMake build tree (configured with CMAKE_EXPORT_COMPILE_COMMANDS);
the link line comes from the http method's (it has every library).
Installed as /usr/lib/apt/apt-multi with every program and method a symlink.
"""
import json, os, re, shlex, subprocess, sys
# (id, source under the source tree, what argv[0] names it)
PROGRAMS = [
('apt', 'cmdline/apt.cc', ['bin/apt']),
('apt_cache', 'cmdline/apt-cache.cc', ['bin/apt-cache']),
('apt_get', 'cmdline/apt-get.cc', ['bin/apt-get']),
('apt_mark', 'cmdline/apt-mark.cc', ['bin/apt-mark']),
('apt_config', 'cmdline/apt-config.cc', ['bin/apt-config']),
('apt_cdrom', 'cmdline/apt-cdrom.cc', ['bin/apt-cdrom']),
('apt_helper', 'cmdline/apt-helper.cc', ['apt-helper']),
('apt_extracttemplates', 'cmdline/apt-extracttemplates.cc', ['apt-extracttemplates']),
('internal_solver', 'cmdline/apt-internal-solver.cc', ['solvers/apt', 'solvers/solver3']),
('dump_solver', 'cmdline/apt-dump-solver.cc', ['solvers/dump', 'planners/dump']),
('internal_planner', 'cmdline/apt-internal-planner.cc', ['planners/apt']),
('m_file', 'methods/file.cc', ['methods/file']),
('m_copy', 'methods/copy.cc', ['methods/copy']),
('m_store', 'methods/store.cc', ['methods/store']),
('m_gpgv', 'methods/gpgv.cc', ['methods/gpgv']),
('m_cdrom', 'methods/cdrom.cc', ['methods/cdrom']),
('m_http', 'methods/http.cc', ['methods/http', 'methods/https']),
('m_mirror', 'methods/mirror.cc', ['methods/mirror', 'methods/mirror+http', 'methods/mirror+https',
'methods/mirror+file', 'methods/mirror+copy']),
('m_rred', 'methods/rred.cc', ['methods/rred']),
]
# objects the http method links besides its main source
HTTP_EXTRA = ['methods/CMakeFiles/http.dir/basehttp.cc.o', 'methods/CMakeFiles/connectlib.dir/connect.cc.o',
'methods/CMakeFiles/connectlib.dir/rfc2553emu.cc.o']
DISPATCH = r'''
// apt-multi: which of apt's programs to run, from the name it was run as
#include <cstring>
#include <cstdio>
%(decls)s
static const struct { const char *name; int (*fn)(int, const char **); } table[] = {
%(rows)s
};
int main(int argc, const char **argv) {
const char *a0 = argc > 0 ? argv[0] : "";
size_t n = strlen(a0);
// longest suffix match on a path component boundary ("solvers/apt" before "apt")
int best = -1; size_t bestlen = 0;
for (size_t i = 0; i < sizeof table / sizeof *table; i++) {
size_t l = strlen(table[i].name);
if (l <= n && strcmp(a0 + n - l, table[i].name) == 0 && (l == n || a0[n - l - 1] == '/') && l > bestlen) {
best = (int)i; bestlen = l;
}
}
if (best < 0) {
fprintf(stderr, "apt-multi: run as one of apt's programs, not as %%s\n", a0);
return 127;
}
return table[best].fn(argc, argv);
}
'''
def main():
build, out = sys.argv[1], sys.argv[2]
src = os.path.dirname(os.path.abspath(build.rstrip('/')))
cc = {os.path.normpath(e['file']): e for e in json.load(open(os.path.join(build, 'compile_commands.json')))}
mdir = os.path.join(build, 'multi')
os.makedirs(mdir, exist_ok=True)
objs, decls, rows = [], [], []
for ident, rel, names in PROGRAMS:
e = cc[os.path.normpath(os.path.join(src, rel))]
args = shlex.split(e['command'])
i = args.index('-o')
o = os.path.join(mdir, ident + '.o')
args[i + 1] = o
args.insert(1, f'-Dmain=apt_multi_{ident}')
r = subprocess.run(args, cwd=e['directory'])
if r.returncode:
sys.exit(f'aptmulti: {rel} failed')
objs.append(o)
# the renamed main keeps its C++ signature (and so its mangled name):
# declare it as written, call it through an adapter
sig = re.search(r'^int main\((.*?)\)', open(os.path.join(src, rel)).read(), re.M)[1]
if not sig.strip():
decls.append(f'int apt_multi_{ident}();\n'
f'static int run_{ident}(int, const char **) {{ return apt_multi_{ident}(); }}')
elif 'const' in sig:
decls.append(f'int apt_multi_{ident}(int, const char **);\n'
f'static int run_{ident}(int c, const char **v) {{ return apt_multi_{ident}(c, v); }}')
else:
decls.append(f'int apt_multi_{ident}(int, char **);\n'
f'static int run_{ident}(int c, const char **v) {{ return apt_multi_{ident}(c, (char **)v); }}')
# programs in /usr/bin are run by name from PATH: match the name alone
rows += [f' {{"{n[4:] if n.startswith("bin/") else n}", run_{ident}}},' for n in names]
d = os.path.join(mdir, 'dispatch.cc')
open(d, 'w').write(DISPATCH % {'decls': '\n'.join(decls), 'rows': '\n'.join(rows)})
link = shlex.split(open(os.path.join(build, 'methods/CMakeFiles/http.dir/link.txt')).read())
i = link.index('-o')
cxx, rest = link[0], link[i + 2:]
libs = [a for a in rest if not a.startswith('-Wl,-rpath')]
# libapt-private before libapt-pkg (the cmdline programs need it)
libs = [os.path.join(build, 'apt-private/libapt-private.a') if a == '../apt-pkg/libapt-pkg.a' else a for a in libs]
libs.insert(libs.index(os.path.join(build, 'apt-private/libapt-private.a')) + 1,
os.path.join(build, 'apt-pkg/libapt-pkg.a'))
cflags = [a for a in link[1:i] if a.startswith('-') and not a.endswith('.o')]
cmd = [cxx] + cflags + [d] + objs + [os.path.join(build, x) for x in HTTP_EXTRA] + ['-o', out] + libs
r = subprocess.run(cmd, cwd=os.path.join(build, 'methods'))
if r.returncode:
sys.exit('aptmulti: link failed')
for ident, rel, names in PROGRAMS:
for n in names:
print(n)
if __name__ == '__main__':
main()
+69 -1
View File
@@ -2,7 +2,10 @@
# binary packages. A file goes to the first matching line; BINPKG `-` drops
# it. A source package with no lines becomes one package of its own name.
# `depends BINPKG DEPENDS...` adds a Depends field (Debian's data packages
# ours use: architecture-independent content, Multi-Arch: foreign).
# ours use: architecture-independent content, Multi-Arch: foreign);
# `predepends BINPKG ...` a Pre-Depends field; `arch BINPKG ARCH` labels a
# package with another architecture (dpkg and apt: arm64, docs/gexcc.md
# §6.1a). The rest of a depends/predepends line is one relation.
zlib - *
libmd - *
readline - *
@@ -20,3 +23,68 @@ sysvinit sysvinit-utils *
file file *
depends file libmagic-mgc
depends vim vim-common
libonig - *
libevent - *
expat - *
make-dfsg make *
lua5.4 lua5.4 *
lz4 lz4 *
mpdecimal - *
python3.13 python3.13 *
openssl openssl *
curl curl *
git - usr/lib/git-core/git-http-push usr/lib/git-core/git-imap-send usr/lib/git-core/git-http-fetch usr/lib/git-core/git-daemon usr/lib/git-core/git-http-backend usr/lib/git-core/scalar usr/bin/scalar usr/lib/git-core/git-shell usr/bin/git-shell
git git *
xxhash - *
dpkg dpkg usr/bin/* usr/sbin/* usr/share/dpkg/* etc/* var/* usr/share/man/*
dpkg - *
arch dpkg arm64
predepends dpkg tar (>= 1.28-1)
apt apt *
arch apt arm64
depends apt base-passwd (>= 3.6.1) | adduser
depends apt gpgv
depends apt debian-archive-keyring
libgpg-error - *
libgcrypt20 - *
libassuan - *
libksba - *
npth - *
gnupg2 gpgv *
# gui: libraries static in the sysroot; fontconfig (its tools and /etc/fonts)
# and foot as packages, with Debian's fonts and keyboard data
wayland - *
wayland-protocols - *
libxkbcommon - *
pixman - *
freetype - *
utf8proc - *
tllist - *
fcft - *
fontconfig - usr/lib/* usr/include/*
fontconfig fontconfig *
depends fontconfig fonts-dejavu-core
foot foot *
depends foot xkb-data
depends foot fontconfig
depends foot libx11-data
# X11 clients and SDL: static libraries; the locale and compose data are
# Debian's libx11-data. Chocolate Doom's engine, on Debian's Freedoom WADs
xorgproto - *
xtrans - *
xcb-proto - *
libxau - *
libxdmcp - *
libxcb - *
libx11 - *
libxext - *
libsdl2 - *
libsdl2-net - *
libsdl2-mixer - *
chocolate-doom chocolate-doom usr/bin/chocolate-doom usr/bin/chocolate-doom-setup usr/share/applications/org.chocolate_doom.Doom* usr/share/icons/* usr/share/metainfo/org.chocolate_doom.Doom*
chocolate-doom - *
depends chocolate-doom freedoom
# perl: built and working (XS extensions static), not shipped yet: Debian's
# arm64 perl-base stays until ours provides perlapi-5.40.1 and the layout
# Debian's arm64 Perl packages expect
perl - *
+418
View File
@@ -0,0 +1,418 @@
/* libffi for gexcc's wasm32 (not emscripten): ffi_call in plain C.
Replaces src/wasm32/ffi.c, whose emscripten port makes its calls in
JavaScript (EM_JS); a gexcc program has no JavaScript of its own and runs
natively under wasmtime as well as in the page.
A wasm call_indirect names the callee's exact signature, so the call goes
through one of the trampolines gen.py writes (tramp.inc), chosen by the
signature the cif lowers to. Lowering follows clang's wasm32 C ABI:
- integers up to 32 bits, pointers: i32, extended by the caller;
64-bit integers: i64; float: f32; double: f64;
- long double (binary128): two i64 as an argument, returned through a
hidden pointer;
- a struct or union with one scalar inside (after unwrapping): as that
scalar; any other aggregate as a pointer to a copy, and returned through
a hidden first pointer argument;
- variadic arguments: one i32 pointer to a buffer holding them in order,
each at its type's alignment (aggregates as pointers).
Closures are not supported: a closure's code pointer is a table index of
a function with the closure's signature, and that has to be chosen before
the signature is known (ffi_closure_alloc). ffi_closure_alloc returns
NULL. */
#include <ffi.h>
#include <ffi_common.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
typedef void (*tramp_fn)(void (*fn)(void), const uint64_t *a, void *ret);
#include "tramp.inc"
#define MAX_PARAMS 64
/* The scalar a single-element aggregate is passed as, or NULL. */
static ffi_type *
single_scalar (ffi_type *t)
{
while (t->type == FFI_TYPE_STRUCT)
{
ffi_type **e = t->elements;
ffi_type *only = NULL;
if (!e)
return NULL;
for (; *e; e++)
{
/* empty members do not count */
if ((*e)->type == FFI_TYPE_STRUCT && (*e)->size == 0)
continue;
if (only)
return NULL;
only = *e;
}
if (!only)
return NULL;
t = only;
}
return t;
}
/* The wasm value class of a scalar: 'I', 'J', 'F', 'D', 'L' (long double:
two i64), or 0 for one we do not lower. */
static char
scalar_class (ffi_type *t)
{
switch (t->type)
{
case FFI_TYPE_INT:
case FFI_TYPE_UINT8:
case FFI_TYPE_SINT8:
case FFI_TYPE_UINT16:
case FFI_TYPE_SINT16:
case FFI_TYPE_UINT32:
case FFI_TYPE_SINT32:
case FFI_TYPE_POINTER:
return 'I';
case FFI_TYPE_UINT64:
case FFI_TYPE_SINT64:
return 'J';
case FFI_TYPE_FLOAT:
return 'F';
case FFI_TYPE_DOUBLE:
return 'D';
#if FFI_TYPE_LONGDOUBLE != FFI_TYPE_DOUBLE
case FFI_TYPE_LONGDOUBLE:
return 'L';
#endif
default:
return 0;
}
}
/* How a value of type t travels as an argument: a scalar class, or 'P' for
a pointer to a copy. */
static char
arg_class (ffi_type *t)
{
ffi_type *s = single_scalar (t);
if (s)
return scalar_class (s);
return t->type == FFI_TYPE_STRUCT ? 'P' : 0;
}
static int
cmp_sig (const void *key, const void *elem)
{
return strcmp ((const char *) key, ((const struct tramp *) elem)->sig);
}
/* Build the cif's wasm signature (cif->flags holds the trampoline's index). */
static ffi_status
lower (ffi_cif *cif, unsigned nfixed, int variadic)
{
char sig[MAX_PARAMS + 2];
unsigned n = 1, i;
ffi_type *rt = cif->rtype;
ffi_type *rs = single_scalar (rt);
char rc;
if (rt->type == FFI_TYPE_VOID)
rc = 'V';
else if (rs && scalar_class (rs) && scalar_class (rs) != 'L')
rc = scalar_class (rs);
else if ((rs && scalar_class (rs) == 'L') || rt->type == FFI_TYPE_STRUCT)
{
rc = 'V';
sig[n++] = 'I'; /* the hidden return pointer */
}
else
return FFI_BAD_TYPEDEF;
sig[0] = rc;
for (i = 0; i < nfixed; i++)
{
char c = arg_class (cif->arg_types[i]);
if (!c)
return FFI_BAD_TYPEDEF;
if (n + 2 > MAX_PARAMS)
return FFI_BAD_TYPEDEF;
if (c == 'L')
{
sig[n++] = 'J';
sig[n++] = 'J';
}
else
sig[n++] = c == 'P' ? 'I' : c;
}
if (variadic)
{
for (i = nfixed; i < cif->nargs; i++)
if (!arg_class (cif->arg_types[i]))
return FFI_BAD_TYPEDEF;
sig[n++] = 'I'; /* the buffer */
}
sig[n] = 0;
const struct tramp *t = bsearch (sig, tramps, sizeof tramps / sizeof *tramps,
sizeof *tramps, cmp_sig);
if (!t)
return FFI_BAD_TYPEDEF;
cif->flags = (unsigned) (t - tramps);
cif->nfixedargs = variadic ? nfixed : cif->nargs;
/* bytes ffi_call needs for copies and the variadic buffer: generous */
cif->bytes = 0;
return FFI_OK;
}
ffi_status FFI_HIDDEN
ffi_prep_cif_machdep (ffi_cif *cif)
{
if (cif->abi != FFI_WASM32)
return FFI_BAD_ABI;
return lower (cif, cif->nargs, 0);
}
ffi_status FFI_HIDDEN
ffi_prep_cif_machdep_var (ffi_cif *cif, unsigned nfixedargs,
unsigned ntotalargs)
{
(void) ntotalargs;
if (cif->abi != FFI_WASM32)
return FFI_BAD_ABI;
return lower (cif, nfixedargs, 1);
}
/* One integer argument, extended to 32 bits as the caller must. */
static uint32_t
load_int (ffi_type *t, const void *p)
{
switch (t->type)
{
case FFI_TYPE_UINT8:
return *(const uint8_t *) p;
case FFI_TYPE_SINT8:
return (uint32_t) (int32_t) * (const int8_t *) p;
case FFI_TYPE_UINT16:
return *(const uint16_t *) p;
case FFI_TYPE_SINT16:
return (uint32_t) (int32_t) * (const int16_t *) p;
default:
return *(const uint32_t *) p;
}
}
static size_t
align_up (size_t v, size_t a)
{
return a ? (v + a - 1) & ~(a - 1) : v;
}
void
ffi_call (ffi_cif *cif, void (*fn) (void), void *rvalue, void **avalue)
{
uint64_t a[MAX_PARAMS];
unsigned n = 0, i;
ffi_type *rt = cif->rtype;
ffi_type *rs = single_scalar (rt);
int sret = rt->type != FFI_TYPE_VOID
&& !(rs && scalar_class (rs) && scalar_class (rs) != 'L');
/* Copies of aggregates passed by pointer, the variadic buffer, and a
return slot when the caller gave none. */
size_t room = 16 + align_up (rt->size, 16);
for (i = 0; i < cif->nargs; i++)
room += align_up (cif->arg_types[i]->size, 16) + 16;
unsigned char *scratch = __builtin_alloca (room);
size_t used = 0;
uint64_t rbuf[4] = { 0 };
void *rp = rvalue;
if (sret)
{
if (!rp)
{
rp = scratch;
used = align_up (rt->size, 16);
}
a[n++] = (uint32_t) (uintptr_t) rp;
}
for (i = 0; i < cif->nfixedargs; i++)
{
ffi_type *t = cif->arg_types[i];
ffi_type *s = single_scalar (t);
char c = arg_class (t);
const void *p = avalue[i];
switch (c)
{
case 'I':
a[n++] = load_int (s, p);
break;
case 'J':
a[n++] = *(const uint64_t *) p;
break;
case 'F':
a[n] = 0;
memcpy (&a[n++], p, 4);
break;
case 'D':
memcpy (&a[n++], p, 8);
break;
case 'L':
memcpy (&a[n], p, 16);
n += 2;
break;
case 'P':
{
used = align_up (used, t->alignment > 16 ? 16 : t->alignment);
void *copy = scratch + used;
memcpy (copy, p, t->size);
used += t->size;
a[n++] = (uint32_t) (uintptr_t) copy;
break;
}
}
}
if (cif->nfixedargs < cif->nargs)
{
/* Copies of aggregates first, then the buffer that points at them. */
void *copies[MAX_PARAMS];
for (i = cif->nfixedargs; i < cif->nargs && i < MAX_PARAMS; i++)
{
ffi_type *t = cif->arg_types[i];
copies[i] = NULL;
if (arg_class (t) == 'P')
{
used = align_up (used, t->alignment > 16 ? 16 : t->alignment);
copies[i] = scratch + used;
memcpy (copies[i], avalue[i], t->size);
used += t->size;
}
}
used = align_up (used, 16);
unsigned char *buf = scratch + used;
size_t off = 0;
for (i = cif->nfixedargs; i < cif->nargs && i < MAX_PARAMS; i++)
{
ffi_type *t = cif->arg_types[i];
ffi_type *s = single_scalar (t);
char c = arg_class (t);
switch (c)
{
case 'I':
{
uint32_t v = load_int (s, avalue[i]);
off = align_up (off, 4);
memcpy (buf + off, &v, 4);
off += 4;
break;
}
case 'F':
/* promoted to double by C; a caller that passed float anyway */
{
double d = *(const float *) avalue[i];
off = align_up (off, 8);
memcpy (buf + off, &d, 8);
off += 8;
break;
}
case 'J':
case 'D':
off = align_up (off, 8);
memcpy (buf + off, avalue[i], 8);
off += 8;
break;
case 'L':
off = align_up (off, 16);
memcpy (buf + off, avalue[i], 16);
off += 16;
break;
case 'P':
{
uint32_t v = (uint32_t) (uintptr_t) copies[i];
off = align_up (off, 4);
memcpy (buf + off, &v, 4);
off += 4;
break;
}
}
}
a[n++] = (uint32_t) (uintptr_t) buf;
}
tramps[cif->flags].fn (fn, a, rbuf);
if (sret || rt->type == FFI_TYPE_VOID || !rvalue)
return;
/* A scalar result: integers narrower than ffi_arg widen to it. */
switch (rs->type)
{
case FFI_TYPE_UINT8:
case FFI_TYPE_UINT16:
case FFI_TYPE_UINT32:
case FFI_TYPE_INT:
case FFI_TYPE_SINT8:
case FFI_TYPE_SINT16:
case FFI_TYPE_SINT32:
case FFI_TYPE_POINTER:
if (rt->type == FFI_TYPE_STRUCT)
memcpy (rvalue, rbuf, rt->size);
else
{
uint32_t v = (uint32_t) rbuf[0];
switch (rs->type)
{
case FFI_TYPE_UINT8: v = (uint8_t) v; break;
case FFI_TYPE_SINT8: v = (uint32_t) (int32_t) (int8_t) v; break;
case FFI_TYPE_UINT16: v = (uint16_t) v; break;
case FFI_TYPE_SINT16: v = (uint32_t) (int32_t) (int16_t) v; break;
default: break;
}
*(ffi_arg *) rvalue = v;
}
break;
default:
memcpy (rvalue, rbuf, rt->size < 8 ? rt->size : 8);
break;
}
}
void *
ffi_closure_alloc (size_t size, void **code)
{
(void) size;
if (code)
*code = NULL;
return NULL;
}
void
ffi_closure_free (void *ptr)
{
(void) ptr;
}
ffi_status
ffi_prep_closure_loc (ffi_closure *closure, ffi_cif *cif,
void (*fun) (ffi_cif *, void *, void **, void *),
void *user_data, void *codeloc)
{
(void) closure; (void) cif; (void) fun; (void) user_data; (void) codeloc;
return FFI_BAD_ABI;
}
void *
ffi_data_to_code_pointer (void *data)
{
return data;
}
int
ffi_tramp_is_present (void *ptr)
{
(void) ptr;
return 0;
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""ffi_call's trampolines for gexcc's wasm32 libffi backend (ffi.c here).
A wasm `call_indirect` names the callee's exact signature, so calling a
function pointer whose signature is known only at run time needs one
compiled call site per signature. This writes them: one function per
(return, parameters) over the wasm value types i32 (I), i64 (J), f32 (F),
f64 (D), plus void (V) for the return, and a sorted table ffi.c searches.
Covered: every combination of up to 4 parameters; I/D and I/J up to 7; I/J/D up
to 5; all-I up to 24. Anything else returns FFI_BAD_TYPEDEF from ffi_prep_cif.
"""
import itertools, sys
def sigs():
s = set()
def add(types, n):
for k in range(n + 1):
for p in itertools.product(types, repeat=k):
s.add(''.join(p))
add('IJFD', 4)
add('ID', 7)
add('IJ', 7)
add('IJD', 5)
for k in range(25):
s.add('I' * k)
return s
CT = {'I': 'int32_t', 'J': 'int64_t', 'F': 'float', 'D': 'double', 'V': 'void'}
def arg(t, k):
if t == 'I': return f'(int32_t)a[{k}]'
if t == 'J': return f'(int64_t)a[{k}]'
if t == 'F': return f'*(const float *)&a[{k}]'
return f'*(const double *)&a[{k}]'
def main():
out = sys.stdout
out.write('/* generated by tools/gexcc/debian/libffi-wasm32/gen.py */\n')
rows = []
for p in sorted(sigs()):
for r in 'VIJFD':
key = r + p
ps = ', '.join(CT[t] for t in p) or 'void'
call = f'(({CT[r]} (*)({ps}))fn)({", ".join(arg(t, k) for k, t in enumerate(p))})'
body = f'{call};' if r == 'V' else f'*({CT[r]} *)ret = {call};'
out.write(f'static void t_{key}(void (*fn)(void), const uint64_t *a, void *ret) {{ (void)a; (void)ret; {body} }}\n')
rows.append(key)
rows.sort()
out.write('static const struct tramp { const char *sig; tramp_fn fn; } tramps[] = {\n')
for k in rows:
out.write(f' {{"{k}", t_{k}}},\n')
out.write('};\n')
main()
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""`meson install` for gexcc builds: meson-install.py -C build [args].
meson rewrites the rpath of every ELF it installs, and its ELF reader fails
on a gexcc program's container (a 32-bit ELF with no section headers: a
wasm module wrapped for gex). Such a file has no rpath to fix; skip it.
"""
import sys
from mesonbuild.scripts import depfixer
_fix = depfixer.fix_rpath
def fix_rpath(fname, *args, **kwargs):
with open(fname, 'rb') as f:
h = f.read(52)
if h[:4] == b'\x7fELF' and h[4] == 1 and int.from_bytes(h[48:50], 'little') == 0:
return
return _fix(fname, *args, **kwargs)
depfixer.fix_rpath = fix_rpath
from mesonbuild import mesonmain # noqa: E402
sys.argv = ['meson', 'install'] + sys.argv[1:]
sys.exit(mesonmain.main())
+29 -12
View File
@@ -24,6 +24,8 @@ ARCH = 'musl-linux-wasm32'
DROP = ['usr/include/*', 'usr/lib/*.a', 'usr/lib/pkgconfig/*', 'usr/share/pkgconfig/*',
'usr/share/aclocal/*', 'usr/share/info/*', 'usr/lib/*.la', 'usr/share/man/man3/*']
SUFFIX = '+gex1'
# binpkgs.txt lines that are not splits
KINDS = ('depends', 'predepends', 'arch')
def control_stanzas(path):
@@ -51,20 +53,26 @@ def split_rules(srcpkg):
rules = []
for line in open(os.path.join(HERE, 'binpkgs.txt')):
f = line.split()
if f and not f[0].startswith('#') and f[0] == srcpkg and f[0] != 'depends':
if f and not f[0].startswith('#') and f[0] == srcpkg and f[0] not in KINDS:
rules.append((f[1], f[2:]))
return rules or [(srcpkg, ['*'])]
def depends(binpkg):
def field(kind, binpkg):
"""`KIND BINPKG VALUE...` lines of binpkgs.txt: the rest of each line,
joined with ", " (a Depends field: `depends dpkg tar (>= 1.28-1)`)."""
out = []
for line in open(os.path.join(HERE, 'binpkgs.txt')):
f = line.split()
if len(f) > 2 and f[0] == 'depends' and f[1] == binpkg:
out += f[2:]
f = line.split(None, 2)
if len(f) > 2 and f[0] == kind and f[1] == binpkg:
out.append(f[2].strip())
return ', '.join(out)
def depends(binpkg):
return field('depends', binpkg)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--src', required=True)
@@ -79,8 +87,10 @@ def main():
rules = split_rules(a.srcpkg)
owned = {b: [] for b, _ in rules}
for root, dirs, files in os.walk(a.stage):
for n in files + [d for d in dirs if os.path.islink(os.path.join(root, d))]:
rel = os.path.relpath(os.path.join(root, n), a.stage)
# (an empty directory is part of a package too: python's lib-dynload)
empty = [] if (files or dirs or root == a.stage) else ['']
for n in files + [d for d in dirs if os.path.islink(os.path.join(root, d))] + empty:
rel = os.path.relpath(os.path.join(root, n) if n else root, a.stage)
if any(fnmatch.fnmatch(rel, g) for g in DROP):
continue
for b, globs in rules:
@@ -96,25 +106,30 @@ def main():
print(f'{b}: no files', file=sys.stderr)
continue
st = bins.get(b, {})
# dpkg and apt are labelled with the native architecture, arm64
# (docs/gexcc.md §6.1a): `arch dpkg arm64`
arch = field('arch', b) or ARCH
with tempfile.TemporaryDirectory() as t:
for rel in rels:
s, d = os.path.join(a.stage, rel), os.path.join(t, rel)
os.makedirs(os.path.dirname(d), exist_ok=True)
if os.path.islink(s):
if os.path.isdir(s) and not os.path.islink(s):
os.makedirs(d, exist_ok=True)
elif os.path.islink(s):
os.symlink(os.readlink(s), d)
else:
shutil.copy2(s, d)
size = sum(os.path.getsize(os.path.join(t, r)) for r in rels
if not os.path.islink(os.path.join(t, r))) // 1024
if os.path.isfile(os.path.join(t, r)) and not os.path.islink(os.path.join(t, r))) // 1024
os.makedirs(os.path.join(t, 'DEBIAN'))
md5 = []
for rel in sorted(rels):
p = os.path.join(t, rel)
if not os.path.islink(p):
if os.path.isfile(p) and not os.path.islink(p):
md5.append(f'{hashlib.md5(open(p, "rb").read()).hexdigest()} {rel}')
open(os.path.join(t, 'DEBIAN', 'md5sums'), 'w').write('\n'.join(md5) + '\n')
ctl = [f'Package: {b}', f'Source: {a.srcpkg}' + (f' ({a.version})' if a.version else ''),
f'Version: {version}', f'Architecture: {ARCH}',
f'Version: {version}', f'Architecture: {arch}',
'Maintainer: gex native builds <gex@localhost>',
f'Original-Maintainer: {source.get("Maintainer", "unknown")}',
f'Installed-Size: {size}']
@@ -131,6 +146,8 @@ def main():
if x.strip() and '${' not in x)
if v:
ctl.append(f'{k}: {v}')
if field('predepends', b):
ctl.append(f"Pre-Depends: {field('predepends', b)}")
if depends(b):
ctl.append(f'Depends: {depends(b)}')
if 'Homepage' in source:
@@ -138,7 +155,7 @@ def main():
desc = st.get('Description', f'{b} (native wasm32 build)')
ctl.append(f'Description: {desc}')
open(os.path.join(t, 'DEBIAN', 'control'), 'w').write('\n'.join(ctl) + '\n')
deb = os.path.join(a.out, f'{b}_{version.split(":", 1)[-1]}_{ARCH}.deb')
deb = os.path.join(a.out, f'{b}_{version.split(":", 1)[-1]}_{arch}.deb')
subprocess.run(['dpkg-deb', '--root-owner-group', '-Zxz', '--build', t, deb],
check=True, stdout=subprocess.DEVNULL)
print(b, version, deb)
+35 -24
View File
@@ -144,29 +144,34 @@ def write_repo(root, debs):
Release; the .debs themselves are installed already and not copied
unless --repo-debs)."""
repo = os.path.join(root, 'var/lib/gex-apt')
bindir = os.path.join(repo, f'dists/{SUITE}/main/binary-{OURS_ARCH}')
os.makedirs(bindir, exist_ok=True)
stanzas = []
for deb, ctl in debs:
name = ctl['Package']
fn = f'pool/main/{name[0]}/{name}/{os.path.basename(deb)}'
data = open(deb, 'rb').read()
s = [f'{k}: {v}' for k, v in ctl.items()]
s += [f'Filename: {fn}', f'Size: {len(data)}', f'MD5sum: {hashlib.md5(data).hexdigest()}',
f'SHA256: {hashlib.sha256(data).hexdigest()}']
stanzas.append('\n'.join(s) + '\n')
text = '\n'.join(stanzas).encode()
open(os.path.join(bindir, 'Packages'), 'wb').write(text)
open(os.path.join(bindir, 'Packages.xz'), 'wb').write(lzma.compress(text))
rel_sums = []
# wasm32 tools, and dpkg/apt labelled arm64 (one index per architecture)
for arch in (OURS_ARCH, NATIVE):
bindir = os.path.join(repo, f'dists/{SUITE}/main/binary-{arch}')
os.makedirs(bindir, exist_ok=True)
stanzas = []
for deb, ctl in debs:
if ctl['Architecture'] != arch:
continue
name = ctl['Package']
fn = f'pool/main/{name[0]}/{name}/{os.path.basename(deb)}'
data = open(deb, 'rb').read()
st = [f'{k}: {v}' for k, v in ctl.items()]
st += [f'Filename: {fn}', f'Size: {len(data)}', f'MD5sum: {hashlib.md5(data).hexdigest()}',
f'SHA256: {hashlib.sha256(data).hexdigest()}']
stanzas.append('\n'.join(st) + '\n')
text = '\n'.join(stanzas).encode()
open(os.path.join(bindir, 'Packages'), 'wb').write(text)
open(os.path.join(bindir, 'Packages.xz'), 'wb').write(lzma.compress(text))
for leaf in ('Packages', 'Packages.xz'):
p = os.path.join(bindir, leaf)
rel_sums.append(f' {hashlib.sha256(open(p, "rb").read()).hexdigest()} {os.path.getsize(p)} '
f'main/binary-{arch}/{leaf}')
import email.utils
rel = [f'Origin: gex', 'Label: gex native', f'Suite: {SUITE}', f'Codename: {SUITE}',
f'Date: {email.utils.formatdate(usegmt=True)}',
f'Architectures: {OURS_ARCH}', 'Components: main',
'Description: Debian tools built from source with gexcc as native wasm32', 'SHA256:']
for leaf in ('Packages', 'Packages.xz'):
p = os.path.join(bindir, leaf)
rel.append(f' {hashlib.sha256(open(p, "rb").read()).hexdigest()} {os.path.getsize(p)} '
f'main/binary-{OURS_ARCH}/{leaf}')
f'Architectures: {OURS_ARCH} {NATIVE}', 'Components: main',
'Description: Debian tools built from source with gexcc as native wasm32', 'SHA256:'] + rel_sums
open(os.path.join(repo, f'dists/{SUITE}/Release'), 'w').write('\n'.join(rel) + '\n')
return repo
@@ -261,8 +266,9 @@ def configure(root, ours, native_pkgs):
f'Architectures: {NATIVE}\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg\n')
w('etc/apt/sources.list.d/gex.sources',
f'Types: deb\nURIs: file:/var/lib/gex-apt\nSuites: {SUITE}\nComponents: main\n'
f'Architectures: {OURS_ARCH}\nTrusted: yes\n')
names = sorted({c['Package'] for _, c in ours})
f'Architectures: {OURS_ARCH} {NATIVE}\nTrusted: yes\n')
# (dpkg and apt of ours are labelled arm64: o=gex's 1001 keeps them)
names = sorted({c['Package'] for _, c in ours if c['Architecture'] == OURS_ARCH})
w('etc/apt/preferences.d/gex-native',
'# Our native tools (musl-linux-wasm32) are never replaced by Debian\'s\n'
'# arm64 builds; libraries are not pinned (docs/gexcc.md §6.1a).\n'
@@ -281,7 +287,11 @@ def configure(root, ours, native_pkgs):
'// said here so apt does not fork dpkg to ask (two dpkg runs, 3 s of\n'
'// every apt-cache in the page)\n'
f'APT::Architecture "{NATIVE}";\n'
f'APT::Architectures {{ "{NATIVE}"; "{OURS_ARCH}"; }};\n')
f'APT::Architectures {{ "{NATIVE}"; "{OURS_ARCH}"; }};\n'
'// debconf\'s dpkg-preconfigure (perl, about 19 s per apt install in the\n'
'// page) only asks questions early; postinst asks them anyway, and the\n'
'// demo is noninteractive\n'
'#clear DPkg::Pre-Install-Pkgs;\n')
for d in ('var/lib/apt/lists/partial', 'var/cache/apt/archives/partial', 'var/lib/dpkg/updates',
'var/lib/dpkg/triggers', 'var/log/apt', 'etc/apt/trusted.gpg.d', 'etc/apt/keyrings',
'etc/dpkg/dpkg.cfg.d', 'var/cache/debconf', 'var/lib/apt/periodic', 'proc', 'dev', 'run', 'sys',
@@ -312,7 +322,8 @@ def main():
a_minimal = not a.full
ours = []
for deb in sorted(glob.glob(os.path.join(a.pool, f'*_{OURS_ARCH}.deb'))):
for deb in sorted(glob.glob(os.path.join(a.pool, f'*_{OURS_ARCH}.deb'))
+ glob.glob(os.path.join(a.pool, f'*_{NATIVE}.deb'))):
ours.append((deb, control(deb)))
excluded = set(filter(None, a.exclude.split(',')))
image_ours = [(d, c) for d, c in ours if c['Package'] not in excluded]
+35 -2
View File
@@ -29,6 +29,7 @@ util-linux 2.41.5-0+deb13u1 tier1
make-dfsg 4.4.1-2 tier2
sqlite3 3.46.1-7+deb13u2 tier2
lua5.4 5.4.7-1 tier2
libonig 6.9.9-1 tier2
jq 1.7.1-6+deb13u3 tier2
tree 2.2.1-1 tier2
patch 2.8-2 tier2
@@ -45,8 +46,40 @@ curl 8.14.1-2+deb13u5 tier2
git 1:2.47.3-0+deb13u1 tier2
python3.13 3.13.5-2+deb13u5 tier2
perl 5.40.1-6+deb13u1 tier2
dpkg 1.22.22 tier2
apt 3.0.3 tier2
lz4 1.10.0-4 tier2
mawk 1.3.4.20250131-1 tier1
sysvinit 3.14-4 tier1
# pm: dpkg and apt themselves, labelled arm64 (docs/gexcc.md §6.1a), and
# the OpenPGP verifier apt runs (gpgv); needs tier1's compressors and openssl
xxhash 0.8.3-2 pm
dpkg 1.22.22 pm
apt 3.0.3 pm
libgpg-error 1.51-4 pm
libgcrypt20 1.11.0-7+deb13u1 pm
libassuan 3.0.2-2 pm
libksba 1.6.7-2 pm
npth 1.8-3 pm
gnupg2 2.4.7-21+deb13u1 pm
# gui: Wayland clients for gex's compositor (docs/gexcc.md §6.2 tier 3)
wayland 1.23.1-3 gui
wayland-protocols 1.44-1 gui
libxkbcommon 1.7.0-2 gui
pixman 0.44.0-3 gui
freetype 2.13.3+dfsg-1+deb13u1 gui
fontconfig 2.15.0-2.3 gui
utf8proc 2.9.0-1 gui
tllist 1.1.0-1 gui
fcft 3.3.1-1 gui
foot 1.21.0-2 gui
xorgproto 2024.1-1 gui
xtrans 1.4.0-1 gui
xcb-proto 1.17.0-1 gui
libxau 1:1.0.11-1 gui
libxdmcp 1:1.1.5-1 gui
libxcb 1.17.0-2 gui
libx11 2:1.8.12-1 gui
libxext 2:1.3.4-1 gui
libsdl2 2.32.4+dfsg-1 gui
libsdl2-net 2.2.0+dfsg-3 gui
libsdl2-mixer 2.8.1+dfsg-2 gui
chocolate-doom 3.1.0-1 gui
@@ -0,0 +1,33 @@
libc++ has no std::char_traits<unsigned char> (LLVM 19+): hex digests from a char view.
--- a/apt-pkg/contrib/hashes.cc 2026-10-09 00:21:28.929355189 -0400
+++ b/apt-pkg/contrib/hashes.cc 2026-10-09 00:21:28.932355194 -0400
@@ -296,7 +296,7 @@
return !(*this == other);
}
/*}}}*/
-static APT_PURE std::string HexDigest(std::basic_string_view<unsigned char> const &Sum)
+static APT_PURE std::string HexDigest(std::string_view const &Sum)
{
char Conv[16] =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
@@ -308,8 +308,8 @@
size_t I = 0;
for (; I != (Sum.size()) * 2; J++, I += 2)
{
- Result[I] = Conv[Sum[J] >> 4];
- Result[I + 1] = Conv[Sum[J] & 0xF];
+ Result[I] = Conv[(unsigned char)Sum[J] >> 4];
+ Result[I + 1] = Conv[(unsigned char)Sum[J] & 0xF];
}
return Result;
};
@@ -360,7 +360,7 @@
EVP_DigestFinal_ex(tmpContext, Sum, nullptr);
EVP_MD_CTX_destroy(tmpContext);
- return ::HexDigest(std::basic_string_view<unsigned char>(Sum, Size));
+ return ::HexDigest(std::string_view(reinterpret_cast<const char *>(Sum), Size));
}
bool Enable(HashAlgo const &algo)
@@ -0,0 +1,25 @@
libc++'s std::deque needs a complete element type; Var is declared later: queue it in a list.
--- a/apt-pkg/solver3.h 2026-10-09 00:23:16.699550486 -0400
+++ b/apt-pkg/solver3.h 2026-10-09 00:23:16.695550479 -0400
@@ -10,6 +10,7 @@
#include <cassert>
#include <memory>
#include <optional>
+#include <list>
#include <queue>
#include <vector>
@@ -212,9 +213,10 @@
std::vector<Solved> solved{};
// \brief Propagation queue
- std::queue<Var> propQ;
+ // (libc++: a deque needs a complete type, a list does not)
+ std::queue<Var, std::list<Var>> propQ;
// \brief Discover variables
- std::queue<Var> discoverQ;
+ std::queue<Var, std::list<Var>> discoverQ;
// \brief Current decision level.
//
@@ -0,0 +1,13 @@
musl has no GNU basename (and argv is const here): apt's own flNotDir.
--- a/cmdline/apt-internal-solver.cc 2026-10-09 00:33:52.586737971 -0400
+++ b/cmdline/apt-internal-solver.cc 2026-10-09 00:33:52.589737977 -0400
@@ -119,7 +119,7 @@
_config->Set("Debug::EDSP::WriteSolution", true);
_config->Set("APT::System", "Debian APT solver interface");
- if (strcmp(basename(argv[0]), "solver3") == 0)
+ if (flNotDir(argv[0]) == "solver3")
_config->Set("APT::Solver", "3.0");
else if (_config->Find("APT::Solver") != "3.0")
_config->Set("APT::Solver", "internal");
@@ -0,0 +1,16 @@
SDL_nullvideo.c declares SDL_EVDEV_Init as void; it returns int
(SDL_evdev.h). C on a real machine gets away with it; wasm calls check
the signature, and the link either traps there or (with the mismatch
folded into the call) leaves a module binaryen refuses.
--- a/src/video/dummy/SDL_nullvideo.c 2026-10-09 05:09:30.933302323 -0400
+++ b/src/video/dummy/SDL_nullvideo.c 2026-10-09 05:09:30.936302328 -0400
@@ -130,7 +130,7 @@
DUMMY_CreateDevice,
NULL /* no ShowMessageBox implementation */
};
-void SDL_EVDEV_Init(void);
+int SDL_EVDEV_Init(void);
void SDL_EVDEV_Poll(void);
void SDL_EVDEV_Quit(void);
static void DUMMY_EVDEV_Poll(_THIS)
@@ -0,0 +1,19 @@
ctypes without dlopen: a static wasm32 python has no handle for itself, so
import ctypes failed at pythonapi. Keep the module importable.
--- a/Lib/ctypes/__init__.py 2026-10-09 05:38:42.395624558 -0400
+++ b/Lib/ctypes/__init__.py 2026-10-09 05:38:42.422624611 -0400
@@ -482,7 +482,12 @@
elif _sys.platform == "cygwin":
pythonapi = PyDLL("libpython%d.%d.dll" % _sys.version_info[:2])
else:
- pythonapi = PyDLL(None)
+ # A program with no dynamic loader (a static wasm32 build) has no handle
+ # for itself: ctypes' types and buffers still work, a library does not.
+ try:
+ pythonapi = PyDLL(None)
+ except OSError:
+ pythonapi = None
if _os.name == "nt":
+562
View File
@@ -269,3 +269,565 @@ build_util_linux() {
rm -rf $DEST/usr/include $DEST/usr/lib $DEST/usr/share/doc $DEST/usr/sbin $DEST/usr/bin/kill $DEST/usr/bin/rename \
$DEST/usr/share/man/man8 $DEST/usr/share/man/man1/kill.1 $DEST/usr/share/man/man1/rename.1
}
# ---- tier 2
build_make_dfsg() {
# the dfsg tarball has no doc/ (the manual is non-free): out of the build,
# then the build system regenerated, as dh does
sed -i 's| doc/Makefile||' configure.ac
sed -i '/^SUBDIRS/s/ doc//' Makefile.am
AUTOPOINT=true autoreconf -fi
at_configure --disable-nls --without-guile
make -j$JOBS
make install DESTDIR=$DEST
rm -rf $DEST/usr/include
}
# A tclsh for the build machine: SQLite's full source tree generates its
# amalgamation and keyword tables with Tcl scripts. Debian's tcl8.6, built
# once into $GEXCC_DEB/host.
host_tclsh() {
local h=$GEXCC_DEB/host
if [ ! -x $h/bin/tclsh8.6 ]; then
python3 -I "$HERE/debsrc.py" --into $h tcl8.6=8.6.16+dfsg-1 > /dev/null
(cd $h/tcl8.6-8.6.16+dfsg-1/unix && CONFIG_SITE= CC=cc ./configure -q --prefix=$h --disable-shared \
&& make -j$JOBS > /dev/null && make install-binaries install-libraries > /dev/null)
ln -sf tclsh8.6 $h/bin/tclsh
fi
export PATH=$h/bin:$PATH
}
build_sqlite3() {
host_tclsh
# Debian's CFLAGS: the extensions its sqlite3 has (FTS, R*Tree, JSON is
# built in, column metadata for python)
at_configure --disable-tcl --enable-readline --disable-shared --enable-static --enable-fts5 \
CFLAGS="-O2 -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_RTREE -DSQLITE_ENABLE_COLUMN_METADATA -DSQLITE_ENABLE_DBSTAT_VTAB -DSQLITE_SECURE_DELETE -DSQLITE_ENABLE_UNLOCK_NOTIFY -DSQLITE_MAX_VARIABLE_NUMBER=250000" \
CPPFLAGS="-I$SYSROOT/usr/include/ncursesw" LIBS="-lncursesw"
make -j$JOBS
both install; nola
rm -rf $DEST/usr/include $DEST/usr/lib
}
build_libonig() {
[ -x configure ] || autoreconf -fi
at_configure --disable-shared --enable-static
make -j$JOBS
both install; nola
}
build_jq() {
autoreconf -fi
at_configure --with-oniguruma=$SYSROOT/usr --disable-docs --disable-maintainer-mode --disable-shared --enable-static \
--disable-valgrind
make -j$JOBS
mkdir -p $DEST/usr/bin; cp jq $DEST/usr/bin/
}
build_lua5_4() {
# Debian's makefile builds liblua5.4.so with libtool; here the same
# objects, static: the POSIX build plus readline, as Debian's lua5.4 has
# what debian/rules generates: where Debian puts C modules
echo '#define DEB_HOST_MULTIARCH "wasm32-linux-musl"' > src/lua5.4-deb-multiarch.h
cd src
local f="-O2 -std=gnu99 -DLUA_COMPAT_5_3 -DLUA_USE_POSIX -DLUA_USE_READLINE -I$SYSROOT/usr/include/ncursesw"
local core="lapi lcode lctype ldebug ldo ldump lfunc lgc llex lmem lobject lopcodes lparser lstate lstring ltable ltm lundump lvm lzio lauxlib lbaselib lcorolib ldblib liolib lmathlib loadlib loslib lstrlib ltablib lutf8lib linit"
for o in $core lua luac; do gexcc $f -c $o.c -o $o.o & done; wait
gexcc-ar rcs liblua.a $(for o in $core; do echo $o.o; done)
gexcc -o lua lua.o liblua.a -lreadline -lncursesw -ltinfo -lm
gexcc -o luac luac.o liblua.a -lm
cd ..
mkdir -p $DEST/usr/bin $SYSROOT/usr/lib $SYSROOT/usr/include/lua5.4
cp src/lua $DEST/usr/bin/lua5.4; cp src/luac $DEST/usr/bin/luac5.4
# the names Debian's alternatives give them
ln -sf lua5.4 $DEST/usr/bin/lua; ln -sf luac5.4 $DEST/usr/bin/luac
cp src/liblua.a $SYSROOT/usr/lib/liblua5.4.a
cp src/lua.h src/luaconf.h src/lualib.h src/lauxlib.h src/lua.hpp $SYSROOT/usr/include/lua5.4/
}
build_tree() {
make -j$JOBS CC=gexcc CFLAGS="-O2 -std=c11 -D_DEFAULT_SOURCE -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE"
mkdir -p $DEST/usr/bin; cp tree $DEST/usr/bin/
}
build_patch() {
at_configure
make -j$JOBS
make install DESTDIR=$DEST
}
build_bc() {
# bc's math library is compiled at build time by the bc just built: a host
# bc of the same source makes libmath.h first
# (the patched tree's timestamps ask for automake 1.14: regenerate)
autoreconf -fi
# libmath.h's last step is an ed script, and the build machine has no ed:
# the same edit in sed
cat > bc/fix-libmath_h <<'FIX'
sed -i -e '1s/^/{"/' -e 's/$/",/' -e '2,$s/^/"/' libmath.h
sed -i -e '$d' libmath.h
sed -i -e '$s/,$/,0}/' libmath.h
FIX
rm -rf ../bc-host; cp -a . ../bc-host
(cd ../bc-host && CONFIG_SITE= CC=cc ./configure -q --build=x86_64-pc-linux-gnu && make -j$JOBS MAKEINFO=true >/dev/null)
at_configure --with-readline CPPFLAGS="-I$SYSROOT/usr/include/ncursesw" LIBS="-lncursesw -ltinfo"
cp ../bc-host/bc/libmath.h bc/libmath.h
make -j$JOBS MAKEINFO=true fbc_EXEEXT= || true
touch bc/libmath.h
make -j$JOBS MAKEINFO=true
make install DESTDIR=$DEST MAKEINFO=true
rm -rf $DEST/usr/share/info
}
build_ed() {
./configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info CC=gexcc CFLAGS=-O2
make -j$JOBS
make install DESTDIR=$DEST
rm -rf $DEST/usr/share/info
}
build_libevent() {
# Debian's patches touch the build system (dh autoreconfs)
autoreconf -fi
at_configure --disable-shared --enable-static --disable-openssl --disable-samples --disable-libevent-regress \
--disable-debug-mode
make -j$JOBS
both install; nola
}
build_tmux() {
PKG_CONFIG=gexcc-pkg-config at_configure --disable-utf8proc CPPFLAGS="-I$SYSROOT/usr/include/ncursesw"
make -j$JOBS
make install DESTDIR=$DEST
}
build_htop() {
[ -x configure ] || autoreconf -fi
PKG_CONFIG=gexcc-pkg-config at_configure --enable-unicode --disable-sensors --disable-capabilities \
--disable-hwloc --disable-delayacct --disable-unwind CPPFLAGS="-I$SYSROOT/usr/include/ncursesw"
make -j$JOBS
make install DESTDIR=$DEST
rm -rf $DEST/usr/share/icons $DEST/usr/share/pixmaps
}
build_expat() {
# the git tree (Debian's orig): the library in expat/, no configure
cd expat
./buildconf.sh
at_configure --without-docbook --without-examples --without-tests --disable-shared --enable-static
make -j$JOBS
both install; nola
rm -rf $DEST
mkdir -p $DEST
}
build_lz4() {
make -j$JOBS -C lib CC=gexcc AR=gexcc-ar liblz4.a
make -j$JOBS -C programs CC=gexcc AR=gexcc-ar lz4
for d in $DEST $SYSROOT; do
make -C lib install-static install-includes install-pc DESTDIR=$d PREFIX=/usr LIBDIR=/usr/lib 2>/dev/null \
|| { mkdir -p $d/usr/lib $d/usr/include; cp lib/liblz4.a $d/usr/lib/; cp lib/lz4.h lib/lz4hc.h lib/lz4frame.h $d/usr/include/; }
done
rm -rf $DEST/usr/lib $DEST/usr/include
mkdir -p $DEST/usr/bin; cp programs/lz4 $DEST/usr/bin/
for n in unlz4 lz4cat; do ln -sf lz4 $DEST/usr/bin/$n; done
}
build_libffi() {
# Our wasm32 backend (libffi-wasm32/): Debian's src/wasm32 is emscripten's,
# which calls through JavaScript. Static, into the sysroot only.
[ -x configure ] || autoreconf -fi
cp "$HERE/libffi-wasm32/ffi.c" src/wasm32/ffi.c
python3 -I "$HERE/libffi-wasm32/gen.py" > src/wasm32/tramp.inc
# closures.c's allocator is for machines with executable memory
sed -i 's/^#ifndef __EMSCRIPTEN__$/#if !defined(__EMSCRIPTEN__) \&\& !defined(__wasm__)/' src/closures.c
at_configure --disable-shared --enable-static --disable-docs --disable-multi-os-directory \
--disable-exec-static-tramp --disable-raw-api
make -j$JOBS
both install; nola
rm -rf $DEST; mkdir -p $DEST
}
build_mpdecimal() {
at_configure --disable-shared --enable-static --disable-cxx
make -j$JOBS
both install; nola
rm -rf $DEST; mkdir -p $DEST
}
build_python3_13() {
# A native-style configure, as the corpus's: its checks and the build's
# own steps (freezing, the stdlib's bytecode) run the programs just built,
# under gexc. Extension modules static (no shared objects). Debian's
# patches give its layout (dist-packages).
export ac_cv_file__dev_ptmx=yes ac_cv_file__dev_ptc=no
MODULE_BUILDTYPE=static PKG_CONFIG=gexcc-pkg-config CC=gexcc CXX=gexc++ AR=gexcc-ar RANLIB=gexcc-ranlib \
CPPFLAGS="-I$SYSROOT/usr/include/ncursesw" \
./configure --prefix=/usr --disable-shared --without-ensurepip --disable-test-modules \
--with-system-expat --with-system-libmpdec --with-pkg-config=yes --with-readline=readline \
--without-static-libpython --with-openssl=$SYSROOT/usr
make -j$JOBS
make install DESTDIR=$DEST
ln -sf python3.13 $DEST/usr/bin/python3
# extension modules are built in; the directory still says where exec_prefix is
mkdir -p $DEST/usr/lib/python3.13/lib-dynload
rm -rf $DEST/usr/lib/python3.13/test $DEST/usr/lib/python3.13/idlelib/idle_test \
$DEST/usr/lib/python3.13/config-3.13-* $DEST/usr/lib/*.a $DEST/usr/include
find $DEST -name '*.opt-1.pyc' -o -name '*.opt-2.pyc' | xargs rm -f
}
build_openssl() {
# no assembly (wasm32), static, Debian's openssldir
./Configure linux-generic32 no-asm no-shared no-dso no-tests no-docs no-afalgeng no-async \
--prefix=/usr --libdir=lib --openssldir=/usr/lib/ssl CC=gexcc AR=gexcc-ar RANLIB=gexcc-ranlib \
-DOPENSSL_NO_SECURE_MEMORY
make -j$JOBS build_sw
make install_sw DESTDIR=$SYSROOT
make install_sw install_ssldirs DESTDIR=$DEST
# the openssl binary; the libraries live in the programs that link them
rm -rf $DEST/usr/include $DEST/usr/lib/*.a $DEST/usr/lib/pkgconfig $DEST/usr/lib/cmake $DEST/usr/bin/c_rehash
# Debian's /usr/lib/ssl points at /etc/ssl (ca-certificates' bundle)
rm -rf $DEST/usr/lib/ssl/certs $DEST/usr/lib/ssl/private $DEST/usr/lib/ssl/openssl.cnf*
ln -sf /etc/ssl/certs $DEST/usr/lib/ssl/certs
ln -sf /etc/ssl/private $DEST/usr/lib/ssl/private
mkdir -p $DEST/etc/ssl; cp apps/openssl.cnf $DEST/etc/ssl/openssl.cnf
ln -sf /etc/ssl/openssl.cnf $DEST/usr/lib/ssl/openssl.cnf
}
build_curl() {
# OpenSSL, as Debian's curl; the CA bundle ca-certificates writes
PKG_CONFIG=gexcc-pkg-config at_configure --with-openssl=$SYSROOT/usr --with-zlib=$SYSROOT/usr \
--without-libpsl --disable-ldap --without-brotli --with-zstd=$SYSROOT/usr --without-libidn2 --without-nghttp2 \
--without-libssh2 --without-librtmp --disable-shared --enable-static \
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt --with-ca-path=/etc/ssl/certs LIBS="-lzstd"
make -j$JOBS
both install; nola
rm -rf $DEST/usr/include $DEST/usr/lib $DEST/usr/bin/curl-config $DEST/usr/share/aclocal
}
build_git() {
# git's own makefile; no Tcl/Tk (gitk, git-gui), gettext, Perl or Python
# parts (send-email, svn, p4); https through libcurl and OpenSSL
local m="prefix=/usr gitexecdir=/usr/lib/git-core CC=gexcc AR=gexcc-ar RANLIB=gexcc-ranlib uname_S=Linux uname_M=wasm32
NO_TCLTK=1 NO_GETTEXT=1 NO_PERL=1 NO_PYTHON=1 NO_SVN_TESTS=1 USE_LIBPCRE2=1 NO_INSTALL_HARDLINKS=1
CURL_CONFIG=$SYSROOT/usr/bin/curl-config CURL_LDFLAGS='-lcurl -lssl -lcrypto -lz -lzstd'
EXPATDIR=$SYSROOT/usr ZLIB_PATH=$SYSROOT/usr NEEDS_LIBICONV= HAVE_DEV_TTY=1 NO_REGEX=NeedsStartEnd
CFLAGS=-O2 V=1"
eval make -j$JOBS $m all
eval make $m install DESTDIR=$DEST
rm -rf $DEST/usr/share/perl5 $DEST/usr/share/gitweb $DEST/usr/share/git-gui $DEST/usr/share/gitk
}
build_dpkg() {
# Native architecture arm64 (docs/gexcc.md §6.1a): configure would ask
# dpkg-architecture about the wasm32 host triplet; say arm64 instead
sed -i 's|^\( *\)cpu_type=\$(PERL=.*$|\1cpu_type=arm64|; s|^\( *\)os_type=\$(PERL=.*$|\1os_type=linux|; s|^\( *\)dpkg_arch=\$(PERL=.*$|\1dpkg_arch=arm64|' configure
at_configure --libexecdir=/usr/lib --disable-nls --disable-dselect --disable-devel-docs \
--without-libselinux --with-libz --with-liblzma --with-libzstd --with-libbz2 \
--disable-shared --enable-static PERL=/usr/bin/perl
grep -q '#define ARCHITECTURE "arm64"' config.h
make -j$JOBS -C lib
make -j$JOBS -C src
make -j$JOBS -C utils
make -C src install DESTDIR=$DEST
make -C utils install DESTDIR=$DEST
# Debian's dpkg package (debian/dpkg.install), the C side
mkdir -p $DEST/etc/dpkg/dpkg.cfg.d $DEST/etc/alternatives \
$DEST/var/lib/dpkg/{alternatives,info,parts,updates} $DEST/usr/share/dpkg/sh
cp debian/dpkg.cfg $DEST/etc/dpkg/
cp data/*table $DEST/usr/share/dpkg/
cp src/sh/dpkg-error.sh $DEST/usr/share/dpkg/sh/
rm -rf $DEST/usr/share/polkit-1
}
build_xxhash() {
make -j$JOBS CC=gexcc AR=gexcc-ar libxxhash.a
mkdir -p $SYSROOT/usr/lib $SYSROOT/usr/include
cp libxxhash.a $SYSROOT/usr/lib/; cp xxhash.h xxh3.h $SYSROOT/usr/include/
}
# apt's build runs triehash (a perl script) on the build machine
host_triehash() {
local h=$GEXCC_DEB/host
if [ ! -x $h/bin/triehash ]; then
python3 -I "$HERE/debsrc.py" --into $h triehash=0.3-3 > /dev/null
mkdir -p $h/bin; install -m755 $h/triehash-0.3-3/triehash.pl $h/bin/triehash
fi
export PATH=$h/bin:$PATH
}
build_apt() {
# Native architecture arm64 like our dpkg (docs/gexcc.md §6.1a). Static:
# libapt-pkg and libapt-private go into each program. No seccomp, udev,
# systemd, berkeley db (apt-ftparchive only), nls or docs. No sqv: the
# gpgv method runs our native gpgv.
host_triehash
# (Size is 64-bit in FileFd, size_t 32-bit: ZSTD buffers narrow it)
# without NLS apt defines setlocale and friends inline with throw(); musl
# declares them without, which C++ rejects
sed -i '/^extern "C" inline/s/ throw()//' CMake/apti18n.h.in
sed -i 's/add_library(apt-pkg SHARED/add_library(apt-pkg STATIC/; /-Wl,-version-script=/d' apt-pkg/CMakeLists.txt
sed -i 's/add_library(apt-private SHARED/add_library(apt-private STATIC/' apt-private/CMakeLists.txt
sed -i 's/^find_package(Berkeley REQUIRED)/find_package(Berkeley)/; s/^add_subdirectory(ftparchive)/#&/; s/^add_subdirectory(test)/#&/; s/^add_subdirectory(doc)/#&/; s/^add_subdirectory(dselect)/#&/' CMakeLists.txt
mkdir -p build && cd build
cmake .. -G "Unix Makefiles" -DCMAKE_TOOLCHAIN_FILE=$GEXCC_PREFIX/share/gexcc/toolchain.cmake \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_SYSCONFDIR=/etc -DCMAKE_INSTALL_LOCALSTATEDIR=/var \
-DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_INSTALL_LIBEXECDIR=lib -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_STAGING_PREFIX= \
-DCMAKE_CXX_FLAGS="-Wno-c++11-narrowing" -DWITH_DOC=OFF -DWITH_TESTS=OFF -DUSE_NLS=OFF -DCOMMON_ARCH=arm64 -DDPKG_DATADIR=/usr/share/dpkg \
-DROOT_GROUP=root -DSQV_EXECUTABLE=SQV_EXECUTABLE-NOTFOUND \
-DSECCOMP_FOUND=OFF -DUDEV_FOUND=OFF -DSYSTEMD_FOUND=OFF -DBERKELEY_FOUND=OFF \
-DCMAKE_DISABLE_FIND_PACKAGE_SECCOMP=ON -DCMAKE_DISABLE_FIND_PACKAGE_Udev=ON \
-DCMAKE_DISABLE_FIND_PACKAGE_Systemd=ON -DCMAKE_DISABLE_FIND_PACKAGE_Berkeley=ON
make -j$JOBS
make install DESTDIR=$DEST
cd ..
# (the toolchain's staging prefix, emptied above, also empties the install
# prefix for the /usr parts; the compiled-in paths are /usr's)
mkdir -p $DEST/usr
for d in bin lib share include; do if [ -d $DEST/$d ]; then cp -a $DEST/$d $DEST/usr/; rm -rf $DEST/$d; fi; done
# Debian's apt package (debian/apt.install): the programs, methods, config
rm -rf $DEST/usr/include $DEST/usr/lib/*.a $DEST/usr/lib/pkgconfig $DEST/usr/share/doc
rm -f $DEST/usr/bin/apt-extracttemplates $DEST/usr/bin/apt-ftparchive $DEST/usr/bin/apt-sortpkgs
# one program for all of apt's (aptmulti.py): 15 MB instead of 100
python3 -I "$HERE/aptmulti.py" $PWD/build $PWD/build/apt-multi > build/multi-names
rm -f $DEST/usr/bin/apt*; rm -rf $DEST/usr/lib/apt/methods $DEST/usr/lib/apt/solvers $DEST/usr/lib/apt/planners \
$DEST/usr/lib/apt/apt-helper $DEST/usr/lib/apt/apt-extracttemplates
install -m755 build/apt-multi $DEST/usr/lib/apt/apt-multi
while read -r n; do
case $n in
bin/*) ln -s ../lib/apt/apt-multi $DEST/usr/$n ;;
*/*) mkdir -p $DEST/usr/lib/apt/${n%/*}; ln -s ../apt-multi $DEST/usr/lib/apt/$n ;;
*) ln -s apt-multi $DEST/usr/lib/apt/$n ;;
esac
done < build/multi-names
mkdir -p $DEST/etc/apt/{apt.conf.d,preferences.d,sources.list.d,trusted.gpg.d,keyrings,auth.conf.d} \
$DEST/var/lib/apt/{lists/partial,periodic,mirrors/partial} $DEST/var/cache/apt/archives/partial $DEST/var/log/apt
cp debian/apt.conf.autoremove $DEST/etc/apt/apt.conf.d/01autoremove 2>/dev/null || true
}
# the GnuPG libraries' configure scripts find each other through gpgrt-config
# and the .pc files in the sysroot (gpg-error-config is not installed)
export GPGRT_CONFIG=$SYSROOT/usr/bin/gpgrt-config
build_libgpg_error() {
at_configure --enable-static --disable-shared --disable-nls --disable-doc --disable-tests
make -j$JOBS
both install; nola
}
build_libgcrypt20() {
# no assembly (wasm32); the generic C code paths
at_configure --enable-static --disable-shared --disable-asm --disable-doc --disable-jent-support \
--disable-padlock-support --disable-aesni-support --disable-shaext-support --disable-pclmul-support \
--disable-sse41-support --disable-drng-support --disable-avx-support --disable-avx2-support \
--disable-neon-support --disable-arm-crypto-support
make -j$JOBS -C cipher; make -j$JOBS -C random; make -j$JOBS -C mpi; make -j$JOBS -C compat; make -j$JOBS -C src
make -C src install DESTDIR=$SYSROOT; nola
}
build_libassuan() {
at_configure --enable-static --disable-shared --disable-doc
make -j$JOBS -C src
make -C src install DESTDIR=$SYSROOT; nola
}
build_libksba() {
at_configure --enable-static --disable-shared --disable-doc
make -j$JOBS -C gl; make -j$JOBS -C src
make -C src install DESTDIR=$SYSROOT; nola
}
build_npth() {
# (gnupg's configure requires it; gpgv itself does not use threads)
at_configure --enable-static --disable-shared --disable-tests
make -j$JOBS -C src
make install DESTDIR=$SYSROOT SUBDIRS=src; nola
}
build_gnupg2() {
# gpgv only, configured as Debian's gpgv (udeb) build: what apt runs to
# check InRelease against debian-archive-keyring
at_configure --libexecdir=/usr/lib/gnupg --disable-doc --disable-nls --disable-bzip2 \
--disable-gpgtar --disable-gpgsm --disable-scdaemon --disable-dirmngr --disable-tofu --disable-exec \
--disable-ldap --disable-gnutls --disable-sqlite --disable-libdns --disable-keyboxd --disable-tpm2d \
--disable-card-support --disable-ccid-driver --disable-wks-tools \
--with-zlib=$SYSROOT/usr
make -j$JOBS -C common; make -j$JOBS -C regexp; make -j$JOBS -C kbx libkeybox.a
make -j$JOBS -C g10 gpgv
mkdir -p $DEST/usr/bin; cp g10/gpgv $DEST/usr/bin/
}
# -- gui: Wayland clients (docs/gexcc.md §6.2 tier 3) -------------------------
export PATH=$PATH:$HOME/.local/bin
# meson cross setup into build/, static, with the host's own tools (a host
# wayland-scanner) found through the build machine's pkg-config path
ms_setup() {
meson setup build --cross-file $GEXCC_PREFIX/share/gexcc/meson-cross.ini \
--prefix=/usr --libdir=lib --sysconfdir=/etc --localstatedir=/var --buildtype=release \
-Ddefault_library=static -Dbuild.pkg_config_path=$GEXCC_DEB/host/lib/pkgconfig "$@"
}
ms_install() { python3 "$HERE/meson-install.py" -C build --no-rebuild "$@"; }
ms_both() { DESTDIR=$DEST ms_install; DESTDIR=$SYSROOT ms_install; }
# wayland-scanner for the build machine (protocol code is generated at build time)
host_wayland_scanner() {
[ -x $GEXCC_DEB/host/bin/wayland-scanner ] && return
meson setup hostbuild --prefix=$GEXCC_DEB/host --libdir=lib -Dlibraries=false -Dscanner=true \
-Dtests=false -Ddocumentation=false -Ddtd_validation=false
meson install -C hostbuild
}
build_wayland() {
host_wayland_scanner
ms_setup -Dscanner=false -Dtests=false -Ddocumentation=false -Ddtd_validation=false
meson compile -C build
ms_both
}
build_wayland_protocols() {
ms_setup -Dtests=false
meson compile -C build
ms_both
# headers and XML only: the sysroot is where they are used
rm -rf $DEST; mkdir -p $DEST
}
build_libxkbcommon() {
ms_setup -Denable-tools=false -Denable-x11=false -Denable-docs=false -Denable-wayland=false \
-Denable-xkbregistry=false -Denable-bash-completion=false \
-Dxkb-config-root=/usr/share/X11/xkb -Dx-locale-root=/usr/share/X11/locale
meson compile -C build
ms_both
}
build_pixman() {
ms_setup -Dloongson-mmi=disabled -Dmmx=disabled -Dsse2=disabled -Dssse3=disabled -Dvmx=disabled \
-Darm-simd=disabled -Dneon=disabled -Da64-neon=disabled -Dmips-dspr2=disabled -Drvv=disabled \
-Dgnu-inline-asm=disabled -Dopenmp=disabled -Dtimers=false -Dgtk=disabled -Dlibpng=disabled \
-Dtests=disabled -Ddemos=disabled
meson compile -C build
ms_both
}
build_freetype() {
ms_setup -Dbrotli=disabled -Dbzip2=disabled -Dharfbuzz=disabled -Dpng=disabled -Dzlib=system -Dtests=disabled
meson compile -C build
ms_both
}
build_fontconfig() {
ms_setup -Ddoc=disabled -Dnls=disabled -Dtests=disabled -Dtools=enabled -Dcache-build=disabled \
-Dcache-dir=/var/cache/fontconfig
meson compile -C build
ms_both
}
build_utf8proc() {
make -j$JOBS CC=gexcc AR=gexcc-ar libutf8proc.a
for d in $DEST $SYSROOT; do
install -Dm644 libutf8proc.a $d/usr/lib/libutf8proc.a
install -Dm644 utf8proc.h $d/usr/include/utf8proc.h
mkdir -p $d/usr/lib/pkgconfig
sed -e 's|PREFIX|/usr|; s|LIBDIR|lib|; s|INCLUDEDIR|include|' -e "s|VERSION|${PWD##*/utf8proc-}|; s|-[0-9]*$||" \
libutf8proc.pc.in > $d/usr/lib/pkgconfig/libutf8proc.pc
done
rm -rf $DEST; mkdir -p $DEST
}
build_tllist() {
ms_setup
ms_both
rm -rf $DEST; mkdir -p $DEST
}
build_fcft() {
ms_setup -Ddocs=disabled -Dgrapheme-shaping=disabled -Drun-shaping=disabled -Dtest-text-shaping=false \
-Dexamples=false -Dsvg-backend=nanosvg
meson compile -C build
ms_both
}
build_foot() {
ms_setup -Ddocs=disabled -Dgrapheme-clustering=disabled -Dtests=false -Dterminfo=enabled \
-Dutmp-backend=none -Dime=true -Dthemes=true -Dwerror=false -Dc_args=-Wno-c2y-extensions
meson compile -C build
DESTDIR=$DEST ms_install
}
# -- gui: X11 clients (the page's X server, hostx) and SDL ---------------------
# autotools library: configure, make, install into the staging tree and the
# sysroot; static, with docs and specs off
at_lib() { at_configure --enable-static --disable-shared "$@"; make -j$JOBS; both install; nola; }
build_xorgproto() { at_configure --disable-specs; both install; rm -rf $DEST; mkdir -p $DEST; }
build_xtrans() { at_configure --disable-docs; both install; rm -rf $DEST; mkdir -p $DEST; }
build_xcb_proto() { at_configure; both install; rm -rf $DEST; mkdir -p $DEST; }
build_libxau() { at_lib; }
build_libxdmcp() { at_lib --disable-docs; }
build_libxcb() {
# xcbgen is the host's: the copy xcb-proto put in the sysroot
export PYTHONPATH=$(echo $SYSROOT/usr/lib/python3*/site-packages)
at_lib --disable-docs --without-doxygen --disable-devel-docs
}
build_libx11() {
# the keysym/compose tables are built by host tools (makekeys)
at_lib --disable-specs --without-xmlto --without-fop --enable-xthreads --disable-xf86bigfont \
--enable-malloc0returnsnull=no
}
build_libxext() { at_lib --disable-specs --without-xmlto --without-fop --enable-malloc0returnsnull=no; }
build_libsdl2() {
# X11 only (Wayland needs EGL, which wasm32 programs do not have yet), linked
# in; no dlopen, no GL; audio through the dummy driver
at_lib --disable-rpath --disable-sdl-dlopen --disable-assembly --disable-video-wayland \
--disable-video-opengl --disable-video-opengles --disable-video-opengles1 --disable-video-opengles2 \
--disable-video-vulkan --disable-video-kmsdrm --disable-video-rpi --disable-video-vivante \
--disable-x11-shared --disable-video-x11-xinput --disable-video-x11-xrandr --disable-video-x11-scrnsaver \
--disable-video-x11-xcursor --disable-video-x11-xfixes --disable-video-x11-xdbe \
--disable-alsa --disable-pulseaudio --disable-pipewire --disable-jack --disable-sndio --disable-oss \
--disable-esd --disable-arts --disable-nas --disable-diskaudio --disable-libsamplerate \
--disable-libudev --disable-dbus --disable-ime --disable-ibus --disable-fcitx --disable-hidapi \
--disable-joystick-mfi --enable-pthreads
# static X11 pulls in xcb, Xau and Xdmcp: say so in what programs link
local x; x=$(wasm32-unknown-linux-musl-pkg-config --static --libs-only-l xext x11)
sed -i "s|-lX11 -lXext|$x|" $DEST/usr/lib/pkgconfig/sdl2.pc $SYSROOT/usr/lib/pkgconfig/sdl2.pc \
$DEST/usr/bin/sdl2-config $SYSROOT/usr/bin/sdl2-config
}
build_libsdl2_net() { at_lib --disable-gui; }
build_libsdl2_mixer() {
# sound effects (WAV) and MIDI through the built-in timidity; no codecs
# (the dfsg tarball has no configure: autogen.sh's way, without running it)
[ -x configure ] || autoreconf -fi -I acinclude
at_lib --disable-music-cmd --disable-music-flac --disable-music-ogg --disable-music-mp3 \
--disable-music-mod --disable-music-opus --disable-music-midi-fluidsynth --disable-music-wavpack \
--disable-music-gme --enable-music-midi-timidity --enable-music-wave
}
build_chocolate_doom() {
[ -x configure ] || ACLOCAL_PATH=$SYSROOT/usr/share/aclocal autoreconf -fi
at_configure --disable-sdltest --without-libsamplerate --without-libpng --without-fluidsynth \
--disable-werror
make -j$JOBS
make install DESTDIR=$DEST
rm -rf $DEST/usr/share/man $DEST/usr/share/doc
}
# -- tier 2: perl -------------------------------------------------------------
build_perl() {
# Debian's layout and options (debian/config.debian), Configured natively:
# its probes and miniperl run under gexc. No dynamic loading, so every
# extension is built into perl.
local v=5.40 a=/usr/lib/wasm32-linux-musl
# miniperl (under gexc) runs make and the compiler for every extension:
# on the host, not translated (gexc's GEXC_HOST_EXEC)
export GEXC_HOST_EXEC=/usr/bin/make:$GEXCC_PREFIX/bin/
# perl -V names Debian's patches (debian/rules)
local dv; dv=$(dpkg-parsechangelog -S Version)
debian/gen-patchlevel -p DEBPKG: -v $dv debian/patches/series > patchlevel-debian.h
sh ./Configure -des -Dcc=gexcc -Dld=gexcc -Dar=gexcc-ar -Dranlib=gexcc-ranlib \
-Dccflags='-DDEBIAN -D_GNU_SOURCE' -Doptimize=-O2 -Dusethreads -Duselargefiles -Duse64bitint -Uusedl \
-Darchname=wasm32-linux-musl -Dprefix=/usr -Dprivlib=/usr/share/perl/$v -Darchlib=$a/perl/$v \
-Dvendorprefix=/usr -Dvendorlib=/usr/share/perl5 -Dvendorarch=$a/perl5/$v \
-Dsiteprefix=/usr/local -Dsitelib=/usr/local/share/perl/5.40.1 -Dsitearch=/usr/local$a/perl/5.40.1 \
-Dman1dir=none -Dman3dir=none -Dsiteman1dir=none -Dsiteman3dir=none \
-Dpager=/usr/bin/sensible-pager -Uafs -Ud_csh -Ud_ualarm -Uusesfio -Uusenm -Ui_libutil \
-Duseperlio -Dd_dosuid=undef -Dcf_by=Debian -Dcf_email=[email protected] \
-Dperladmin=root@localhost -Dmyhostname=localhost -Dmydomain= -Dlibpth=$SYSROOT/usr/lib
make -j$JOBS
make install DESTDIR=$DEST
rm -rf $DEST/usr/share/man
}
+4
View File
@@ -372,6 +372,10 @@ if not (nostdlib or nodefaultlibs):
# gexcc's runtime: the loop safepoint (rt/poll.c)
if os.path.exists(LIB + '/libgexcc.a'):
libs.insert(0, LIB + '/libgexcc.a')
# thread_local destructors (rt/cxa_thread_atexit.c): libc++abi's
# reference is weak, which pulls nothing out of an archive
if CXX:
ld_flags += ['-u', '__cxa_thread_atexit_impl']
# main(argc, argv, envp): see rt/main3.c
if not nostartfiles and not nostdlib:
cand = objs + [x for x in ins if not x.startswith('-') and not is_archive(x)]
+4
View File
@@ -19,6 +19,10 @@ G=${GEXCC_GEXC:-@GEXC@}
L=@LIBDIR@
slots=${GEXCC_SLOTS:-4}
# Under a gexc that ran a host program for its guest (GEXC_HOST_EXEC: perl's
# miniperl -> make -> miniperl), that gexc holds a slot and waits on this
# one: taking another would deadlock once every slot is such a parent.
[ -n "${GEXC_HOSTED:-}" ] && slots=0
if [ "$slots" -gt 0 ] 2>/dev/null && command -v flock >/dev/null; then
d=${GEXCC_SLOT_DIR:-${XDG_RUNTIME_DIR:-/tmp}/gexcc-slots-$(id -u)}
mkdir -p "$d" 2>/dev/null
+1 -1
View File
@@ -54,7 +54,7 @@ rm -f "$L/hoststub.o" "$L/hoststub.elf"
mkdir -p "$PREFIX/build/rt"
. "$WW/flags.sh"
clang $WASM_CFLAGS --sysroot="$PREFIX/sysroot" -c "$HERE/rt/main3.c" -o "$L/main3.o"
for f in "$HERE"/rt/poll.c "$HERE"/rt/sigsetjmp.c "$HERE"/rt/*.s; do
for f in "$HERE"/rt/poll.c "$HERE"/rt/sigsetjmp.c "$HERE"/rt/cxa_thread_atexit.c "$HERE"/rt/*.s; do
clang $WASM_CFLAGS --sysroot="$PREFIX/sysroot" -c "$f" -o "$PREFIX/build/rt/$(basename "$f").o"
done
rm -f "$PREFIX/sysroot/lib/libgexcc.a"
+60
View File
@@ -0,0 +1,60 @@
// thread_local destructors. libc++abi's __cxa_thread_atexit calls
// __cxa_thread_atexit_impl when the C library has one (glibc) through a weak
// reference; musl has none, and in a -pie wasm32 link the weak reference
// becomes a GOT.func import that nothing provides (apt's programs failed to
// instantiate). gexc++ links this one in (-u): a per-thread list, run in
// reverse order at thread exit (a pthread key's destructor) and, for the
// thread that calls exit(), from an atexit handler.
#include <pthread.h>
#include <stdlib.h>
struct dtor {
void (*fn)(void *);
void *obj;
struct dtor *next;
};
static pthread_key_t key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void run(struct dtor *d) {
while (d) {
struct dtor *n = d->next;
d->fn(d->obj);
free(d);
d = n;
}
}
// a destructor may register more: each pass takes the list as it stands
static void drain(void) {
struct dtor *d;
while ((d = pthread_getspecific(key))) {
pthread_setspecific(key, 0);
run(d);
}
}
static void at_thread_exit(void *p) {
run(p);
drain();
}
static void at_exit(void) { drain(); }
static void init(void) {
pthread_key_create(&key, at_thread_exit);
atexit(at_exit);
}
int __cxa_thread_atexit_impl(void (*fn)(void *), void *obj, void *dso) {
(void)dso;
struct dtor *d = malloc(sizeof *d);
if (!d) return -1;
pthread_once(&once, init);
d->fn = fn;
d->obj = obj;
d->next = pthread_getspecific(key);
pthread_setspecific(key, d);
return 0;
}
+30 -2
View File
@@ -409,8 +409,10 @@ const nativeDebian = {
title: 'Debian, built from source to wasm',
blurb:
"Debian trixie whose tools (bash, coreutils, grep, sed, vim...) are built from Debian's own " +
'sources with gexcc into native wasm32 programs. apt, dpkg and the libraries stay Debian\'s ' +
'arm64 builds, translated; `apt install` fetches arm64 packages over Wisp.',
'sources with gexcc into native wasm32 programs, dpkg and apt included; `apt install` ' +
'fetches Debian\'s arm64 packages over Wisp and runs them translated. `?gui=1` adds a ' +
'screen (Wayland, and X through the page\'s X server) for foot, Chocolate Doom and the ' +
'other GUI programs.',
network: true,
async enter(ctx) {
@@ -427,10 +429,30 @@ const nativeDebian = {
},
};
/** `?gui=1`: the compositor's output above the terminal, for the Wayland
* programs in the image (foot...). Made once per visit. */
function nativeDebianScreen(ctx) {
let el = document.getElementById('gex-wl');
if (el) return el;
const wrap = document.createElement('div');
wrap.className = 'canvas-wrap';
el = document.createElement('canvas');
el.id = 'gex-wl';
el.width = 1024;
el.height = 640;
el.tabIndex = 0;
el.classList.add('wl-output');
wrap.append(el);
document.getElementById('terminal')?.before(wrap);
ctx.onLeave(() => wrap.remove());
return el;
}
async function bootNativeDebian(ctx) {
document.getElementById('nboot').disabled = true;
const q = new URLSearchParams(location.search);
const cmd = q.get('cmd');
const screen = q.get('gui') === '1' ? nativeDebianScreen(ctx) : null;
const t0 = performance.now();
try {
const tree = await ctx.rootfs(q.get('rootfs') || '/rootfs/native-debian.tar.zst');
@@ -473,8 +495,14 @@ async function bootNativeDebian(ctx) {
'TERM=xterm-256color',
'DEBIAN_FRONTEND=noninteractive',
'LANG=C.UTF-8',
...(screen ? ['XDG_RUNTIME_DIR=/tmp', 'WAYLAND_DISPLAY=wayland-0', 'DISPLAY=:0'] : []),
],
args: cmd ? ['/bin/bash', '/cmd.sh'] : ['/bin/bash', '-l', '-i'],
wl: screen ?? undefined,
wlInput: screen ?? undefined,
// X clients (SDL programs...) reach the page's X server, which draws
// into the same compositor; `?hostx=0` leaves it out.
hostX: !!screen && q.get('hostx') !== '0',
cwd: '/root',
tty: true,
network,
+7 -5
View File
@@ -584,11 +584,13 @@ export class HostX {
const p = hw.hw_scratch(SLOTS * REC * 4);
const n = hw.hw_scene(p, SLOTS);
this.#proxy.publish(this.#hwMem, p, n);
// The first publish, and every hundredth: a scene that never leaves here
// and a scene the page never draws look the same from the page.
// The first few always, then one in a hundred: the interesting ones are
// at the start (a window appearing) and a busy client publishes forever.
if (this.#published++ < 5 || this.#published % 100 === 0) {
// The first few, then at 10, 100, 1000...: a scene that never leaves
// here and a scene the page never draws look the same from the page, and
// the interesting ones are at the start (a window appearing). A game
// publishes forever, and one line in a hundred was a line every three
// seconds in its terminal.
const k = this.#published++;
if (k < 5 || (k >= 10 && /^10*$/.test(String(k)))) {
this.log(`[hostx] published ${n} surface(s) as token ${this.#proxy.token}`
+ `${this.#proxy.ok ? '' : ' (no block in the arena!)'}`);
}
+12 -6
View File
@@ -12,8 +12,11 @@ const OPEN = 1;
const CLOSED = 2;
const REFUSED = 3;
/** How much may pile up unread on one stream before the page stops reading. */
const HIGH_WATER = 4 << 20;
/** How much may pile up unread on one stream before the page says so. The
* Wisp client cannot be told to stop reading, so past this the bytes are
* kept anyway: dropping them is what turned a 9.6 MB download into a Hash
* Sum mismatch. */
const HIGH_WATER = 16 << 20;
class Stream {
constructor(id) {
@@ -117,13 +120,13 @@ export class WispNetwork {
if (!s.firstRecv) {
s.firstRecv = now;
const waited = s.firstSend ? (now - s.firstSend) / 1000 : -1;
console.error(
console.info(
`[wisp] stream ${id}: first reply at ${(now / 1000).toFixed(1)}s, `
+ `${waited.toFixed(1)}s after the request`,
);
}
if (meter.last && now - meter.last > 1000) {
console.error(
console.info(
`[wisp] nothing for ${((now - meter.last) / 1000).toFixed(1)}s `
+ `after ${meter.n} messages and `
+ `${(meter.bytes / 1048576).toFixed(1)} MiB`,
@@ -132,7 +135,10 @@ export class WispNetwork {
meter.last = now;
meter.n += 1;
meter.bytes += bytes.length;
if (s.buffered > HIGH_WATER) return;
if (s.buffered > HIGH_WATER && !s.warned) {
s.warned = true;
console.warn(`[wisp] stream ${id}: ${s.buffered >> 20} MiB unread`);
}
s.chunks.push(bytes);
s.buffered += bytes.length;
};
@@ -166,7 +172,7 @@ export class WispNetwork {
// drains.
s.sends = (s.sends ?? 0) + 1;
if (!s.firstSend) s.firstSend = performance.now();
console.error(
console.info(
`[wisp] stream ${id}: send ${s.sends}, ${bytes.length} bytes at `
+ `${(performance.now() / 1000).toFixed(1)}s`,
);
+15 -1
View File
@@ -500,6 +500,17 @@ export async function runProgram(
}
}
// The network: the page's Wisp connection, which every Worker calls across
// shared memory. A WebSocket in the guest's own Worker delivers on that
// Worker's event loop, which a wasm32 program waiting in a syscall never
// gives back.
let netChannel = null;
if (network && globalThis.crossOriginIsolated) {
const { makeNetChannel, serveNet } = await import('@gex/net-bridge.mjs');
netChannel = makeNetChannel();
if (netChannel) serveNet(network, netChannel);
}
// The Wayland compositor. The protocol state machine runs in each guest
// process (the Rust shim, next to the client's memory, which is the only
// place that can read a wl_shm pool); the scene, the output and the input
@@ -641,7 +652,10 @@ export async function runProgram(
// The Wayland ring's shared buffers, so a Worker that binds the
// compositor can carry its frames to the page's canvas. Like glChannel.
wlChannel: wlChannel ?? undefined,
wispUrl: network?.url ?? undefined,
// The page's network, served to every Worker over shared memory
// (net-bridge.mjs); `wispUrl` only where the page cannot serve it.
netChannel: netChannel ?? undefined,
wispUrl: netChannel ? undefined : network?.url ?? undefined,
// Only when there is no shared terminal to hold the size. With one this
// must not be sent: it is captured at `runProgram` time and every Worker
// writes it on startup, so a process forked after a resize would reset the
+29
View File
@@ -171,6 +171,8 @@ function parseArgs(argv) {
else if (a === '--shot') o.shot = next();
else if (a === '--gl-capture') o.glCapture = next();
else if (a === '--input') o.input = true;
else if (a === '--wltype') o.wltype = next();
else if (a === '--wltype-wait') o.wltypeWait = Number(next()) * 1000;
else if (a === '--applit') o.applit = true;
else if (a === '--wllit') o.wllit = true;
else if (a === '--wlwindows') o.wlwindows = Number(next());
@@ -731,6 +733,33 @@ async function runDemo(o) {
}
}
// --wltype TEXT: once passed, type TEXT and Enter onto the Wayland canvas as a
// person would (keydown/keyup with \`code\`s the evdev table maps; letters,
// digits, space and - . / only), then wait so the shot shows the answer.
if (o.wltype && verdict?.how === 'pass') {
const codes = [...o.wltype].map((ch) =>
/[a-z]/.test(ch) ? `Key${ch.toUpperCase()}`
: /[0-9]/.test(ch) ? `Digit${ch}`
: ({ ' ': 'Space', '-': 'Minus', '.': 'Period', '/': 'Slash' })[ch]).concat(['Enter']);
const js = `(() => {
const cv = document.querySelector('.canvas-wrap canvas:not(.wl-offscreen)');
if (!cv) return 'no visible app canvas';
cv.focus?.();
for (const code of ${JSON.stringify(codes)}) {
cv.dispatchEvent(new KeyboardEvent('keydown', { code, bubbles: true }));
cv.dispatchEvent(new KeyboardEvent('keyup', { code, bubbles: true }));
}
return 'typed ' + ${JSON.stringify(codes.length)} + ' keys';
})()`;
try {
const r = await browser.send('Runtime.evaluate', { expression: js, returnByValue: true }, page);
log(` wltype: ${r?.result?.value ?? '?'}`);
} catch (e) {
log(` wltype failed: ${e?.message ?? e}`);
}
await new Promise((r) => setTimeout(r, o.wltypeWait ?? 5000));
}
// --- report ---------------------------------------------------------------
const secs = ((Date.now() - began) / 1000).toFixed(1);
let shotPath = o.shot ?? join(STATE, `shot-${o.demo}.png`);