diff --git a/AGENTS.md b/AGENTS.md index 9314478..90b6441 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,10 @@ mix test --exclude integration # skip the device-dependent ones - **Compile-time regex literals are unsafe** on Elixir 1.19 / OTP 28.0. Use `Regex.compile!("...", "flags")` for runtime compilation. Already swept in 0.3.17 — don't reintroduce. +- **Hex packages omit repository-root dotfiles by default.** Code under `lib/` + must not compile-time read `.tool-versions` or another root-only file. Keep a + packaged authority in source, enforce exact lockstep with the root file in a + source test, and compile the unpacked Hex artifact in the regression suite. - **`mix mob.deploy --device `** resolves the id via discovery before deciding which platform to build. The narrowing logic is in `narrow_platforms_for_device/2` and is the single source of truth for both diff --git a/lib/mob_dev/toolchain.ex b/lib/mob_dev/toolchain.ex index adb5f20..f1e6469 100644 --- a/lib/mob_dev/toolchain.ex +++ b/lib/mob_dev/toolchain.ex @@ -1,17 +1,9 @@ defmodule MobDev.Toolchain do @moduledoc false - @tool_versions_path Path.expand("../../.tool-versions", __DIR__) - @external_resource @tool_versions_path - @required_zig_version @tool_versions_path - |> File.read!() - |> String.split("\n") - |> Enum.find_value(fn line -> - case String.split(line) do - ["zig", version] -> version - _ -> nil - end - end) || raise("zig is not pinned in #{@tool_versions_path}") + # This value must live in packaged source; the root toolchain file is not in + # Hex archives. The source test keeps both release authorities in lockstep. + @required_zig_version "0.17.0-dev.269+ebff43698" @type zig_status :: :missing diff --git a/test/mob_dev/toolchain_package_test.exs b/test/mob_dev/toolchain_package_test.exs new file mode 100644 index 0000000..ceb6640 --- /dev/null +++ b/test/mob_dev/toolchain_package_test.exs @@ -0,0 +1,87 @@ +defmodule MobDev.ToolchainPackageTest do + use ExUnit.Case, async: false + + test "packed Hex artifact compiles and retains exact Zig preflights" do + root = Path.expand("../..", __DIR__) + + temp = + Path.join( + System.tmp_dir!(), + "mob_dev_toolchain_package_#{System.unique_integer([:positive])}" + ) + + package = Path.join(temp, "package") + build = Path.join(temp, "build") + on_exit(fn -> File.rm_rf!(temp) end) + + {pack_output, pack_status} = + System.cmd("mix", ["hex.build", "--unpack", "--output", package], + cd: root, + stderr_to_stdout: true + ) + + assert pack_status == 0, pack_output + refute File.exists?(Path.join(package, ".tool-versions")) + File.cp!(Path.join(root, "mix.lock"), Path.join(package, "mix.lock")) + link_compiled_dependencies(root, build) + + env = [ + {"MIX_ENV", "prod"}, + {"MIX_BUILD_PATH", build}, + {"MIX_DEPS_PATH", Path.join(root, "deps")} + ] + + {compile_output, compile_status} = + System.cmd("mix", ["compile"], + cd: package, + env: env, + stderr_to_stdout: true + ) + + assert compile_status == 0, compile_output + + probe = ~S''' + beam = :code.which(MobDev.Toolchain) |> List.to_string() + true = String.contains?(beam, "mob_dev_toolchain_package_") + + version = MobDev.Toolchain.required_zig_version() + exact = MobDev.Toolchain.zig_status_from_result({version <> "\n", 0}) + mismatch = MobDev.Toolchain.zig_status_from_result({"0.15.2\n", 0}) + failed = MobDev.Toolchain.zig_status_from_result({"dyld failure\n", 127}) + + {:ok, "zig", ^version, nil} = Mix.Tasks.Mob.Doctor.__zig_check_result__(exact) + {:fail, "zig", _, _} = Mix.Tasks.Mob.Doctor.__zig_check_result__(mismatch) + {:fail, "zig", _, _} = Mix.Tasks.Mob.Doctor.__zig_check_result__(failed) + + :run_zig = MobDev.NativeBuild.zig_build_plan(true, exact, false) + {:zig_required, ^mismatch} = MobDev.NativeBuild.zig_build_plan(true, mismatch, true) + {:zig_required, ^failed} = MobDev.NativeBuild.zig_build_plan(true, failed, true) + IO.puts("packed_toolchain_ok=#{version}") + ''' + + {probe_output, probe_status} = + System.cmd( + "mix", + ["run", "--no-compile", "--no-start", "-e", probe], + cd: package, + env: env, + stderr_to_stdout: true + ) + + assert probe_status == 0, probe_output + assert probe_output =~ "packed_toolchain_ok=#{MobDev.Toolchain.required_zig_version()}" + end + + defp link_compiled_dependencies(root, build) do + build_lib = Path.join(build, "lib") + File.mkdir_p!(build_lib) + + root + |> Path.join("_build/test/lib/*") + |> Path.wildcard() + |> Enum.reject(&(Path.basename(&1) == "mob_dev")) + |> Enum.each(fn dependency -> + File.ln_s!(dependency, Path.join(build_lib, Path.basename(dependency))) + end) + end +end diff --git a/test/mob_dev/toolchain_test.exs b/test/mob_dev/toolchain_test.exs index a61f2e3..3ebed9c 100644 --- a/test/mob_dev/toolchain_test.exs +++ b/test/mob_dev/toolchain_test.exs @@ -6,7 +6,17 @@ defmodule MobDev.ToolchainTest do test "required Zig version stays in lockstep with .tool-versions" do tool_versions = File.read!(Path.expand("../../.tool-versions", __DIR__)) - assert tool_versions =~ "zig #{Toolchain.required_zig_version()}" + pinned_zig = + tool_versions + |> String.split("\n") + |> Enum.find_value(fn line -> + case String.split(line) do + ["zig", version] -> version + _ -> nil + end + end) + + assert pinned_zig == Toolchain.required_zig_version() end test "accepts the exact required Zig version" do