From 082ccffd9a89144adcb139a47a1fd40023e6fea9 Mon Sep 17 00:00:00 2001 From: Ari Karo Date: Thu, 22 Jan 2026 17:56:22 +0000 Subject: [PATCH 1/2] initial implementation of making mix_test a dependency of mix_library --- MODULE.bazel.lock | 43 +++++++-- defs.bzl | 4 +- examples/basic/BUILD.bazel | 22 +++++ examples/basic/test/helped_test.exs | 2 +- examples/internal-elixir/BUILD.bazel | 26 ------ examples/plug-sample/BUILD.bazel | 19 +++- mix_test.bzl | 33 ++++--- private/mix_info.bzl | 1 + private/mix_library.bzl | 17 ++-- private/mix_test.bzl | 129 +++++++++++++++++++-------- 10 files changed, 200 insertions(+), 96 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7c5626b..11ec42d 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -138,7 +138,7 @@ "moduleExtensions": { "//bzlmod:extensions.bzl%elixir_config": { "general": { - "bzlTransitiveDigest": "AgaxxqpwL4S/7RQ99BCUdUmNvnfxiGx2+O8L/WoOkCs=", + "bzlTransitiveDigest": "nKLE1T2qrbRkYN4bFQb7oros493uoHbtw9KKRxvCJSI=", "usagesDigest": "lZ8LBqq5U16M0e+eBcnUFrKVGa83wzWb9XD/42iDu+w=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -177,7 +177,7 @@ }, "//bzlmod:extensions.bzl%elixir_packages": { "general": { - "bzlTransitiveDigest": "AgaxxqpwL4S/7RQ99BCUdUmNvnfxiGx2+O8L/WoOkCs=", + "bzlTransitiveDigest": "nKLE1T2qrbRkYN4bFQb7oros493uoHbtw9KKRxvCJSI=", "usagesDigest": "Ad4H9mIpXKptAZmMVOpq45Ouu6rYDrYeh6xizvSWUE8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, @@ -191,7 +191,7 @@ ], "strip_prefix": "hex-2.2.2", "sha256": "f3ba423f2937eb593eccc863c060f147af333e188620b7879ceb4e3b97faf07c", - "build_file_content": "\nload(\"@rules_elixir//:defs.bzl\", \"mix_library\")\n\nmix_library(\n name = \"lib\",\n app_name = \"hex\",\n srcs = glob([\"src/*.erl\", \"src/*.xrl\", \"src/*.hrl\", \"lib/**/*.ex\"]),\n data = [\"lib/hex/http/ca-bundle.crt\"],\n mix_config = \":mix.exs\",\n visibility = [\"//visibility:public\"],\n)\n" + "build_file_content": "\nload(\"@rules_elixir//:mix_library.bzl\", \"mix_library\")\n\nmix_library(\n name = \"lib\",\n app_name = \"hex\",\n srcs = glob([\"src/*.erl\", \"src/*.xrl\", \"src/*.hrl\", \"lib/**/*.ex\"]),\n data = [\"lib/hex/http/ca-bundle.crt\"],\n mix_config = \":mix.exs\",\n visibility = [\"//visibility:public\"],\n)\n" } }, "jason": { @@ -200,7 +200,7 @@ "package_name": "jason", "version": "1.4.4", "sha256": "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b", - "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_elixir//:defs.bzl\", \"mix_library\")\n\n# A little ugly, but lets us keep a relatively-consistent interface to\n# rules_erlang\nalias(\n name = \"erlang_app\",\n actual = \":jason\",\n)\n\nmix_library(\n name = \"jason\",\n app_name = \"jason\",\n include = glob([\n \"include/**/*.hrl\",\n ], allow_empty = True),\n srcs = glob([\n \"lib/**/*.ex\",\n \"lib/**/*.exs\",\n ], allow_empty = True),\n deps = [],\n mix_config = \":mix.exs\",\n)\n", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_elixir//:mix_library.bzl\", \"mix_library\")\n\n# A little ugly, but lets us keep a relatively-consistent interface to\n# rules_erlang\nalias(\n name = \"erlang_app\",\n actual = \":jason\",\n)\n\nmix_library(\n name = \"jason\",\n app_name = \"jason\",\n include = glob([\n \"include/**/*.hrl\",\n ], allow_empty = True),\n srcs = glob([\n \"lib/**/*.ex\",\n \"lib/**/*.exs\",\n ], allow_empty = True),\n deps = [],\n mix_config = \":mix.exs\",\n)\n", "patches": [], "patch_args": [ "-p0" @@ -264,8 +264,8 @@ }, "@@rules_erlang+//bzlmod:extensions.bzl%erlang_config": { "general": { - "bzlTransitiveDigest": "pcKL16v06F9wwO8lSlxTYszdj1PpMq+wmEvsqC6oaVw=", - "usagesDigest": "2mlGUm86FPZOH0K+ht1JoV81hZZCZOrHNJzRjlwuZQo=", + "bzlTransitiveDigest": "hEjE3X+zNRjxVZUSQbocYSICA4kaa8IqXZp8riq7GHM=", + "usagesDigest": "U8zB6BIRGIuea/SM4vrnQj7dnmq4f1ckiubh2ipG1Z8=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, @@ -361,6 +361,37 @@ ] } }, + "@@rules_erlang+//bzlmod:extensions.bzl%gmake_config": { + "general": { + "bzlTransitiveDigest": "hEjE3X+zNRjxVZUSQbocYSICA4kaa8IqXZp8riq7GHM=", + "usagesDigest": "lNRVW9LcmyfHI28I7FALwjESg77YJ8jUV5RX7xbDwJI=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "gmake_config": { + "repoRuleId": "@@rules_erlang+//repositories:gmake_config.bzl%gmake_config", + "attributes": { + "gmakes": { + "default": "/usr/bin/make" + } + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_erlang+", + "bazel_skylib", + "bazel_skylib+" + ], + [ + "rules_erlang+", + "bazel_tools", + "bazel_tools" + ] + ] + } + }, "@@rules_fuzzing+//fuzzing/private:extensions.bzl%non_module_dependencies": { "general": { "bzlTransitiveDigest": "v2H25KPHEkN56IHR41S67Kzp5Xjxd75GBiazhON8jzc=", diff --git a/defs.bzl b/defs.bzl index 09e1c71..4dad5b5 100644 --- a/defs.bzl +++ b/defs.bzl @@ -11,5 +11,5 @@ def mix_library(*args, **kwargs): def mix_release(*args, **kwargs): _mix_release(*args, **kwargs) -def mix_test(*args, **kwargs): - _mix_test(*args, **kwargs) +def mix_test(name, lib, **kwargs): + _mix_test(name = name, lib = lib, **kwargs) diff --git a/examples/basic/BUILD.bazel b/examples/basic/BUILD.bazel index 6ff5959..5c1ae22 100644 --- a/examples/basic/BUILD.bazel +++ b/examples/basic/BUILD.bazel @@ -3,7 +3,9 @@ load("@rules_elixir//:ex_unit_test.bzl", "ex_unit_test") load("@rules_erlang//:escript.bzl", "escript_archive") load("@rules_elixir//:mix_library.bzl", "mix_library") # , "mix_release") +load("@rules_elixir//:mix_test.bzl", "mix_test") +# Production library mix_library( name = "basic", # TODO: we should generate this target, and elicit this name from the mix @@ -13,6 +15,26 @@ mix_library( srcs = glob(['lib/**/*.ex']), ) +# Test library (compiled with MIX_ENV=test) +mix_library( + name = "basic_test_lib", + app_name = "main", + mix_env = "test", + mix_config = ":mix.exs", + srcs = glob(['lib/**/*.ex']), +) + +# Test runner using pre-compiled artifacts +mix_test( + name = "basic_test", + lib = ":basic_test_lib", + data = glob(['lib/**/*.ex', 'test/**/*.exs']), + env = { + "ERL_COMPILER_OPTIONS": "deterministic", + "ELIXIR_ERL_OPTIONS": "+fnu", + }, +) + # TODO: Migrate to elixir_release + elixir_release_bundle # The new approach uses: # 1. elixir_release to create the release with Elixir-specific processing diff --git a/examples/basic/test/helped_test.exs b/examples/basic/test/helped_test.exs index 49ee2dc..5c00c1c 100644 --- a/examples/basic/test/helped_test.exs +++ b/examples/basic/test/helped_test.exs @@ -1,4 +1,4 @@ -defmodule AssertionTest do +defmodule HelpedTest do use ExUnit.Case, async: true import TestHelper diff --git a/examples/internal-elixir/BUILD.bazel b/examples/internal-elixir/BUILD.bazel index b608d3f..0f8ff30 100644 --- a/examples/internal-elixir/BUILD.bazel +++ b/examples/internal-elixir/BUILD.bazel @@ -39,32 +39,6 @@ ex_unit_test( }, ) -# Example of running mix test on the entire project -mix_test( - name = "mix_test_all", - mix_config = "mix.exs", - deps = [":erlang_app"], - env = { - "ERL_COMPILER_OPTIONS": "deterministic", - "ELIXIR_ERL_OPTIONS": "+fnu", - }, -) - -# Example of running mix test on specific test files -mix_test( - name = "mix_test_specific", - srcs = [ - "test/assertion_test.exs", - ], - mix_config = "mix.exs", - deps = [":erlang_app"], - env = { - "ERL_COMPILER_OPTIONS": "deterministic", - "ELIXIR_ERL_OPTIONS": "+fnu", - }, - mix_test_opts = ["--trace"], -) - platform( name = "erlang_internal_platform", constraint_values = [ diff --git a/examples/plug-sample/BUILD.bazel b/examples/plug-sample/BUILD.bazel index 57a8c71..230841c 100644 --- a/examples/plug-sample/BUILD.bazel +++ b/examples/plug-sample/BUILD.bazel @@ -5,6 +5,7 @@ load("@rules_erlang//:escript.bzl", "escript_archive") load("@rules_elixir//:mix_library.bzl", "mix_library") # , "mix_release") load("@rules_elixir//:mix_test.bzl", "mix_test") +# Production library mix_library( name = "lib", # TODO: we should generate this target, and elicit this name from the mix @@ -17,6 +18,18 @@ mix_library( ], ) +# Test library (compiled with MIX_ENV=test) +mix_library( + name = "lib_test", + app_name = "plug_sample", + mix_env = "test", + mix_config = ":mix.exs", + srcs = glob(['lib/**/*.ex']), + deps = [ + "@plug_cryptoasdasd//:plug_crypto" + ], +) + # TODO: Migrate to elixir_release + elixir_release_bundle # The new approach uses: # 1. elixir_release to create the release with Elixir-specific processing @@ -43,13 +56,11 @@ mix_library( # command_line_args = ["--no-halt"], # ) +# Test runner using pre-compiled artifacts mix_test( name = "plug_sample_test", - mix_config = ":mix.exs", + lib = ":lib_test", data = glob(['lib/**/*.ex', 'test/**/*.exs']), - deps = [ - "@plug_cryptoasdasd//:plug_crypto" - ], env = { "ERL_COMPILER_OPTIONS": "deterministic", "ELIXIR_ERL_OPTIONS": "+fnu", diff --git a/mix_test.bzl b/mix_test.bzl index 080b0ec..6fc8e05 100644 --- a/mix_test.bzl +++ b/mix_test.bzl @@ -3,17 +3,18 @@ load( _mix_test = "mix_test", ) -def mix_test(**kwargs): - """Run mix test on an Elixir project. +def mix_test(name, lib, **kwargs): + """Run mix test using pre-compiled artifacts from a mix_library. - This rule runs `mix test` on a Mix project, executing the project's test suite. + This rule runs `mix test` on a Mix project using pre-compiled artifacts + from a mix_library target. The library must be compiled with mix_env="test". Args: name: The name of the test target - mix_config: The mix.exs configuration file (default: ":mix.exs") + lib: The mix_library target containing the compiled application. + Must be compiled with mix_env="test". srcs: Optional list of specific test files to run. If empty, runs all tests in test/ - data: Additional data files needed for tests - deps: Dependencies required for the tests (must provide ErlangAppInfo) + data: Additional data files needed for tests (include test/**/*.exs here) ez_deps: Erlang/Elixir archive dependencies (.ez files) tools: Additional tools needed for tests env: Environment variables to set during test execution @@ -23,15 +24,21 @@ def mix_test(**kwargs): Example: ```python + # First, create a test library compiled with MIX_ENV=test + mix_library( + name = "my_app_test_lib", + app_name = "my_app", + mix_env = "test", + srcs = glob(["lib/**/*.ex"]), + ) + + # Then create the test target mix_test( - name = "my_project_test", - mix_config = "mix.exs", - deps = [":my_app"], - env = { - "MIX_ENV": "test", - }, + name = "my_app_test", + lib = ":my_app_test_lib", + data = glob(["test/**/*.exs"]), mix_test_opts = ["--trace"], ) ``` """ - _mix_test(**kwargs) + _mix_test(name = name, lib = lib, **kwargs) diff --git a/private/mix_info.bzl b/private/mix_info.bzl index b2f8d2b..9df69ee 100644 --- a/private/mix_info.bzl +++ b/private/mix_info.bzl @@ -5,6 +5,7 @@ MixProjectInfo = provider( # ErlangAppInfo # 'app_name': 'Name of the OTP application', "mix_config": "Path to the mix.exs file of this Mix project", + "mix_env": "The MIX_ENV used to compile this project (prod, test, dev)", # 'ebin': 'Directory containing built libs(???)', # # TODO: find out how elixir works # 'consolidated': 'Directory containing...consolidated...stuff??? how does elixir work?', diff --git a/private/mix_library.bzl b/private/mix_library.bzl index 33d539b..d71af0b 100644 --- a/private/mix_library.bzl +++ b/private/mix_library.bzl @@ -43,9 +43,9 @@ def _mix_library_impl(ctx): # TODO: i don't _think_ we need to explicitly pass the output dir in, and # should instead return a Provider that can provide erlang...library info? # TBD - # This also needs a better name - ebin = ctx.actions.declare_directory("ebin") - priv_dir = ctx.actions.declare_directory("priv") if ctx.files.priv else None + # Use target name in output directory to avoid conflicts between targets + ebin = ctx.actions.declare_directory(ctx.label.name + "_ebin") + priv_dir = ctx.actions.declare_directory(ctx.label.name + "_priv") if ctx.files.priv else None # app_file = ctx.actions.declare_file("{app_name}.app".format(app_name=ctx.attr.app_name)) erl_libs_dir = ctx.label.name + "_deps" @@ -171,7 +171,7 @@ if [[ -n "$ERL_LIBS_PATH" ]]; then done fi -MIX_ENV=prod \\ +MIX_ENV={mix_env} \\ MIX_BUILD_ROOT=_output \\ MIX_HOME=/tmp \\ MIX_OFFLINE=true \\ @@ -190,7 +190,7 @@ mkdir -p "$ABS_OUT_DIR" # NOTE: this directory can contain files other than .app and .beam, but we only # want to keep these in our build output. -cp _output/prod/lib/{app_name}/ebin/*.beam _output/prod/lib/{app_name}/ebin/*.app "$ABS_OUT_DIR/" +cp _output/{mix_env}/lib/{app_name}/ebin/*.beam _output/{mix_env}/lib/{app_name}/ebin/*.app "$ABS_OUT_DIR/" # Set priv output directory if priv files exist if [[ -n "{priv_out_dir}" ]]; then @@ -210,6 +210,7 @@ fi erl_libs_path = erl_libs_path, build_dir = ctx.file.mix_config.dirname, name = ctx.label.name, + mix_env = ctx.attr.mix_env, # env = env, # setup = ctx.attr.setup, out_dir = ebin.path, @@ -252,6 +253,7 @@ fi MixProjectInfo( # app_name = ctx.attr.app_name, mix_config = ctx.file.mix_config, + mix_env = ctx.attr.mix_env, # ebin = '/'.join([ebin.path, 'ebin', 'lib', ctx.attr.app_name, 'ebin']), # TODO: should we actually keep this, or should be just YEET the # consolidated directory? it seems only have dependencies @@ -293,6 +295,11 @@ mix_library = rule( implementation = _mix_library_impl, attrs = { "app_name": attr.string(), + "mix_env": attr.string( + default = "prod", + values = ["prod", "test", "dev"], + doc = "The MIX_ENV to use when compiling (default: prod)", + ), "mix_config": attr.label( allow_single_file = [".exs"], default = ":mix.exs", diff --git a/private/mix_test.bzl b/private/mix_test.bzl index 5ef9b67..16134b0 100644 --- a/private/mix_test.bzl +++ b/private/mix_test.bzl @@ -8,19 +8,34 @@ load( "erlang_dirs", "maybe_install_erlang", ) +load("//private:mix_info.bzl", "MixProjectInfo") def _mix_test_impl(ctx): + # Get providers from the lib dependency + lib_erlang_info = ctx.attr.lib[ErlangAppInfo] + lib_mix_info = ctx.attr.lib[MixProjectInfo] + + # Validate that lib was compiled with mix_env="test" + if lib_mix_info.mix_env != "test": + fail("mix_test requires a mix_library compiled with mix_env='test'. " + + "Target '{}' was compiled with mix_env='{}'. ".format(ctx.attr.lib.label, lib_mix_info.mix_env) + + "Create a separate mix_library with mix_env='test' for testing.") + + app_name = lib_erlang_info.app_name + mix_config = lib_mix_info.mix_config + + # Get the ebin directory from the library + lib_beam_dirs = lib_erlang_info.beam # List containing the ebin directory + lib_priv_dirs = lib_erlang_info.priv # List containing the priv directory (if any) + + # Build ERL_LIBS for all dependencies (excluding the lib itself) erl_libs_dir = ctx.label.name + "_deps" + lib_deps = lib_erlang_info.deps erl_libs_files = erl_libs_contents( ctx, dir = erl_libs_dir, - deps = flat_deps(ctx.attr.deps), - # NOTE: even though we provide `ez_deps` here, these don't actually - # correctly get added to our necessary include path. We explicitly set - # MIX_ARCHIVES later to placate mix here. - # TODO: improve this? it would be nice if we didn't have to do - # explicit further handling for .ez files. + deps = lib_deps, ez_deps = ctx.files.ez_deps, expand_ezs = False, ) @@ -43,6 +58,17 @@ def _mix_test_impl(ctx): if ctx.files.srcs: test_paths = " ".join([s.short_path for s in ctx.files.srcs]) + # Get the path to the lib's ebin directory + # lib_beam_dirs is a list of File objects (directories) + lib_ebin_path = "" + if lib_beam_dirs: + lib_ebin_path = lib_beam_dirs[0].short_path + + # Get the path to the lib's priv directory (if any) + lib_priv_path = "" + if lib_priv_dirs: + lib_priv_path = lib_priv_dirs[0].short_path + script = """\ #!/usr/bin/env bash set -eo pipefail @@ -60,7 +86,7 @@ cd "$TEST_SRCDIR/$TEST_WORKSPACE/{package}" # Set up ERL_LIBS for dependencies ERL_LIBS_PATH="" -if [[ -n "$TEST_SRCDIR/$TEST_WORKSPACE/{erl_libs_path}" && -d "$TEST_SRCDIR/$TEST_WORKSPACE/{erl_libs_path}" ]]; then +if [[ -n "{erl_libs_path}" && -d "$TEST_SRCDIR/$TEST_WORKSPACE/{erl_libs_path}" ]]; then ERL_LIBS_PATH="$(realpath $TEST_SRCDIR/$TEST_WORKSPACE/{erl_libs_path})" fi export ERL_LIBS="$ERL_LIBS_PATH" @@ -73,8 +99,42 @@ export MIX_ENV=test {setup} +# Set up _build directory structure with pre-compiled artifacts +# This makes Mix think the project is already compiled +mkdir -p _build/test/lib/{app_name}/ebin + +# Copy pre-compiled .beam and .app files from the mix_library +LIB_EBIN_PATH="$TEST_SRCDIR/$TEST_WORKSPACE/{lib_ebin_path}" +if [[ -d "$LIB_EBIN_PATH" ]]; then + cp -r "$LIB_EBIN_PATH"/* _build/test/lib/{app_name}/ebin/ +fi + +# Copy priv directory if it exists +if [[ -n "{lib_priv_path}" ]]; then + LIB_PRIV_PATH="$TEST_SRCDIR/$TEST_WORKSPACE/{lib_priv_path}" + if [[ -d "$LIB_PRIV_PATH" ]]; then + mkdir -p _build/test/lib/{app_name}/priv + cp -r "$LIB_PRIV_PATH"/* _build/test/lib/{app_name}/priv/ + fi +fi + +# Also set up dependency ebin directories in _build +if [[ -n "$ERL_LIBS_PATH" ]]; then + for app_dir in "$ERL_LIBS_PATH"/*; do + app_basename=$(basename "$app_dir") + if [[ -d "$app_dir/ebin" ]]; then + mkdir -p "_build/test/lib/$app_basename/ebin" + cp -r "$app_dir/ebin"/* "_build/test/lib/$app_basename/ebin/" + fi + if [[ -d "$app_dir/priv" ]]; then + mkdir -p "_build/test/lib/$app_basename/priv" + cp -r "$app_dir/priv"/* "_build/test/lib/$app_basename/priv/" + fi + done +fi + # Build -pa options for each dependency's ebin directory -PA_OPTIONS="" +PA_OPTIONS="-pa _build/test/lib/{app_name}/ebin" if [[ -n "$ERL_LIBS_PATH" ]]; then for app_dir in "$ERL_LIBS_PATH"/*; do if [[ -d "$app_dir/ebin" ]]; then @@ -83,36 +143,24 @@ if [[ -n "$ERL_LIBS_PATH" ]]; then done fi -# Compile the project first (similar to mix_library) -mkdir -p _build -MIX_ENV=test \\ - MIX_BUILD_ROOT=_build \\ - MIX_HOME=/tmp \\ - MIX_OFFLINE=true \\ - ELIXIR_ERL_OPTIONS="$PA_OPTIONS" \\ - ERL_LIBS="$ERL_LIBS_PATH" \\ - ${{ABS_ELIXIR_HOME}}/bin/mix compile --no-deps-check --no-elixir-version-check --skip-protocol-consolidation --no-optional-deps - -# Run mix test -set -x +# Run mix test with --no-compile to use pre-compiled artifacts +# Note: test/*.exs files are still compiled on-the-fly by ExUnit (this is by design) MIX_ENV=test \\ MIX_BUILD_ROOT=_build \\ MIX_HOME=/tmp \\ MIX_OFFLINE=true \\ ELIXIR_ERL_OPTIONS="$PA_OPTIONS" \\ ERL_LIBS="$ERL_LIBS_PATH" \\ - ${{ABS_ELIXIR_HOME}}/bin/mix test --no-start --no-deps-check {test_paths} {mix_test_opts} \\ - | tee "${{TEST_UNDECLARED_OUTPUTS_DIR}}/test.log" -set +x - -# Verify tests passed (no failures) -tail -n 10 "${{TEST_UNDECLARED_OUTPUTS_DIR}}/test.log" | grep -E --silent "0 failures" + ${{ABS_ELIXIR_HOME}}/bin/mix test --no-compile --no-start --no-deps-check {test_paths} {mix_test_opts} """.format( maybe_install_erlang = maybe_install_erlang(ctx), erlang_home = erlang_home, elixir_home = elixir_home, erl_libs_path = erl_libs_path, package = package, + app_name = app_name, + lib_ebin_path = lib_ebin_path, + lib_priv_path = lib_priv_path, env = env, setup = ctx.attr.setup, test_paths = test_paths, @@ -124,7 +172,13 @@ tail -n 10 "${{TEST_UNDECLARED_OUTPUTS_DIR}}/test.log" | grep -E --silent "0 fai content = script, ) - # Include mix.exs and any additional data files in runfiles + # Collect all files needed at runtime + lib_files = [] + for beam_dir in lib_beam_dirs: + lib_files.append(beam_dir) + for priv_dir in lib_priv_dirs: + lib_files.append(priv_dir) + runfiles = erlang_runfiles.merge(elixir_runfiles) runfiles = runfiles.merge_all( [ @@ -132,7 +186,8 @@ tail -n 10 "${{TEST_UNDECLARED_OUTPUTS_DIR}}/test.log" | grep -E --silent "0 fai ctx.files.srcs + ctx.files.data + erl_libs_files + - ([ctx.file.mix_config] if ctx.file.mix_config else []) + lib_files + + [mix_config] ), ] + [ tool[DefaultInfo].default_runfiles @@ -148,22 +203,18 @@ tail -n 10 "${{TEST_UNDECLARED_OUTPUTS_DIR}}/test.log" | grep -E --silent "0 fai mix_test = rule( implementation = _mix_test_impl, attrs = { - "mix_config": attr.label( - allow_single_file = [".exs"], - default = ":mix.exs", - doc = "The mix.exs configuration file for the project", + "lib": attr.label( + mandatory = True, + providers = [ErlangAppInfo, MixProjectInfo], + doc = "The mix_library target containing compiled application (must have mix_env='test')", ), "srcs": attr.label_list( - allow_files = [".ex", ".exs"], - doc = "Optional list of specific test files to run. If empty, runs all tests.", + allow_files = [".exs"], + doc = "Optional list of specific test files to run. If empty, runs all tests in test/.", ), "data": attr.label_list( allow_files = True, - doc = "Additional data files needed for tests", - ), - "deps": attr.label_list( - providers = [ErlangAppInfo], - doc = "Dependencies required for the tests", + doc = "Additional data files needed for tests (include test/**/*.exs here)", ), "ez_deps": attr.label_list( allow_files = [".ez"], From 0205b73aa22feea77a8187ee30a089cbfd58ec51 Mon Sep 17 00:00:00 2001 From: Ari Karo Date: Thu, 22 Jan 2026 20:18:51 +0000 Subject: [PATCH 2/2] mix_library cleanup --- private/mix_library.bzl | 49 ++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/private/mix_library.bzl b/private/mix_library.bzl index d71af0b..f0d6a8d 100644 --- a/private/mix_library.bzl +++ b/private/mix_library.bzl @@ -43,9 +43,9 @@ def _mix_library_impl(ctx): # TODO: i don't _think_ we need to explicitly pass the output dir in, and # should instead return a Provider that can provide erlang...library info? # TBD - # Use target name in output directory to avoid conflicts between targets - ebin = ctx.actions.declare_directory(ctx.label.name + "_ebin") - priv_dir = ctx.actions.declare_directory(ctx.label.name + "_priv") if ctx.files.priv else None + # This also needs a better name + ebin = ctx.actions.declare_directory("ebin") + priv_dir = ctx.actions.declare_directory("priv") if ctx.files.priv else None # app_file = ctx.actions.declare_file("{app_name}.app".format(app_name=ctx.attr.app_name)) erl_libs_dir = ctx.label.name + "_deps" @@ -70,7 +70,7 @@ def _mix_library_impl(ctx): # - confirm if we need a secondary one of these for non-runtime deps # (or maybe we just construct a combination classpath?) erl_libs_path = "" - if len(erl_libs_files) > 0: + if erl_libs_files: erl_libs_path = path_join( ctx.bin_dir.path, ctx.label.workspace_root, @@ -93,33 +93,26 @@ def _mix_library_impl(ctx): priv_copy_commands = "" priv_copy_to_build_dir = "" if priv_dir: - # Commands to copy priv files to build directory (for Mix to access during compilation) - priv_copy_to_build_dir = "\n# Copy priv files to build directory for Mix access\n" - for priv_file in ctx.files.priv: - rel_path = _priv_file_dest_relative_path(ctx.label, priv_file) - priv_copy_to_build_dir += " mkdir -p \"priv/$(dirname {})\"\n".format(rel_path) - # Only copy if source and destination are different (avoid "same file" errors) - priv_copy_to_build_dir += " if [[ \"$ORIG_PWD/{}\" != \"$(pwd)/priv/{}\" ]]; then\n".format( - priv_file.path, - rel_path - ) - priv_copy_to_build_dir += " cp -L \"$ORIG_PWD/{}\" \"priv/{}\"\n".format( - priv_file.path, - rel_path - ) - priv_copy_to_build_dir += " fi\n" - - # Commands to copy priv files to output directory (for ErlangAppInfo provider) - priv_copy_commands = "\n# Copy priv files to output directory preserving directory structure\n" - priv_copy_commands += "mkdir -p \"$ABS_OUT_PRIV_DIR\"\n" + build_dir_cmds = [] + output_dir_cmds = [] for priv_file in ctx.files.priv: rel_path = _priv_file_dest_relative_path(ctx.label, priv_file) - priv_copy_commands += "mkdir -p \"$ABS_OUT_PRIV_DIR/$(dirname {})\"\n".format(rel_path) - priv_copy_commands += "cp -L \"$ORIG_PWD/{}\" \"$ABS_OUT_PRIV_DIR/{}\"\n".format( - priv_file.path, - rel_path - ) + src_path = priv_file.path + + # Commands to copy priv files to build directory (for Mix to access during compilation) + build_dir_cmds.append(' mkdir -p "priv/$(dirname {})"'.format(rel_path)) + # Conditional to avoid same-file errors + build_dir_cmds.append(' if [[ "$ORIG_PWD/{}" != "$(pwd)/priv/{}" ]]; then'.format(src_path, rel_path)) + build_dir_cmds.append(' cp -L "$ORIG_PWD/{}" "priv/{}"'.format(src_path, rel_path)) + build_dir_cmds.append(" fi") + + # Commands to copy priv files to output directory (for ErlangAppInfo provider) + output_dir_cmds.append('mkdir -p "$ABS_OUT_PRIV_DIR/$(dirname {})"'.format(rel_path)) + output_dir_cmds.append('cp -L "$ORIG_PWD/{}" "$ABS_OUT_PRIV_DIR/{}"'.format(src_path, rel_path)) + + priv_copy_to_build_dir = "\n# Copy priv files to build directory for Mix access\n" + "\n".join(build_dir_cmds) + "\n" + priv_copy_commands = "\n# Copy priv files to output directory preserving directory structure\nmkdir -p \"$ABS_OUT_PRIV_DIR\"\n" + "\n".join(output_dir_cmds) + "\n" # TODO: confirm if we need to use include dir from other modules, or if # that's just a way for elixir to expose and interface to erlang.