Skip to content

Add Android platform support#306

Draft
tonyfettes wants to merge 7 commits intomainfrom
worktree-android
Draft

Add Android platform support#306
tonyfettes wants to merge 7 commits intomainfrom
worktree-android

Conversation

@tonyfettes
Copy link

@tonyfettes tonyfettes commented Feb 28, 2026

Summary

  • pidfd fallback: wait_pid now tries pidfd_open first, falls back to blocking waitpid in a worker thread when unavailable (graceful degradation on any Linux kernel < 5.3, not just Android)
  • addchdir_np runtime guard: On Android, checks availability via dlsym at runtime (API 34+ only), returns ENOSYS if unavailable
  • Thread stack size: Sets 64KB minimum on Android (the default 512 bytes is below Android's enforced minimum)
  • TLS/BoringSSL compat: Android-specific libssl.so loading, SSLeay fallback for version detection, SSL_CTX_load_verify_locations for /system/etc/security/cacerts/
  • Temp directory: Replaces hardcoded /tmp/ with a C function that checks $TMPDIR and falls back to /data/local/tmp/ on Android

Approach

All Android-specific logic is in C via #ifdef __ANDROID__. MoonBit code stays #cfg(not(platform="windows")) for Unix — no MoonBit-level Android guards needed.

Test plan

  • moon check passes
  • moon build --target native compiles cleanly
  • moon test --target native — all tests pass (no regressions)
  • Cross-compile with Android NDK for aarch64 and verify linking
  • Run on Android emulator: event loop, TCP sockets, file I/O, process spawn, process wait
  • Verify TLS loading behavior on Android
  • Verify posix_spawn with cwd on API < 34 returns clear ENOSYS error

🤖 Generated with Claude Code

@coveralls
Copy link

coveralls commented Feb 28, 2026

Pull Request Test Coverage Report for Build 92

Details

  • 22 of 34 (64.71%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage decreased (-0.2%) to 80.683%

Changes Missing Coverage Covered Lines Changed/Added Lines %
src/internal/event_loop/process_unix.mbt 7 13 53.85%
src/process/process.mbt 14 20 70.0%
Totals Coverage Status
Change from base Build 86: -0.2%
Covered Lines: 2243
Relevant Lines: 2780

💛 - Coveralls

Comment on lines +210 to +233
// Android's /system/etc/security/cacerts/ uses MD5-based hash filenames,
// but OpenSSL 3.x directory lookup expects SHA1-based hashes. Bypass hash
// lookup by iterating the directory and loading each cert directly.
if (PEM_read_X509 && X509_STORE_add_cert && X509_free) {
X509_STORE *store = SSL_CTX_get_cert_store(client_ctx);
DIR *dir = opendir("/system/etc/security/cacerts");
if (dir) {
struct dirent *entry;
char path[512];
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') continue;
snprintf(path, sizeof(path), "/system/etc/security/cacerts/%s", entry->d_name);
FILE *fp = fopen(path, "r");
if (fp) {
X509 *cert = PEM_read_X509(fp, NULL, NULL, NULL);
if (cert) {
X509_STORE_add_cert(store, cert);
X509_free(cert);
}
fclose(fp);
}
}
closedir(dir);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We iterate the directory and loads all certificates here. The "correct" approach would be hook into JNI to get certificates, but it is way to complex here.

@tonyfettes
Copy link
Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a619abcc2c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

tonyfettes and others added 6 commits March 2, 2026 16:06
- pidfd fallback: try pidfd_open, fall back to blocking waitpid in
  worker thread when unavailable (benefits all Linux < 5.3)
- guard posix_spawn_file_actions_addchdir_np with runtime dlsym check
  on Android (only available on API 34+, returns ENOSYS otherwise)
- set 64KB thread stack size on Android (512 bytes is below minimum)
- TLS: add Android libssl.so dlopen path, BoringSSL SSLeay version
  compat, SSL_CTX_load_verify_locations for /system/etc/security/cacerts/
- replace hardcoded /tmp/ with C function that checks $TMPDIR and
  falls back to /data/local/tmp/ on Android

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
openssl.c: Android's /system/etc/security/cacerts/ uses MD5-based hash
filenames (OpenSSL 1.0 era), but OpenSSL 3.x directory lookup expects
SHA1-based hashes. The previous code relied on SSL_CTX_set_default_verify_paths
(which always returns 1) with a fallback to SSL_CTX_load_verify_locations using
the hash-based CApath — both fail due to the hash mismatch.

Fix by dlopen'ing libcrypto.so for PEM_read_X509/X509_STORE_add_cert/X509_free,
then iterating the Android CA cert directory and loading each cert directly into
the X509_STORE, bypassing hash-based lookup entirely.

thread_pool.c: Guard #include <spawn.h> and all posix_spawn usage with
#if !defined(__ANDROID__) || __ANDROID_API__ >= 28. Provide a stub
moonbitlang_async_make_spawn_job that returns NULL on older Android,
eliminating the need for per-app posix_spawn_compat.c workarounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When posix_spawn is unavailable (Android API < 28), make_spawn_job
previously returned NULL, causing the worker thread to exit silently
without sending a completion notification. This left the caller in
perform_job_in_worker waiting forever.

Return a proper job struct with a worker function that sets err = ENOSYS,
so the caller receives a clear "Function not implemented" error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When pidfd_open is unavailable (Android, Linux <5.3), the fallback
waitpid in the worker thread reaps the child but discards the exit
status. The subsequent get_process_result then calls waitpid again,
which fails with ECHILD. Store the exit code in job->ret during the
fallback waitpid and propagate it back via wait_pid's return type
(Unit -> Int?) so callers skip the redundant second waitpid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ory leak

The #owned parameters (path, args, envp, cwd) were silently discarded
without moonbit_decref(), leaking memory on every Job::spawn call on
Android API < 28.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tonyfettes
Copy link
Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c65cd3d809

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +58 to +60
let exit_code = perform_job_in_worker(job, context~, cancel=_ => {
job.cancel_process_waiter()
NeedWait

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel fallback wait worker when task is cancelled

When register_pid fails (for example on kernels without pidfd_open), this fallback calls perform_job_in_worker with a cancel callback that only sets job.cancel_process_waiter() and returns NeedWait. In EventLoop::wait_for_job, NeedWait suppresses the cancellation until the worker completes, and because this callback never invokes evloop.cancel_job_in_worker(job_id, ...), the blocking waitpid is not interrupted, so @process.run/@process.spawn cancellation handlers do not run until the child exits naturally.

Useful? React with 👍 / 👎.

Comment on lines +95 to +96
const char *tmpdir = getenv("TMPDIR");
path = tmpdir ? tmpdir : "/data/local/tmp/";

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ensure TMPDIR has trailing slash before using as base path

This uses $TMPDIR verbatim on Android, but tmpdir() builds paths by directly concatenating tmp_base_path with the generated name, so a common value like /data/local/tmp (no trailing slash) produces malformed paths such as /data/local/tmpprefix... and can create directories in the wrong location or fail unexpectedly. Normalize the base path (or append / if missing) before returning it.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants