From 0cd18d27266f0a93c04dad52e155560a920f8170 Mon Sep 17 00:00:00 2001 From: Martin Kuntz Jacobsen Date: Tue, 27 Oct 2026 16:11:16 +0100 Subject: [PATCH] feat(elixir_app): add name, target_compatible_with, testonly and tags elixir_app always creates the targets beam_files, app_file, ebin, elixir_without_app_overlap and erlang_app, with the output dirs beam_files, ebin and unconsolidated. Two apps cannot share a package, and the generated targets cannot be marked incompatible, testonly or tagged. Add a name argument, default "erlang_app". With the default, every target name and output dir stays as it is today. With any other name, the internal targets are named _beam_files, _app_file, _ebin and _elixir_without_app_overlap, and their output dirs are /beam_files, /ebin and /unconsolidated. Add target_compatible_with, testonly and tags, and pass them to all five generated targets. Tests in test/: a build_test and a path check for the default names and output dirs, a second app named "other" (testonly) in the same package, and an app with target_compatible_with incompatible that //... skips and that fails when built directly (checked in CI). --- .github/workflows/test.yaml | 8 ++++ elixir_app.bzl | 63 ++++++++++++++++++++------- test/BUILD.bazel | 60 +++++++++++++++++++++++++ test/MODULE.bazel | 10 +++++ test/app_names_test.sh | 20 +++++++++ test/incompatible_lib/incompatible.ex | 3 ++ test/other_lib/other.ex | 3 ++ 7 files changed, 152 insertions(+), 15 deletions(-) create mode 100755 test/app_names_test.sh create mode 100644 test/incompatible_lib/incompatible.ex create mode 100644 test/other_lib/other.ex diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index fa5e2e5..b8d1683 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -71,6 +71,14 @@ jobs: run: | grep -E "Result: 2 passed|2 tests, 0 failures" \ bazel-testlogs/two_file_test/test.log + - name: INCOMPATIBLE APP IS SKIPPED + working-directory: test + # //... above skipped it. Asking for it directly must fail. + run: | + if bazelisk build //:incompatible_app; then + echo "//:incompatible_app built despite target_compatible_with" + exit 1 + fi - name: EXPECTED FAILURES FAIL working-directory: test run: | diff --git a/elixir_app.bzl b/elixir_app.bzl index 33d8c26..724e146 100644 --- a/elixir_app.bzl +++ b/elixir_app.bzl @@ -4,7 +4,15 @@ load("//private:elixir_bytecode.bzl", "elixir_bytecode") load("//private:elixir_ebin_dir.bzl", "elixir_ebin_dir") load("//private:erlang_app_filter_module_conflicts.bzl", "erlang_app_filter_module_conflicts") +def _prefixed(name, suffix, sep): + # The default name keeps the internal names and output dirs that + # elixir_app has always used, so existing users see no change. + if name == "erlang_app": + return suffix + return name + sep + suffix + def elixir_app( + name = "erlang_app", app_name = None, extra_apps = [], srcs = None, @@ -12,10 +20,18 @@ def elixir_app( elixirc_opts = [], ez_deps = [], deps = [], + target_compatible_with = None, + testonly = False, + tags = None, **kwargs): """compiles elixir sources in a manner compatible with @rules_erlang Args: + name: Name of the final ErlangAppInfo target. With the default + "erlang_app", the internal targets are named beam_files, + app_file, ebin and elixir_without_app_overlap. With any other + name they are prefixed with "_", and their output dirs + with "/", so several apps can share a package. app_name: Name of the application extra_apps: additional apps (elixir is always included) injected into the .app file @@ -23,6 +39,9 @@ def elixir_app( elixirc_opts: elixirc options ez_deps: Dependencies that are .ez files deps: ErlangAppInfo labels + target_compatible_with: Passed to every generated target + testonly: Passed to every generated target + tags: Passed to every generated target **kwargs: Additional args passed to the underlying app_file rule, such as app_description @@ -34,45 +53,58 @@ def elixir_app( "lib/**/*.ex", ]) + beam_files = _prefixed(name, "beam_files", "_") + app_file_name = _prefixed(name, "app_file", "_") + ebin = _prefixed(name, "ebin", "_") + without_overlap = _prefixed(name, "elixir_without_app_overlap", "_") + common = dict( + target_compatible_with = target_compatible_with, + testonly = testonly, + tags = tags, + ) + elixir_bytecode( - name = "beam_files", + name = beam_files, srcs = srcs, data = data, - dest = "beam_files", + dest = _prefixed(name, "beam_files", "/"), elixirc_opts = elixirc_opts, ez_deps = ez_deps, deps = deps, + **common ) app_file( - name = "app_file", + name = app_file_name, out = "%s.app" % app_name, app_name = app_name, extra_apps = ["elixir"] + extra_apps, - modules = [":beam_files"], - **kwargs + modules = [":" + beam_files], + **(common | kwargs) ) elixir_ebin_dir( - name = "ebin", - beam_files_dir = ":beam_files", - app_file = ":app_file", - dest = "ebin", + name = ebin, + beam_files_dir = ":" + beam_files, + app_file = ":" + app_file_name, + dest = _prefixed(name, "ebin", "/"), + **common ) erlang_app_filter_module_conflicts( - name = "elixir_without_app_overlap", - dest = "unconsolidated", + name = without_overlap, + dest = _prefixed(name, "unconsolidated", "/"), src = Label("@rules_elixir//elixir:elixir"), - without = [":ebin"], + without = [":" + ebin], + **common ) erlang_app_info( - name = "erlang_app", + name = name, srcs = srcs, hdrs = [], app_name = app_name, - beam = [":ebin"], + beam = [":" + ebin], extra_apps = extra_apps, # allow_empty: plenty of Hex packages ship no LICENSE file (ash among them), and # with --incompatible_disallow_empty_glob on -- the default since Bazel 7 -- an @@ -81,6 +113,7 @@ def elixir_app( priv = [], visibility = ["//visibility:public"], deps = [ - ":elixir_without_app_overlap", + ":" + without_overlap, ] + deps, + **common ) diff --git a/test/BUILD.bazel b/test/BUILD.bazel index caac24f..2e772c2 100644 --- a/test/BUILD.bazel +++ b/test/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_elixir//:elixir_app.bzl", "elixir_app") load("@rules_elixir//:ex_unit_test.bzl", "ex_unit_test") load( @@ -15,6 +16,65 @@ elixir_app( ], ) +# A second app in the same package. Its internal targets and output dirs +# are prefixed with its name, so they do not clash with the default ones. +# testonly is forwarded to every generated target; if it were not, the +# final target would fail analysis because it depends on testonly ones. +elixir_app( + name = "other", + app_name = "other", + srcs = ["other_lib/other.ex"], + testonly = True, + tags = ["other_app"], +) + +build_test( + name = "app_targets_test", + targets = [ + ":erlang_app", + ":beam_files", + ":app_file", + ":ebin", + ":elixir_without_app_overlap", + ":other", + ":other_beam_files", + ":other_app_file", + ":other_ebin", + ":other_elixir_without_app_overlap", + ], +) + +sh_test( + name = "app_names_test", + size = "small", + srcs = ["app_names_test.sh"], + args = [ + "beam_files=$(rootpath :beam_files)", + "ebin=$(rootpath :ebin)", + "unconsolidated=$(rootpath :elixir_without_app_overlap)", + "other/beam_files=$(rootpath :other_beam_files)", + "other/ebin=$(rootpath :other_ebin)", + "other/unconsolidated=$(rootpath :other_elixir_without_app_overlap)", + ], + data = [ + ":beam_files", + ":ebin", + ":elixir_without_app_overlap", + ":other_beam_files", + ":other_ebin", + ":other_elixir_without_app_overlap", + ], +) + +# Incompatible with every platform, so //... skips all of its targets. +# .github/workflows/test.yaml checks that building it directly fails. +elixir_app( + name = "incompatible_app", + app_name = "incompatible_app", + srcs = ["incompatible_lib/incompatible.ex"], + target_compatible_with = ["@platforms//:incompatible"], +) + escript_archive( name = "basic", app = ":erlang_app", diff --git a/test/MODULE.bazel b/test/MODULE.bazel index ce85484..01a157b 100644 --- a/test/MODULE.bazel +++ b/test/MODULE.bazel @@ -3,6 +3,10 @@ module( version = "0.1.0", ) +bazel_dep( + name = "bazel_skylib", + version = "1.9.2", +) bazel_dep( name = "rules_erlang", version = "3.18.0", @@ -23,6 +27,12 @@ bazel_dep( version = "0.8.0", ) +# target_compatible_with in BUILD.bazel uses @platforms//:incompatible. +bazel_dep( + name = "platforms", + version = "1.1.0", +) + # See the note in the root MODULE.bazel. This is a separate module, so it needs # its own override; the one in the ruleset it depends on does not carry over. git_override( diff --git a/test/app_names_test.sh b/test/app_names_test.sh new file mode 100755 index 0000000..a58f079 --- /dev/null +++ b/test/app_names_test.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Each argument is "=". The targets are in the +# root package, so the rootpath of an output dir is its dest. The output +# dirs of the default-named elixir_app must not gain a prefix. +set -euo pipefail + +for arg in "$@"; do + expected="${arg%%=*}" + path="${arg#*=}" + path="${path#./}" + if [ "$path" != "$expected" ]; then + echo "FAIL: output dir is $path, expected $expected" >&2 + exit 1 + fi + if [ ! -d "$path" ]; then + echo "FAIL: $path is not a directory" >&2 + exit 1 + fi + echo "ok: $path" +done diff --git a/test/incompatible_lib/incompatible.ex b/test/incompatible_lib/incompatible.ex new file mode 100644 index 0000000..06cf445 --- /dev/null +++ b/test/incompatible_lib/incompatible.ex @@ -0,0 +1,3 @@ +defmodule Incompatible do + def hello, do: :world +end diff --git a/test/other_lib/other.ex b/test/other_lib/other.ex new file mode 100644 index 0000000..21a19c0 --- /dev/null +++ b/test/other_lib/other.ex @@ -0,0 +1,3 @@ +defmodule Other do + def hello, do: :world +end