diff --git a/lib/mob_dev/native_build.ex b/lib/mob_dev/native_build.ex index 6674454..c8e97e8 100644 --- a/lib/mob_dev/native_build.ex +++ b/lib/mob_dev/native_build.ex @@ -495,20 +495,10 @@ defmodule MobDev.NativeBuild do end end - defp ndk_sysroot do - # NDK ships only one prebuilt — darwin-x86_64 even on Apple Silicon - # (Apple's Rosetta 2 covers it). On Linux it'd be linux-x86_64. - host = - case :os.type() do - {:unix, :darwin} -> "darwin-x86_64" - {:unix, :linux} -> "linux-x86_64" - other -> raise "unsupported host for NDK: #{inspect(other)}" - end - - sdk_root = System.get_env("ANDROID_HOME") || Path.expand("~/Library/Android/sdk") - ndk_version = MobDev.NdkVersion.effective() - Path.join([sdk_root, "ndk", ndk_version, "toolchains", "llvm", "prebuilt", host, "sysroot"]) - end + # Single source of truth in MobDev.NdkVersion (honors ANDROID_HOME / + # ANDROID_SDK_ROOT + host detection) — shared with cpp_archive / nx_eigen_nif + # so the NDK path can't diverge again (MOB-89). + defp ndk_sysroot, do: MobDev.NdkVersion.sysroot() defp resolve_driver_tab_android(mob_dir) do resolve_driver_tab(mob_dir, "android", ["android", "jni"]) diff --git a/lib/mob_dev/ndk_version.ex b/lib/mob_dev/ndk_version.ex index c26c125..4989c25 100644 --- a/lib/mob_dev/ndk_version.ex +++ b/lib/mob_dev/ndk_version.ex @@ -189,15 +189,47 @@ defmodule MobDev.NdkVersion do default_sdk_root_for_os() end - defp default_sdk_root_for_os do + @doc false + # The Android NDK toolchain host tag — the NDK ships a single prebuilt + # (darwin-x86_64 even on Apple Silicon; Apple's Rosetta 2 covers it). + @spec host() :: String.t() + def host do case :os.type() do - {:unix, :darwin} -> Path.expand("~/Library/Android/sdk") - {:unix, _} -> Path.expand("~/Android/Sdk") - _ -> nil + {:unix, :darwin} -> "darwin-x86_64" + {:unix, :linux} -> "linux-x86_64" + other -> raise "unsupported host for NDK: #{inspect(other)}" end - |> then(fn path -> - if path && File.dir?(path), do: path, else: nil - end) + end + + @doc false + # NDK root for the effective version, honoring ANDROID_HOME / ANDROID_SDK_ROOT + # (falls back to the OS-conventional SDK dir even when it doesn't exist, so a + # "toolchain not found at " error still names a sensible location). The + # single source of truth for `native_build`, `cpp_archive`, and `nx_eigen_nif` + # — none of them should re-derive this (see MOB-89). + @spec root() :: String.t() + def root, do: Path.join([sdk_root() || os_default_sdk_dir(), "ndk", effective()]) + + @doc false + @spec toolchain_bin() :: String.t() + def toolchain_bin, do: Path.join([root(), "toolchains", "llvm", "prebuilt", host(), "bin"]) + + @doc false + @spec sysroot() :: String.t() + def sysroot, do: Path.join([root(), "toolchains", "llvm", "prebuilt", host(), "sysroot"]) + + # The OS-conventional SDK dir (path only, no existence check) — used by + # `root/0` as the last-resort fallback so error messages name a real path. + defp os_default_sdk_dir do + case :os.type() do + {:unix, :linux} -> Path.expand("~/Android/Sdk") + _ -> Path.expand("~/Library/Android/sdk") + end + end + + defp default_sdk_root_for_os do + path = os_default_sdk_dir() + if File.dir?(path), do: path, else: nil end @doc """ diff --git a/lib/mob_dev/nx_eigen_nif.ex b/lib/mob_dev/nx_eigen_nif.ex index b8cd730..c5115ac 100644 --- a/lib/mob_dev/nx_eigen_nif.ex +++ b/lib/mob_dev/nx_eigen_nif.ex @@ -455,12 +455,13 @@ defmodule MobDev.NxEigenNif do defp ios_min_version_flag(:ios_sim), do: "-mios-simulator-version-min=#{@ios_min_version}" defp ios_min_version_flag(:ios_device), do: "-miphoneos-version-min=#{@ios_min_version}" - defp default_ndk_root do - Path.join([System.user_home!(), "Library/Android/sdk/ndk", NdkVersion.effective()]) - end + # Honors ANDROID_HOME / ANDROID_SDK_ROOT via the shared NdkVersion helper (MOB-89). + defp default_ndk_root, do: NdkVersion.root() defp android_toolchain_bin(opts) do - ndk_root = opts[:ndk_root] || default_ndk_root() - Path.join([ndk_root, "toolchains/llvm/prebuilt/darwin-x86_64/bin"]) + case opts[:ndk_root] do + nil -> NdkVersion.toolchain_bin() + root -> Path.join([root, "toolchains", "llvm", "prebuilt", NdkVersion.host(), "bin"]) + end end end diff --git a/lib/mob_dev/plugin/cpp_archive.ex b/lib/mob_dev/plugin/cpp_archive.ex index 0e93ff5..e2a1f44 100644 --- a/lib/mob_dev/plugin/cpp_archive.ex +++ b/lib/mob_dev/plugin/cpp_archive.ex @@ -289,12 +289,15 @@ defmodule MobDev.Plugin.CppArchive do defp ios_min_flag(:ios_sim), do: "-mios-simulator-version-min=#{@ios_min_version}" defp ios_min_flag(:ios_device), do: "-miphoneos-version-min=#{@ios_min_version}" - defp default_ndk_root do - Path.join([System.user_home!(), "Library/Android/sdk/ndk", NdkVersion.effective()]) - end + # Honors ANDROID_HOME / ANDROID_SDK_ROOT via the shared NdkVersion helper — + # do NOT re-derive the SDK path here (MOB-89: this used to hardcode + # ~/Library/Android/sdk, breaking builds where the NDK lives elsewhere). + defp default_ndk_root, do: NdkVersion.root() defp android_toolchain_bin(opts) do - ndk_root = opts[:ndk_root] || default_ndk_root() - Path.join([ndk_root, "toolchains/llvm/prebuilt/darwin-x86_64/bin"]) + case opts[:ndk_root] do + nil -> NdkVersion.toolchain_bin() + root -> Path.join([root, "toolchains", "llvm", "prebuilt", NdkVersion.host(), "bin"]) + end end end diff --git a/test/mob_dev/ndk_version_test.exs b/test/mob_dev/ndk_version_test.exs index 2e228b8..2af8c17 100644 --- a/test/mob_dev/ndk_version_test.exs +++ b/test/mob_dev/ndk_version_test.exs @@ -149,4 +149,51 @@ defmodule MobDev.NdkVersionTest do assert cmd =~ NdkVersion.recommended() end end + + # The single source of truth cpp_archive / nx_eigen_nif / native_build all use. + # MOB-89: cpp_archive + nx_eigen_nif used to hardcode ~/Library/Android/sdk, + # ignoring ANDROID_HOME — so a cpp_archive build failed wherever the NDK lived + # elsewhere. These pin that root/sysroot/toolchain honor the SDK env. + describe "root/0 + host/0 + sysroot/0 (shared NDK path — MOB-89)" do + setup do + prev_home = System.get_env("ANDROID_HOME") + prev_root = System.get_env("ANDROID_SDK_ROOT") + + on_exit(fn -> + restore = fn k, v -> if v, do: System.put_env(k, v), else: System.delete_env(k) end + restore.("ANDROID_HOME", prev_home) + restore.("ANDROID_SDK_ROOT", prev_root) + end) + + :ok + end + + test "root/0 honors ANDROID_HOME, not a hardcoded ~/Library path" do + System.delete_env("ANDROID_SDK_ROOT") + System.put_env("ANDROID_HOME", "/opt/custom-sdk") + + assert NdkVersion.root() == Path.join(["/opt/custom-sdk", "ndk", NdkVersion.effective()]) + refute NdkVersion.root() =~ "Library/Android/sdk" + end + + test "root/0 falls back to ANDROID_SDK_ROOT when ANDROID_HOME is unset" do + System.delete_env("ANDROID_HOME") + System.put_env("ANDROID_SDK_ROOT", "/opt/sdkroot") + + assert NdkVersion.root() =~ "/opt/sdkroot/ndk/" + end + + test "host/0 is the single NDK prebuilt tag for this OS" do + assert NdkVersion.host() in ["darwin-x86_64", "linux-x86_64"] + end + + test "sysroot/0 and toolchain_bin/0 compose root + host" do + System.delete_env("ANDROID_SDK_ROOT") + System.put_env("ANDROID_HOME", "/opt/custom-sdk") + base = Path.join([NdkVersion.root(), "toolchains", "llvm", "prebuilt", NdkVersion.host()]) + + assert NdkVersion.sysroot() == Path.join(base, "sysroot") + assert NdkVersion.toolchain_bin() == Path.join(base, "bin") + end + end end