⋯ 14 lines Show the 14 lines above this hunk @@ -15,6 +15,19 @@ pub fn build(b: *std.Build) void { 15 15 // to avoid Zig bug where --sysroot breaks @cImport (ziglang/zig#22704, #25010) 16 16 const macos_sdk = b.option([]const u8, "macos-sdk", "macOS SDK path for cross-compilation"); 17 17 18 + // The Android NDK, needed by the JNI library and nothing else. An explicit 19 + // path rather than a read of ANDROID_NDK_HOME, matching -Dmacos-sdk: the 20 + // caller passes "$ANDROID_NDK_HOME" when that is what they mean, and a 21 + // build that depends on an environment variable is a build that behaves 22 + // differently in two shells for reasons neither of them shows. 23 + // 24 + // It stopped being optional because without bionic the library builds, 25 + // ships, and then fails to load — std references getauxval and 26 + // __tls_get_addr, both of which live there, and Zig answers "unable to 27 + // provide libc" for an android target. The failure was a silent fallback 28 + // to the Kotlin shim, which is the quietest of the three outcomes. 29 + const android_ndk = b.option([]const u8, "android-ndk", "Android NDK root, required by build-android"); 30 + 18 31 // Set when the opt-in JavaScript tests are wired up, so `test` can include 19 32 // them without the declaration being scoped inside the `if`. 20 33 var js_test_run: ?*std.Build.Step.Run = null; ⋯ 2,370 lines Show the 2,370 lines above this hunk @@ -2391,6 +2404,11 @@ pub fn build(b: *std.Build) void { 2391 2404 .cpu_arch = .aarch64, 2392 2405 .os_tag = .linux, 2393 2406 .abi = .android, 2407 + // Named explicitly, because Zig's default is 29 and the stubs this 2408 + // links against come from the API-26 directory. A target and a sysroot 2409 + // that disagree is how a library binds a symbol the device does not 2410 + // have. 2411 + .android_api_level = android_api_level, 2394 2412 }); 2395 2413 2396 2414 const android_arm64_lib = b.addLibrary(.{ ⋯ 28 lines Show the 28 lines above this hunk @@ -2425,46 +2443,74 @@ pub fn build(b: *std.Build) void { 2425 2443 // the arch-suffixed name beside it: the loader picks the ABI directory, 2426 2444 // and a per-arch name would make the Kotlin need to know its own 2427 2445 // architecture. 2428 - const android_arm64_jni = b.addLibrary(.{ 2429 - .linkage = . dynamic, 2430 - .name = "craft", 2431 - .root_module = b.createModule(.{ 2432 - . root_source_file = b. path ( "src/android_dispatch . zig"), 2433 - .target = android_arm64_target, 2434 - .optimize = optimize, 2435 - }), 2436 - }); 2446 + if (android_ndk) |ndk| { 2447 + const android_arm64_jni = b . addLibrary(.{ 2448 + .linkage = .dynamic, 2449 + .name = "craft", 2450 + . root_module = b. createModule (. { 2451 + .root_source_file = b.path("src/android_dispatch.zig"), 2452 + .target = android_arm64_target, 2453 + .optimize = optimize, 2454 + }), 2455 + }); 2456 + linkAndroidLibc(b, android_arm64_jni, ndk, "aarch64-linux-android"); 2437 2457 2438 - const android_arm64_jni_install = b.addInstallArtifact(android_arm64_jni, .{ 2439 - .dest_dir = .{ .override = .{ .custom = "android/arm64-v8a" } }, 2440 - }); 2441 - build_android.dependOn(&android_arm64_jni_install.step); 2442 - build_android_all.dependOn(&android_arm64_jni_install.step); 2458 + const android_arm64_jni_install = b.addInstallArtifact(android_arm64_jni, .{ 2459 + .dest_dir = .{ .override = .{ .custom = "android/arm64-v8a" } }, 2460 + }); 2461 + build_android.dependOn(&android_arm64_jni_install.step); 2462 + build_android_all.dependOn(&android_arm64_jni_install.step); 2463 + } else { 2464 + // Not built at all rather than built without bionic. The library would 2465 + // compile, install, ship in the APK and then fail at `dlopen` with two 2466 + // unresolved symbols, at which point `CraftNative` catches the 2467 + // `UnsatisfiedLinkError` and every action quietly uses the Kotlin shim. 2468 + // A step that refuses, naming the flag, is the same information 2469 + // delivered where someone can act on it. 2470 + // 2471 + // A `Fail` step rather than a panic, so this is only reached by 2472 + // *asking* for an Android build: `zig build test` on a machine with no 2473 + // NDK must still work, and it did not while this was a configure-time 2474 + // panic. 2475 + const missing = b.addFail( 2476 + "build-android needs the NDK: pass -Dandroid-ndk=\"$ANDROID_NDK_HOME\" " ++ 2477 + "(sdkmanager --install \"ndk;26.1.10909125\"). Without bionic the JNI library " ++ 2478 + "builds and then loads nowhere — std references getauxval and __tls_get_addr, " ++ 2479 + "both of which live there, and Zig cannot supply libc for an android target.", 2480 + ); 2481 + build_android.dependOn(&missing.step); 2482 + build_android_x86.dependOn(&missing.step); 2483 + build_android_all.dependOn(&missing.step); 2484 + } 2443 2485 2444 2486 // Android Emulator (x86_64) 2445 2487 const android_x86_target = b.resolveTargetQuery(.{ 2446 2488 .cpu_arch = .x86_64, 2447 2489 .os_tag = .linux, 2448 2490 .abi = .android, 2491 + .android_api_level = android_api_level, 2449 2492 }); 2450 2493 2451 - const android_x86_jni = b.addLibrary(.{ 2452 - .linkage = . dynamic, 2453 - .name = "craft", 2454 - .root_module = b.createModule(.{ 2455 - . root_source_file = b. path ( "src/android_dispatch . zig"), 2456 - .target = android_x86_target, 2457 - .optimize = optimize, 2458 - }), 2459 - }); 2494 + if (android_ndk) |ndk| { 2495 + const android_x86_jni = b . addLibrary(.{ 2496 + .linkage = .dynamic, 2497 + .name = "craft", 2498 + . root_module = b. createModule (. { 2499 + .root_source_file = b.path("src/android_dispatch.zig"), 2500 + .target = android_x86_target, 2501 + .optimize = optimize, 2502 + }), 2503 + }); 2504 + linkAndroidLibc(b, android_x86_jni, ndk, "x86_64-linux-android"); 2460 2505 2461 - // `x86_64`, matching the jniLibs directory the loader looks in — not 2462 - // `x86`, which is the 32-bit ABI and would be silently ignored. 2463 - const android_x86_jni_install = b.addInstallArtifact(android_x86_jni, .{ 2464 - .dest_dir = .{ .override = .{ .custom = "android/x86_64" } }, 2465 - }); 2466 - build_android_x86.dependOn(&android_x86_jni_install.step); 2467 - build_android_all.dependOn(&android_x86_jni_install.step); 2506 + // `x86_64`, matching the jniLibs directory the loader looks in — not 2507 + // `x86`, which is the 32-bit ABI and would be silently ignored. 2508 + const android_x86_jni_install = b.addInstallArtifact(android_x86_jni, .{ 2509 + .dest_dir = .{ .override = .{ .custom = "android/x86_64" } }, 2510 + }); 2511 + build_android_x86.dependOn(&android_x86_jni_install.step); 2512 + build_android_all.dependOn(&android_x86_jni_install.step); 2513 + } 2468 2514 2469 2515 const android_x86_lib = b.addLibrary(.{ 2470 2516 .linkage = .static, ⋯ 53 lines Show the 53 lines above this hunk @@ -2524,6 +2570,112 @@ pub fn build(b: *std.Build) void { 2524 2570 /// Link platform-specific system libraries for a build module. 2525 2571 /// Centralizes the per-OS library linking that is shared across exe, craft_exe, 2526 2572 /// lib_unit_tests, and cross-compilation targets. 2573 + /// The Android API level the JNI library targets. 2574 + /// 2575 + /// Must not exceed the generator's default `minSdk`, because one prebuilt 2576 + /// library serves every generated app: building against a newer API would let 2577 + /// the linker bind symbols that are simply absent on an older device, and the 2578 + /// failure is `dlopen` refusing the library at startup on exactly the phones 2579 + /// the app claims to support. `packages/android/src/index.test.ts` asserts the 2580 + /// two numbers agree, since nothing else connects a Zig constant to a 2581 + /// TypeScript one. 2582 + const android_api_level: u32 = 26; 2583 + 2584 + /// The NDK's sysroot, or null when `ndk` does not contain one. 2585 + /// 2586 + /// The prebuilt directory is named for the *host* and the NDK ships exactly 2587 + /// one, so the candidates are tried in turn rather than guessed from 2588 + /// `builtin.os`. An Apple-silicon Mac still gets `darwin-x86_64` from most NDK 2589 + /// releases, which is the guess that would have been wrong. 2590 + /// 2591 + /// Null rather than a panic: this runs at configure time, so a panic here 2592 + /// would break `zig build test` for anyone who passed a wrong -Dandroid-ndk, 2593 + /// not just the Android steps. 2594 + fn androidSysroot(b: *std.Build, ndk: []const u8) ?[]const u8 { 2595 + const io = b.graph.io; 2596 + for ([_][]const u8{ "darwin-x86_64", "darwin-arm64", "linux-x86_64", "windows-x86_64" }) |host| { 2597 + const candidate = b.pathJoin(&.{ ndk, "toolchains/llvm/prebuilt", host, "sysroot" }); 2598 + std.Io.Dir.cwd().access(io, candidate, .{}) catch continue; 2599 + return candidate; 2600 + } 2601 + return null; 2602 + } 2603 + 2604 + /// Link one Android artifact against the NDK's bionic. 2605 + /// 2606 + /// A `--libc` file, not include and library paths. Paths alone are what 2607 + /// `applySdkPaths` does for Apple SDKs and they are not enough here: with 2608 + /// `link_libc` set and no libc file, Zig looks the target up in its own table 2609 + /// of libcs it can build, does not find android, and stops with 2610 + /// 2611 + /// error: unable to provide libc for target "x86_64-linux...-android.29" 2612 + /// 2613 + /// before it ever consults a search path. The libc file is how Zig is told the 2614 + /// libc already exists. 2615 + /// 2616 + /// `gcc_dir` is not optional, and its absence is reported under the wrong 2617 + /// name. For any `.linux` target — Android is one — Zig takes crtbegin and 2618 + /// crtend from `gcc_dir`, not `crt_dir`, and returns 2619 + /// `LibCInstallationMissingCrtDir` when `gcc_dir` is empty 2620 + /// (std/zig/LibCInstallation.zig, `resolveCrtPaths`). So a file with a correct 2621 + /// `crt_dir` and a blank `gcc_dir` fails with "libc installation is missing crt 2622 + /// directory", which sends you checking a directory that is fine. Bionic keeps 2623 + /// crtbegin_so.o and crtend_so.o beside its stubs, so both fields point at the 2624 + /// same API-level directory. Zig 0.17.0-dev.1441 read them from `crt_dir`, 2625 + /// which is why this passed locally and failed on CI's 1963. 2626 + /// 2627 + /// Still not `--sysroot`, which breaks `@cImport` (ziglang/zig#22704, #25010) 2628 + /// and is why the Apple side takes the path route too. 2629 + fn linkAndroidLibc(b: *std.Build, compile: *std.Build.Step.Compile, ndk: []const u8, triple: []const u8) void { 2630 + const io = b.graph.io; 2631 + 2632 + const sysroot = androidSysroot(b, ndk) orelse { 2633 + compile.step.dependOn(&b.addFail(b.fmt( 2634 + "-Dandroid-ndk={s} has no toolchains/llvm/prebuilt/<host>/sysroot. " ++ 2635 + "Point it at an NDK root, not at the SDK.", 2636 + .{ndk}, 2637 + )).step); 2638 + return; 2639 + }; 2640 + 2641 + const api = b.fmt("{d}", .{android_api_level}); 2642 + const lib_dir = b.pathJoin(&.{ sysroot, "usr/lib", triple, api }); 2643 + 2644 + // Checked here so a missing API level fails with the path and the fix, 2645 + // rather than as a linker error about crtbegin_so.o. Lazy, like the NDK 2646 + // check: only a build that asks for this artifact is refused. 2647 + std.Io.Dir.cwd().access(io, b.pathJoin(&.{ lib_dir, "crtbegin_so.o" }), .{}) catch { 2648 + compile.step.dependOn(&b.addFail(b.fmt( 2649 + "no crtbegin_so.o in {s}: this NDK does not carry API {s} for {s}. " ++ 2650 + "Install one that does, or lower android_api_level.", 2651 + .{ lib_dir, api, triple }, 2652 + )).step); 2653 + return; 2654 + }; 2655 + 2656 + const libc_file = b.addWriteFile("android-libc.conf", b.fmt( 2657 + \\include_dir={s} 2658 + \\sys_include_dir={s} 2659 + \\crt_dir={s} 2660 + \\msvc_lib_dir= 2661 + \\kernel32_lib_dir= 2662 + \\gcc_dir={s} 2663 + \\ 2664 + , .{ 2665 + b.pathJoin(&.{ sysroot, "usr/include" }), 2666 + // The triple-specific headers: bionic keeps its architecture-dependent 2667 + // definitions there and the generic ones include them by relative path. 2668 + b.pathJoin(&.{ sysroot, "usr/include", triple }), 2669 + lib_dir, 2670 + lib_dir, 2671 + })); 2672 + 2673 + compile.setLibCFile(libc_file.getDirectory().path(b, "android-libc.conf")); 2674 + compile.step.dependOn(&libc_file.step); 2675 + compile.root_module.link_libc = true; 2676 + compile.root_module.addLibraryPath(.{ .cwd_relative = lib_dir }); 2677 + } 2678 + 2527 2679 /// The iOS SDK sysroot, resolved through xcrun at configure time. 2528 2680 /// 2529 2681 /// Needed because the iOS archives compile the vendored sqlite3.c, and C