Skip to content

rules_elixir: split hermetic_elixir.patch into separate upstream PRs to bazelverse/rules_elixir #3744

Open

Description

@TrapsterDK

Blocked on: bazelverse/rules_elixir#2, bazelverse/rules_elixir#3, bazelverse/rules_elixir#4 (open), and bazelverse/rules_erlang#18 for the tree patch. Bot unblock of 2026-11-05 was a false positive (#7581).

Problem
patches/rules_elixir/hermetic_elixir.patch is applied to the pinned bazelverse/rules_elixir (RULES_ELIXIR_COMMIT = 8e13ada..., equal to upstream main today) by the repository rule in elixir/toolchain/repos.bzl:124. It mixes four changes. The fork's issues are disabled, so the only channel is pull requests (maintainer: Marvin Hansen). Split it into separate, tested PRs, and keep in the rules_dx patch only what does not land.

One hazard in the current patch: it renames the internal targets of elixir_app for every user (beam_files becomes <name>_beam_files, ebin becomes <name>_ebin, and so on, even for the default name). An upstream PR must not rename anything for existing users. The name default must stay erlang_app, and with that default the generated target names, output directories (beam_files, ebin, unconsolidated) and the final erlang_app target must be exactly what they are today.

The patch does not reach consumers directly: rules_dx applies it inside its own repo rule, so it is not part of #2283's consumer bug.

Where

  • patches/rules_elixir/hermetic_elixir.patch, patches/rules_elixir/BUILD.bazel
  • elixir/toolchain/pins.bzl (RULES_ELIXIR_COMMIT, RULES_ELIXIR_SHA256, RULES_ELIXIR_URL), elixir/toolchain/repos.bzl:68,124, elixir/toolchain/elixir.bzl:6 (tree)
  • elixir/rules/defs.bzl:73-90: rules_dx's elixir_app wrapper that already forwards target_compatible_with; elixir/README.md:52 (says --warnings-as-errors); elixir/tests/negative/negative_test.rs:268,282 (the warnings-as-errors message)
  • Upstream files (bazelverse/rules_elixir main): elixir_app.bzl:7-100, private/elixir_bytecode.bzl:55-90, private/elixir_build.bzl:15-24 (ElixirInfo), private/elixir_toolchain.bzl:28-70; CI in .github/workflows/test.yaml (bazelisk test //... in test/, plus ! bazelisk test //:failing_test style negative checks)

Fix
Separate PRs, each from a fresh branch on upstream main, each with its own test.

  1. elixir_app gets name, target_compatible_with, testonly and tags. Rules: name = "erlang_app" by default; when name is the default, keep the old internal names and output dirs; for any other name, prefix internal targets and output dirs with name, so two apps can live in one package. Forward target_compatible_with, testonly and tags to every generated target. Test (in test/): the default call still produces :erlang_app, :beam_files, :ebin; a second elixir_app(name = "other", ...) in the same package builds; target_compatible_with incompatible skips the target.
  2. Pass deps to app_file: app_file(..., deps = deps) so the .app file lists dependency applications. Today elixir_app accepts deps but does not hand them to app_file. Test: build an app with a dep and check the .app applications list.
  3. Warnings as errors. First check whether the problem exists on upstream main: build a source with a warning and elixirc_opts = ["--warnings-as-errors"] with Elixir 1.20 and see whether the build fails. The patch runs elixir +elixirc, captures stderr, greps warning: and fails with Compilation failed due to warnings while using the --warnings-as-errors option; it also drops set -x. If upstream already fails correctly, drop this PR and say so. If not, make the smallest change that fails the build, and test with a failing example next to test/test/failing_test.exs using the negative-check pattern in CI.
  4. Relocatable tree support, depends on rules_erlang PR 4 (see the rules_erlang split issue). Add tree to ElixirInfo in private/elixir_build.bzl with a default (the patch adds a bare field, which is absent when a constructor does not pass it; read it with getattr as the patch does, or pass tree = None in elixir_build.bzl). private/elixir_toolchain.bzl: elixir_dirs, erlang_dirs and maybe_install_erlang use tree when set. Use the same neutral variable name as the rules_erlang PR, not a DX_* name. Open this PR only after the rules_erlang PR is merged and the pin includes it. Test: toolchain fixture with tree set builds one .ex file.
  5. For each PR: run bazelisk test //... in test/ and the negative checks from CI in the upstream clone and include the output. Draft the PR title and body. No issue text is needed because issues are disabled.
  6. After a PR lands: bump pins.bzl, regenerate hermetic_elixir.patch without the landed hunks, run elixir/tests. Delete the patch and patches/rules_elixir/ when nothing is left.

Upstream rules (read first)

  • Do not open, edit or comment on any upstream issue or PR, and do not push to any upstream fork. The maintainer submits everything personally.
  • Your output is ready-to-submit material, posted as one comment on this issue: the diff against upstream main (git format-patch or a fenced diff), the test output from the upstream repo, and the drafted upstream issue and PR text.
  • Work in a fresh clone of the upstream repo at its current main. Re-check that no equivalent change or open PR exists before drafting.
  • Write the upstream text in plain, short sentences. State the problem, the change and how it was tested. Do not paste rules_dx internals.
  • The upstream fork has no issue tracker. Do not message the maintainer either.

Done when

  • A comment on this issue holds, per PR: the diff against upstream main, the passing test output from the upstream clone, and the PR text. Up to four PRs, or a note saying why one was dropped (for example warnings-as-errors already works).
  • With name = "erlang_app" the generated target names are unchanged, shown by a test.
  • No upstream PR was opened.
  • After the PRs land: the rules_dx patch holds only what did not land, or is gone.

Related
#2283. rules_erlang split issue (the tree PR here waits for its PR 4).

Activity

  1. added
    P2Improve and finish existing features
    on Oct 26, 2026
  2. TrapsterDK commented on Oct 27, 2026

    @TrapsterDK
    CollaboratorAuthor

    Upstream PR material for bazelverse/rules_elixir

    There are four PRs, each one commit on upstream main. Nothing was opened, pushed or commented upstream.

    Setup

    • Fresh clone of bazelverse/rules_elixir at 8e13ada. This is RULES_ELIXIR_COMMIT and still upstream main.
    • gh pr list --state all shows only the release PR Drill: preset update-loop proof (skew, red test, regen, review, verify) #1, so no equivalent change exists.
    • Bazel 9.2.0 (the repo's .bazelversion).
    • OTP 29.1.2 and Elixir 1.20.4-otp-29 on PATH, with ERLANG_HOME exported, as the CI does. The OTP 28.5 / Elixir 1.19.5 CI pair was not run as a whole. Elixir 1.19.5's warning output and its --warnings-as-errors exit status were checked by hand (run on OTP 29).
    • ~/.bazelrc was skipped (--nohome_rc), so nothing ran remotely.
    • Baseline on main, in test/: //... passes 4/4, the two_file_test log grep matches, and failing_test, broken_test and empty_test fail as expected.

    Elixir bug behind PR 3
    elixirc --warnings-as-errors exits 0 when there are warnings in every released 1.19 and 1.20 that was checked (1.19.5, 1.20.4). Elixir fixed it on main in de015a25d4, which is not backported. So PR 3 is needed and was not dropped. With a fixed Elixir, its check does nothing.

    Order

    1. PR 2.
    2. PR 1.
    3. PR 3. PRs 1–3 can go in any order.
    4. PR 4, after rules_erlang PR 4 (rules_erlang: split hermetic_otp.patch into four upstream PRs to bazelverse/rules_erlang #3743) merges and this repo's MODULE.bazel files point at it.

    Each PR applies alone to main (git apply --check). With git am -3, all four apply together in the order 1, 2, 3, 4. PR 1 and PR 2 change the same app_file(...) call, so PR 2 needs a trivial 3-way merge after PR 1. The resolved call is:

            modules = [":" + beam_files],
            deps = deps,
            **(common | kwargs)

    With all four applied (and the local rules_erlang override for PR 4), test/ //... passes 10/10, the negative checks fail as expected, and each //warnings:* target fails with the message once.

    PR 2: fix(elixir_app): pass deps to app_file

    PR text

    Title: fix(elixir_app): pass deps to app_file

    Problem
    elixir_app accepts deps and compiles against them, but does not pass them to app_file. The generated .app file lists only kernel, stdlib, elixir and extra_apps in applications, so OTP does not start the dependency applications first.

    Change
    Pass deps to app_file. One line. The .app file now lists the dependency apps in applications, so OTP starts them before the app.

    Test
    test/app_deps: an app that depends on a second app, and a sh_test that checks the applications list of its .app file. On main it fails with {applications,[kernel,stdlib,elixir]}; with the fix it is {applications,[kernel,stdlib,elixir,dep_app]}.

    test/ output (bazelisk test //..., CI checks, extra checks)

    (cached) means the same tree had passed earlier in that output base. exit=3 (expected non-zero) is the CI's ! bazelisk test check.

    $ bazelisk test //...
    //:assertion_test                                               (cached) PASSED in 0.3s
    //:escript_test                                                 (cached) PASSED in 0.1s
    //:helped_test                                                  (cached) PASSED in 0.3s
    //:two_file_test                                                (cached) PASSED in 0.3s
    //app_deps:app_file_lists_deps_test                                      PASSED in 0.0s
    Executed 1 out of 5 tests: 5 tests pass.
    exit=0
    $ grep -E "Result: 2 passed|2 tests, 0 failures" bazel-testlogs/two_file_test/test.log
    Result: 2 passed
    exit=0
    $ bazelisk test //:failing_test
    exit=3 (expected non-zero)
    $ bazelisk test //:broken_test
    exit=3 (expected non-zero)
    $ bazelisk test //:empty_test
    exit=3 (expected non-zero)
    $ tail -4 bazel-testlogs/app_deps/app_file_lists_deps_test/test.log
                  {modules,['Elixir.MainApp']},
                  {applications,[kernel,stdlib,elixir,dep_app]},
                  {optional_applications,[]}]}.
    {applications,[kernel,stdlib,elixir,dep_app]}
    exit=0
    $ cd ../ && git stash -q && git checkout -q main -- elixir_app.bzl && git checkout -q pr2-app-file-deps -- test && cd test && bazelisk test //app_deps:app_file_lists_deps_test --color=no --curses=no 2>&1 | grep -E "PASSED|FAILED"; tail -3 bazel-testlogs/app_deps/app_file_lists_deps_test/test.log; cd .. && git checkout -q pr2-app-file-deps -- elixir_app.bzl; git status --short; cd test
    //app_deps:app_file_lists_deps_test                                      FAILED in 0.1s
                  {optional_applications,[]}]}.
    {applications,[kernel,stdlib,elixir]}
    FAIL: dep_app is not in the applications list
    exit=0
    
    git format-patch against main (pr2-app-file-deps)
    From 4a9d51ed87981c6183a096145b6499f1e22bc09f Mon Sep 17 00:00:00 2001
    From: Martin Kuntz Jacobsen <martin.kuntz.jacobsen@gmail.com>
    Date: Tue, 27 Oct 2026 16:11:16 +0100
    Subject: [PATCH] fix(elixir_app): pass deps to app_file
    
    elixir_app accepts deps and compiles against them, but did not pass
    them to app_file. The generated .app file therefore listed only
    kernel, stdlib, elixir and extra_apps in applications, so OTP did not
    start the dependency applications first.
    
    Pass deps to app_file. The .app file now lists the dependency apps.
    
    Test: test/app_deps builds an app that depends on a second app and
    checks that the applications list of its .app file contains it. The
    test fails without the fix.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011u9TXHDZrfmzFQ2WT8QAKK
    ---
     elixir_app.bzl                            |  1 +
     test/app_deps/BUILD.bazel                 | 16 ++++++++++++++++
     test/app_deps/app_file_lists_deps_test.sh | 13 +++++++++++++
     test/app_deps/dep/BUILD.bazel             |  7 +++++++
     test/app_deps/dep/lib/dep_app.ex          |  3 +++
     test/app_deps/lib/main_app.ex             |  3 +++
     6 files changed, 43 insertions(+)
     create mode 100644 test/app_deps/BUILD.bazel
     create mode 100755 test/app_deps/app_file_lists_deps_test.sh
     create mode 100644 test/app_deps/dep/BUILD.bazel
     create mode 100644 test/app_deps/dep/lib/dep_app.ex
     create mode 100644 test/app_deps/lib/main_app.ex
    
    diff --git a/elixir_app.bzl b/elixir_app.bzl
    index 33d8c26..dffd38f 100644
    --- a/elixir_app.bzl
    +++ b/elixir_app.bzl
    @@ -50,6 +50,7 @@ def elixir_app(
             app_name = app_name,
             extra_apps = ["elixir"] + extra_apps,
             modules = [":beam_files"],
    +        deps = deps,
             **kwargs
         )
     
    diff --git a/test/app_deps/BUILD.bazel b/test/app_deps/BUILD.bazel
    new file mode 100644
    index 0000000..0d08c62
    --- /dev/null
    +++ b/test/app_deps/BUILD.bazel
    @@ -0,0 +1,16 @@
    +load("@rules_elixir//:elixir_app.bzl", "elixir_app")
    +load("@rules_shell//shell:sh_test.bzl", "sh_test")
    +
    +elixir_app(
    +    app_name = "main_app",
    +    deps = ["//app_deps/dep:erlang_app"],
    +)
    +
    +# The .app file must list dep_app in applications, so OTP starts it first.
    +sh_test(
    +    name = "app_file_lists_deps_test",
    +    size = "small",
    +    srcs = ["app_file_lists_deps_test.sh"],
    +    args = ["$(rootpath :app_file)"],
    +    data = [":app_file"],
    +)
    diff --git a/test/app_deps/app_file_lists_deps_test.sh b/test/app_deps/app_file_lists_deps_test.sh
    new file mode 100755
    index 0000000..679e297
    --- /dev/null
    +++ b/test/app_deps/app_file_lists_deps_test.sh
    @@ -0,0 +1,13 @@
    +#!/usr/bin/env bash
    +set -euo pipefail
    +
    +app_file="$1"
    +cat "$app_file"
    +applications=$(tr -d '\n' < "$app_file" | grep -o '{applications, *\[[^]]*\]}')
    +echo "$applications"
    +for app in kernel stdlib elixir dep_app; do
    +    if ! grep -Eq "[[ ,]$app[],]" <<< "$applications"; then
    +        echo "FAIL: $app is not in the applications list" >&2
    +        exit 1
    +    fi
    +done
    diff --git a/test/app_deps/dep/BUILD.bazel b/test/app_deps/dep/BUILD.bazel
    new file mode 100644
    index 0000000..754101c
    --- /dev/null
    +++ b/test/app_deps/dep/BUILD.bazel
    @@ -0,0 +1,7 @@
    +load("@rules_elixir//:elixir_app.bzl", "elixir_app")
    +
    +package(default_visibility = ["//app_deps:__pkg__"])
    +
    +elixir_app(
    +    app_name = "dep_app",
    +)
    diff --git a/test/app_deps/dep/lib/dep_app.ex b/test/app_deps/dep/lib/dep_app.ex
    new file mode 100644
    index 0000000..845f32c
    --- /dev/null
    +++ b/test/app_deps/dep/lib/dep_app.ex
    @@ -0,0 +1,3 @@
    +defmodule DepApp do
    +  def hello, do: :world
    +end
    diff --git a/test/app_deps/lib/main_app.ex b/test/app_deps/lib/main_app.ex
    new file mode 100644
    index 0000000..acb1b79
    --- /dev/null
    +++ b/test/app_deps/lib/main_app.ex
    @@ -0,0 +1,3 @@
    +defmodule MainApp do
    +  def hello, do: DepApp.hello()
    +end
    -- 
    2.53.0

    PR 1: feat(elixir_app): add name, target_compatible_with, testonly and tags

    PR text

    Title: feat(elixir_app): add name, target_compatible_with, testonly and tags

    Problem
    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.

    Change

    • New argument name, default "erlang_app". With the default, every target name and output dir stays exactly as it is today.
    • With any other name, the internal targets are <name>_beam_files, <name>_app_file, <name>_ebin and <name>_elixir_without_app_overlap, and the output dirs are <name>/beam_files, <name>/ebin and <name>/unconsolidated.
    • New arguments target_compatible_with, testonly and tags, passed to all five generated targets.

    Test
    In test/:

    • app_targets_test (build_test) builds :erlang_app, :beam_files, :app_file, :ebin, :elixir_without_app_overlap and the five targets of a second app named other.
    • app_names_test checks the output dirs: beam_files, ebin, unconsolidated for the default app (no prefix), and other/beam_files, other/ebin, other/unconsolidated.
    • other is testonly. If testonly were not passed to every target, its final target would fail analysis.
    • incompatible_app has target_compatible_with = ["@platforms//:incompatible"]. //... skips it, and a new CI step checks that bazelisk build //:incompatible_app fails.
    • test/MODULE.bazel gains bazel_skylib and platforms.

    On main the package does not load, because elixir_app has no name argument.

    test/ output (bazelisk test //..., CI checks, extra checks)

    (cached) means the same tree had passed earlier in that output base. exit=3 (expected non-zero) is the CI's ! bazelisk test check.

    $ bazelisk test //...
    //:app_targets_test                                             (cached) PASSED in 0.0s
    //:assertion_test                                               (cached) PASSED in 0.3s
    //:escript_test                                                 (cached) PASSED in 0.1s
    //:helped_test                                                  (cached) PASSED in 0.3s
    //:two_file_test                                                (cached) PASSED in 0.3s
    //:app_names_test                                                        PASSED in 0.0s
    Executed 1 out of 6 tests: 6 tests pass.
    exit=0
    $ grep -E "Result: 2 passed|2 tests, 0 failures" bazel-testlogs/two_file_test/test.log
    Result: 2 passed
    exit=0
    $ bazelisk test //:failing_test
    exit=3 (expected non-zero)
    $ bazelisk test //:broken_test
    exit=3 (expected non-zero)
    $ bazelisk test //:empty_test
    exit=3 (expected non-zero)
    $ if bazelisk build //:incompatible_app --color=no --curses=no 2>&1 | grep -E "^ERROR" | head -3; [ ${PIPESTATUS[0]} = 0 ]; then echo "BUILT (bad)"; false; else echo "failed as expected"; fi
    ERROR: Analysis of target '//:incompatible_app' failed; build aborted: Target //:incompatible_app is incompatible and cannot be built, but was explicitly requested.
    failed as expected
    exit=0
    $ cat bazel-testlogs/app_names_test/test.log | tail -6
    ok: beam_files
    ok: ebin
    ok: unconsolidated
    ok: other/beam_files
    ok: other/ebin
    ok: other/unconsolidated
    exit=0
    
    git format-patch against main (pr1-elixir-app-name)
    From bac8560ca54eef323456a751c116dba1f0b4a5ca Mon Sep 17 00:00:00 2001
    From: Martin Kuntz Jacobsen <martin.kuntz.jacobsen@gmail.com>
    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 <name>_beam_files, <name>_app_file,
    <name>_ebin and <name>_elixir_without_app_overlap, and their output
    dirs are <name>/beam_files, <name>/ebin and <name>/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).
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011u9TXHDZrfmzFQ2WT8QAKK
    ---
     .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 "<name>_", and their output dirs
    +          with "<name>/", 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 "<expected path>=<rootpath>". 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
    -- 
    2.53.0

    PR 3: fix(elixir_bytecode): fail the build on warnings with --warnings-as-errors

    PR text

    Title: fix(elixir_bytecode): fail the build on warnings with --warnings-as-errors

    Problem
    In released Elixir, elixirc --warnings-as-errors exits 0 when there are warnings. Kernel.CLI passes the option to Kernel.ParallelCompiler, which no longer reads it. This was checked with 1.20.4 and 1.19.5. test/BUILD.bazel already passes --warnings-as-errors to the basic app, but nothing checks that it fails, so this went unnoticed.

    Elixir fixed it on main in de015a25d4 ("Fail elixirc on warnings with --warnings-as-errors"). That commit is not in a release yet.

    Change

    • When --warnings-as-errors is in elixirc_opts, the ELIXIRC action captures stderr, prints it again, and fails if elixirc exited 0 but printed a warning: line. It prints Compilation failed due to warnings while using the --warnings-as-errors option, the same message as Mix and the fixed elixirc.
    • The launcher's own warning about a latin1 file name encoding (printed when the VM does not run with +fnu) is not a compiler warning and does not count. Without this exclusion, the basic app in test/ fails under --incompatible_strict_action_env.
    • When elixirc already fails, its status and output are kept unchanged. A compile error still fails with only Elixir's diagnostics. With an Elixir that contains de015a25d4, elixirc fails first and the check does nothing, so the message is not printed twice. The check can be removed once the supported Elixir versions all contain that fix.
    • bin/elixirc is still used. It execs elixir +elixirc.
    • set -x is removed. It wrote a trace of the elixirc command to stderr for every action, and Bazel printed it even when the build succeeded.

    Test

    • test/warnings: three elixir_bytecode targets with --warnings-as-errors, tagged manual. They cover an unused variable, a call to an undefined function (reported after the module compiles), and IO.warn at compile time (printed without indentation).
    • A new CI step checks that each one fails and prints the message. It tests the status with if, because ! cmd does not stop a bash -e step. (The existing ! bazelisk test lines have the same issue: only the last one decides the step's status.)
    • //warnings:allowed compiles the unused-variable source without the option and builds as part of //....
    • On main, all three targets build successfully.
    test/ output (bazelisk test //..., CI checks, extra checks)

    (cached) means the same tree had passed earlier in that output base. exit=3 (expected non-zero) is the CI's ! bazelisk test check.

    $ bazelisk test //...
    //:assertion_test                                               (cached) PASSED in 0.3s
    //:helped_test                                                  (cached) PASSED in 0.3s
    //:two_file_test                                                (cached) PASSED in 0.3s
    //:escript_test                                                          PASSED in 0.1s
    Executed 1 out of 4 tests: 4 tests pass.
    exit=0
    $ grep -E "Result: 2 passed|2 tests, 0 failures" bazel-testlogs/two_file_test/test.log
    Result: 2 passed
    exit=0
    $ bazelisk test //:failing_test
    exit=3 (expected non-zero)
    $ bazelisk test //:broken_test
    exit=3 (expected non-zero)
    $ bazelisk test //:empty_test
    exit=3 (expected non-zero)
    $ for app in unused undef io_warn; do if bazelisk build //warnings:$app --color=no --curses=no 2> w_$app.log; then echo "//warnings:$app built (bad)"; else grep -E "warning:|Compilation failed" w_$app.log | cut -c1-120; fi; done; rm -f w_*.log
    warning: the VM is running with native name encoding of latin1 which may cause Elixir to malfunction as it expects utf8.
        warning: variable "y" is unused (if the variable is not meant to be used, prefix it with an underscore)
    Compilation failed due to warnings while using the --warnings-as-errors option
    warning: the VM is running with native name encoding of latin1 which may cause Elixir to malfunction as it expects utf8.
        warning: NoSuchModule.nope/0 is undefined (module NoSuchModule is not available or is yet to be defined). Make sure 
    Compilation failed due to warnings while using the --warnings-as-errors option
    warning: the VM is running with native name encoding of latin1 which may cause Elixir to malfunction as it expects utf8.
    warning: a warning from IO.warn at compile time
    Compilation failed due to warnings while using the --warnings-as-errors option
    exit=0
    $ cd .. && git checkout -q main -- private/elixir_bytecode.bzl && cd test && for app in unused undef io_warn; do bazelisk build //warnings:$app --color=no --curses=no 2>&1 | grep -E "Build completed|ERROR: Build" ; done; cd .. && git checkout -q pr3-warnings-as-errors -- private/elixir_bytecode.bzl; git status --short; cd test
    exit=0
    
    git format-patch against main (pr3-warnings-as-errors)
    From a919242e8edf50c6b5bd4bb21a0deec74673f6a6 Mon Sep 17 00:00:00 2001
    From: Martin Kuntz Jacobsen <martin.kuntz.jacobsen@gmail.com>
    Date: Tue, 27 Oct 2026 16:11:16 +0100
    Subject: [PATCH] fix(elixir_bytecode): fail the build on warnings with
     --warnings-as-errors
    
    In released Elixir (checked with 1.19.5 and 1.20.4), elixirc accepts
    --warnings-as-errors but exits 0 when there are warnings.
    Kernel.CLI passes the option to Kernel.ParallelCompiler, which no
    longer reads it. Elixir fixed this on main in de015a25d4 ("Fail
    elixirc on warnings with --warnings-as-errors"), which is not in a
    release yet.
    
    When --warnings-as-errors is in elixirc_opts, capture elixirc's
    stderr, print it again, and fail if elixirc exited 0 but printed a
    warning, with the same message that Mix and fixed elixirc use. The
    launcher's warning about a latin1 file name encoding is not a compiler
    warning and does not count. When elixirc already fails, its own status
    and output are kept, so with a fixed Elixir the check does nothing.
    
    Also drop "set -x". It wrote a trace to stderr for every action, which
    Bazel printed even when the build succeeded.
    
    Tests: test/warnings has sources with an unused variable, a call to an
    undefined function and an IO.warn at compile time, each compiled with
    --warnings-as-errors. CI checks that each fails with the message. The
    unused variable source without the option still builds in //....
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011u9TXHDZrfmzFQ2WT8QAKK
    ---
     .github/workflows/test.yaml | 14 ++++++++++++++
     private/elixir_bytecode.bzl | 20 ++++++++++++++++++--
     test/warnings/BUILD.bazel   | 29 +++++++++++++++++++++++++++++
     test/warnings/io_warn.ex    |  7 +++++++
     test/warnings/undef.ex      |  3 +++
     test/warnings/unused.ex     |  6 ++++++
     6 files changed, 77 insertions(+), 2 deletions(-)
     create mode 100644 test/warnings/BUILD.bazel
     create mode 100644 test/warnings/io_warn.ex
     create mode 100644 test/warnings/undef.ex
     create mode 100644 test/warnings/unused.ex
    
    diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
    index fa5e2e5..2fdd92e 100644
    --- a/.github/workflows/test.yaml
    +++ b/.github/workflows/test.yaml
    @@ -77,6 +77,20 @@ jobs:
               ! bazelisk test //:failing_test
               ! bazelisk test //:broken_test
               ! bazelisk test //:empty_test
    +      - name: WARNINGS AS ERRORS FAIL
    +        working-directory: test
    +        run: |
    +          # "! cmd" does not stop a "bash -e" step, so test the status
    +          # explicitly.
    +          for app in unused undef io_warn; do
    +            if bazelisk build //warnings:$app 2> warnings_$app.log; then
    +              echo "//warnings:$app built despite --warnings-as-errors"
    +              exit 1
    +            fi
    +            cat warnings_$app.log
    +            grep -F "Compilation failed due to warnings while using the --warnings-as-errors option" \
    +              warnings_$app.log
    +          done
           - name: UPLOAD TEST LOGS
             if: always()
             uses: actions/upload-artifact@v4
    diff --git a/private/elixir_bytecode.bzl b/private/elixir_bytecode.bzl
    index 2359411..3e7acc6 100644
    --- a/private/elixir_bytecode.bzl
    +++ b/private/elixir_bytecode.bzl
    @@ -58,11 +58,26 @@ fi
     {env}
     
     {setup}
    -set -x
    +diagnostics=$(mktemp)
    +status=0
     ${{ABS_ELIXIR_HOME}}/bin/elixirc \\
         -o {out_dir} \\
         {elixirc_opts} \\
    -    {srcs}
    +    {srcs} 2>"$diagnostics" || status=$?
    +cat "$diagnostics" >&2
    +# Released Elixir ignores --warnings-as-errors in elixirc and exits 0 on
    +# warnings. Fail here instead. Once elixirc fails by itself, status is
    +# not 0 and this check does nothing. The launcher's warning about a
    +# latin1 file name encoding is not a compiler warning, so it is skipped.
    +if [ "$status" = 0 ] && [ -n "{warnings_as_errors}" ] && awk '
    +    /^[[:space:]]*warning:/ && !/native name encoding of latin1/ {{ found = 1 }}
    +    END {{ exit !found }}
    +' "$diagnostics"; then
    +    echo "Compilation failed due to warnings while using the --warnings-as-errors option" >&2
    +    status=1
    +fi
    +rm -f "$diagnostics"
    +exit "$status"
     """.format(
             maybe_install_erlang = maybe_install_erlang(ctx),
             erlang_home = erlang_home,
    @@ -73,6 +88,7 @@ ${{ABS_ELIXIR_HOME}}/bin/elixirc \\
             out_dir = ebin.path,
             elixirc_opts = " ".join([shell.quote(opt) for opt in ctx.attr.elixirc_opts]),
             srcs = " ".join([f.path for f in ctx.files.srcs]),
    +        warnings_as_errors = "1" if "--warnings-as-errors" in ctx.attr.elixirc_opts else "",
         )
     
         inputs = depset(
    diff --git a/test/warnings/BUILD.bazel b/test/warnings/BUILD.bazel
    new file mode 100644
    index 0000000..ab9779a
    --- /dev/null
    +++ b/test/warnings/BUILD.bazel
    @@ -0,0 +1,29 @@
    +load("@rules_elixir//private:elixir_bytecode.bzl", "elixir_bytecode")
    +
    +# Each source has one warning. With --warnings-as-errors the build must
    +# fail; .github/workflows/test.yaml checks that, so these are manual.
    +#   unused:  an unused variable, a compile-time warning
    +#   undef:   a call to an undefined remote function, a warning the
    +#            compiler emits after the module is compiled
    +#   io_warn: IO.warn at compile time, printed without indentation
    +[
    +    elixir_bytecode(
    +        name = name,
    +        srcs = [name + ".ex"],
    +        dest = name,
    +        elixirc_opts = ["--warnings-as-errors"],
    +        tags = ["manual"],
    +    )
    +    for name in [
    +        "unused",
    +        "undef",
    +        "io_warn",
    +    ]
    +]
    +
    +# The same warning as :unused, without --warnings-as-errors. It builds.
    +elixir_bytecode(
    +    name = "allowed",
    +    srcs = ["unused.ex"],
    +    dest = "allowed",
    +)
    diff --git a/test/warnings/io_warn.ex b/test/warnings/io_warn.ex
    new file mode 100644
    index 0000000..57e4b19
    --- /dev/null
    +++ b/test/warnings/io_warn.ex
    @@ -0,0 +1,7 @@
    +defmodule Warnings.IoWarn do
    +  # A warning printed by code that runs at compile time. Unlike the
    +  # compiler's own warnings, it is not indented.
    +  IO.warn("a warning from IO.warn at compile time")
    +
    +  def f, do: :ok
    +end
    diff --git a/test/warnings/undef.ex b/test/warnings/undef.ex
    new file mode 100644
    index 0000000..e1d16f6
    --- /dev/null
    +++ b/test/warnings/undef.ex
    @@ -0,0 +1,3 @@
    +defmodule Warnings.Undef do
    +  def f, do: NoSuchModule.nope()
    +end
    diff --git a/test/warnings/unused.ex b/test/warnings/unused.ex
    new file mode 100644
    index 0000000..0d4454d
    --- /dev/null
    +++ b/test/warnings/unused.ex
    @@ -0,0 +1,6 @@
    +defmodule Warnings.Unused do
    +  def f(x) do
    +    y = 1
    +    x
    +  end
    +end
    -- 
    2.53.0

    PR 4: feat(ElixirInfo): support a relocatable tree

    PR text

    Title: feat(ElixirInfo): support a relocatable tree

    Submit after the rules_erlang PR "feat(OtpInfo): add an optional relocatable tree" is merged, and MODULE.bazel and test/MODULE.bazel point at a rules_erlang that contains it. It loads TREE_ERLANG_HOME from @rules_erlang//tools:erlang_toolchain.bzl.

    Problem
    A toolchain can only provide elixir as an absolute elixir_home or as a release_dir, and erlang as an absolute erlang_home or a release tar. An elixir and erlang that live inside the build cannot be used as plain inputs and runfiles.

    Change

    • ElixirInfo gets an optional, documented tree field: a depset of every file of a relocatable elixir whose root directory holds version_file. The fields are now a dict with docs. The three ElixirInfo(...) calls in this repository pass tree = None, and the field is read with getattr, so other providers do not need it.
    • private/elixir_toolchain.bzl, the same way as rules_erlang does for OtpInfo.tree:
      • erlang_dirs: with OtpInfo.tree set, returns TREE_ERLANG_HOME and runfiles with version_file and the tree.
      • maybe_install_erlang: assigns that variable from the directory of the OTP version_file (path in actions, short path in tests) instead of unpacking a tar.
      • elixir_dirs: with ElixirInfo.tree set, returns the directory of the elixir version_file and runfiles with version_file and the tree.
    • Providers without a tree keep the current behaviour.

    Test
    test/tree uses a fake toolchain. Its erlang and elixir are stub scripts in the package.

    • elixirc_with_tree_test (analysis): the ELIXIRC action derives both homes, has every tree file as input, and unpacks no tar.
    • ex_unit_with_tree_test (analysis): the same checks for the ex_unit_test script, using short paths and runfiles.
    • elixirc_runs_from_tree_test builds a .ex file with the stub elixirc and checks that elixirc and erl ran from the tree.

    To run these before the rules_erlang PR merges, test/MODULE.bazel was changed locally (not part of this patch). The change was a git_override of rules_erlang at 13977b3 with that PR's git format-patch in patches.

    test/ output (bazelisk test //..., CI checks, extra checks)

    (cached) means the same tree had passed earlier in that output base. exit=3 (expected non-zero) is the CI's ! bazelisk test check.

    $ bazelisk test //...
    //:assertion_test                                               (cached) PASSED in 0.3s
    //:escript_test                                                 (cached) PASSED in 0.1s
    //:helped_test                                                  (cached) PASSED in 0.3s
    //:two_file_test                                                (cached) PASSED in 0.3s
    //tree:elixirc_runs_from_tree_test                              (cached) PASSED in 0.0s
    //tree:elixirc_with_tree_test                                            PASSED in 0.0s
    //tree:ex_unit_with_tree_test                                            PASSED in 0.0s
    Executed 2 out of 7 tests: 7 tests pass.
    exit=0
    $ grep -E "Result: 2 passed|2 tests, 0 failures" bazel-testlogs/two_file_test/test.log
    Result: 2 passed
    exit=0
    $ bazelisk test //:failing_test
    exit=3 (expected non-zero)
    $ bazelisk test //:broken_test
    exit=3 (expected non-zero)
    $ bazelisk test //:empty_test
    exit=3 (expected non-zero)
    $ tail -3 bazel-testlogs/tree/elixirc_runs_from_tree_test/test.log
    -----------------------------------------------------------------------------
    elixirc=/home/trapsterdk/.cache/bazel/_bazel_trapsterdk/bf80d84778a53764885f54335d9f773b/sandbox/linux-sandbox/144/execroot/_main/tree/fake_elixir/bin/elixirc
    erl=/home/trapsterdk/.cache/bazel/_bazel_trapsterdk/bf80d84778a53764885f54335d9f773b/sandbox/linux-sandbox/144/execroot/_main/tree/fake_otp/bin/erl
    exit=0
    
    git format-patch against main (pr4-elixir-tree)
    From 63ee4a97e6ce6a267056e3ad33389be2a7e41ebc Mon Sep 17 00:00:00 2001
    From: Martin Kuntz Jacobsen <martin.kuntz.jacobsen@gmail.com>
    Date: Tue, 27 Oct 2026 16:11:16 +0100
    Subject: [PATCH] feat(ElixirInfo): support a relocatable tree
    
    Pairs with the rules_erlang change that adds OtpInfo.tree. A toolchain
    can only provide elixir as an absolute elixir_home or as a
    release_dir, and erlang as an absolute erlang_home or a release tar.
    An elixir and erlang that live inside the build cannot be used as
    plain inputs and runfiles.
    
    Add an optional, documented tree field to ElixirInfo: a depset of
    every file of a relocatable elixir whose root directory holds
    version_file. The rules in this repository pass tree = None, and the
    field is read with getattr, so other providers do not need it.
    
    In private/elixir_toolchain.bzl, as rules_erlang does for OtpInfo:
    - erlang_dirs: with OtpInfo.tree set, return TREE_ERLANG_HOME (loaded
      from rules_erlang) and runfiles with version_file and the tree.
    - maybe_install_erlang: assign that variable from the directory of
      the OTP version_file instead of unpacking a tar.
    - elixir_dirs: with ElixirInfo.tree set, return the directory of the
      elixir version_file (path, or short_path for tests) and runfiles with
      version_file and the tree.
    
    Tests in test/tree, with a fake toolchain whose erlang and elixir are
    stub scripts in the package: analysis tests check that the ELIXIRC
    action and the ex_unit_test script derive both homes, have every tree
    file as input or runfile and unpack no tar; a sh_test builds a .ex
    file with the stub elixirc and checks that it and erl ran from the
    tree.
    
    Needs a rules_erlang release with OtpInfo.tree and TREE_ERLANG_HOME.
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011u9TXHDZrfmzFQ2WT8QAKK
    ---
     private/elixir_build.bzl                 |  21 +++-
     private/elixir_toolchain.bzl             |  23 ++++
     test/MODULE.bazel                        |   4 +
     test/tree/BUILD.bazel                    |  63 +++++++++++
     test/tree/elixirc_runs_from_tree_test.sh |   8 ++
     test/tree/fake_elixir/VERSION            |   1 +
     test/tree/fake_elixir/bin/elixirc        |  17 +++
     test/tree/fake_otp/OTP_VERSION           |   1 +
     test/tree/fake_otp/bin/erl               |   4 +
     test/tree/lib.ex                         |   2 +
     test/tree/test.exs                       |   1 +
     test/tree/tree_test.bzl                  | 127 +++++++++++++++++++++++
     12 files changed, 267 insertions(+), 5 deletions(-)
     create mode 100644 test/tree/BUILD.bazel
     create mode 100755 test/tree/elixirc_runs_from_tree_test.sh
     create mode 100644 test/tree/fake_elixir/VERSION
     create mode 100755 test/tree/fake_elixir/bin/elixirc
     create mode 100644 test/tree/fake_otp/OTP_VERSION
     create mode 100755 test/tree/fake_otp/bin/erl
     create mode 100644 test/tree/lib.ex
     create mode 100644 test/tree/test.exs
     create mode 100644 test/tree/tree_test.bzl
    
    diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl
    index 503e1ac..97096e7 100644
    --- a/private/elixir_build.bzl
    +++ b/private/elixir_build.bzl
    @@ -15,11 +15,19 @@ load(
     
     ElixirInfo = provider(
         doc = "A Home directory of a built Elixir",
    -    fields = [
    -        "release_dir",
    -        "elixir_home",
    -        "version_file",
    -    ],
    +    fields = {
    +        "release_dir": "A directory holding the Elixir installation, or None",
    +        "elixir_home": "An absolute path to an external Elixir, or None",
    +        "version_file": """A file containing the version of this
    +elixir, used to correctly invalidate the cache when an
    +external elixir is used""",
    +        "tree": """Optional. A depset of every file of a relocatable
    +elixir installation whose root directory holds version_file. When set,
    +rules put these files in their inputs and runfiles and find the
    +installation next to version_file, so release_dir and elixir_home are
    +not used by the rules. Providers that do not set this field keep the
    +current behaviour.""",
    +    },
     )
     
     def _impl(ctx):
    @@ -144,6 +152,7 @@ export PATH="{erlang_home}"/bin:${{PATH}}
                 release_dir = release_dir,
                 elixir_home = None,
                 version_file = version_file,
    +            tree = None,
             ),
         ]
     
    @@ -292,6 +301,7 @@ export PATH="{erlang_home}"/bin:${{PATH}}
                 release_dir = release_dir,
                 elixir_home = None,
                 version_file = version_file,
    +            tree = None,
             ),
         ]
     
    @@ -344,6 +354,7 @@ export PATH="{erlang_home}"/bin:${{PATH}}
                 release_dir = None,
                 elixir_home = elixir_home,
                 version_file = version_file,
    +            tree = None,
             ),
         ]
     
    diff --git a/private/elixir_toolchain.bzl b/private/elixir_toolchain.bzl
    index b13b154..3ccb156 100644
    --- a/private/elixir_toolchain.bzl
    +++ b/private/elixir_toolchain.bzl
    @@ -2,6 +2,10 @@ load(
         "@rules_erlang//private:erlang_build.bzl",
         "OtpInfo",
     )
    +load(
    +    "@rules_erlang//tools:erlang_toolchain.bzl",
    +    "TREE_ERLANG_HOME",
    +)
     load(
         ":elixir_build.bzl",
         "ElixirInfo",
    @@ -28,8 +32,17 @@ elixir_toolchain = rule(
     def _build_info(ctx):
         return ctx.toolchains["//:toolchain_type"].otpinfo
     
    +def _tree(info):
    +    return getattr(info, "tree", None)
    +
     def erlang_dirs(ctx):
         info = _build_info(ctx)
    +    tree = _tree(info)
    +    if tree != None:
    +        runfiles = ctx.runfiles(
    +            transitive_files = depset([info.version_file], transitive = [tree]),
    +        )
    +        return (TREE_ERLANG_HOME, None, runfiles)
         if info.release_dir_tar != None:
             runfiles = ctx.runfiles([
                 info.release_dir_tar,
    @@ -43,6 +56,13 @@ def erlang_dirs(ctx):
     
     def elixir_dirs(ctx, short_path = False):
         info = ctx.toolchains["//:toolchain_type"].elixirinfo
    +    tree = _tree(info)
    +    if tree != None:
    +        version_file = info.version_file.short_path if short_path else info.version_file.path
    +        runfiles = ctx.runfiles(
    +            transitive_files = depset([info.version_file], transitive = [tree]),
    +        )
    +        return (version_file.rpartition("/")[0], runfiles)
         if info.elixir_home != None:
             return (info.elixir_home, ctx.runfiles([info.version_file]))
         else:
    @@ -51,6 +71,9 @@ def elixir_dirs(ctx, short_path = False):
     
     def maybe_install_erlang(ctx, short_path = False):
         info = _build_info(ctx)
    +    if _tree(info) != None:
    +        version_file = info.version_file.short_path if short_path else info.version_file.path
    +        return 'RULES_ERLANG_HOME="$(cd "$(dirname "{}")" && pwd)"'.format(version_file)
         release_dir_tar = info.release_dir_tar
         if release_dir_tar == None:
             return ""
    diff --git a/test/MODULE.bazel b/test/MODULE.bazel
    index ce85484..503a938 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",
    diff --git a/test/tree/BUILD.bazel b/test/tree/BUILD.bazel
    new file mode 100644
    index 0000000..ea47cb0
    --- /dev/null
    +++ b/test/tree/BUILD.bazel
    @@ -0,0 +1,63 @@
    +load("@rules_elixir//:elixir_toolchain.bzl", "elixir_toolchain")
    +load("@rules_elixir//private:elixir_bytecode.bzl", "elixir_bytecode")
    +load("@rules_elixir//private:ex_unit_test.bzl", "ex_unit_test")
    +load("@rules_shell//shell:sh_test.bzl", "sh_test")
    +load(
    +    ":tree_test.bzl",
    +    "fake_tree_elixir",
    +    "tree_test_suite",
    +    "with_toolchain",
    +)
    +
    +# A relocatable erlang and elixir whose files are all in this package.
    +# Their bin/ holds shell stubs.
    +fake_tree_elixir(
    +    name = "fake_tree_elixir",
    +    elixir_srcs = glob(["fake_elixir/**"]),
    +    elixir_version_file = "fake_elixir/VERSION",
    +    otp_srcs = glob(["fake_otp/**"]),
    +    otp_version_file = "fake_otp/OTP_VERSION",
    +)
    +
    +elixir_toolchain(
    +    name = "fake_tree_elixir_toolchain",
    +    elixir = ":fake_tree_elixir",
    +)
    +
    +toolchain(
    +    name = "fake_tree_toolchain",
    +    toolchain = ":fake_tree_elixir_toolchain",
    +    toolchain_type = "@rules_elixir//:toolchain_type",
    +)
    +
    +elixir_bytecode(
    +    name = "beam_files",
    +    srcs = ["lib.ex"],
    +    dest = "beam_files",
    +    tags = ["manual"],
    +)
    +
    +ex_unit_test(
    +    name = "fake_ex_unit",
    +    srcs = ["test.exs"],
    +    is_windows = False,
    +    tags = ["manual"],
    +)
    +
    +# Builds :beam_files with the tree toolchain, so the stub elixirc
    +# actually runs from the homes that the toolchain derived.
    +with_toolchain(
    +    name = "beam_files_with_tree",
    +    target = ":beam_files",
    +    toolchain = ":fake_tree_toolchain",
    +)
    +
    +sh_test(
    +    name = "elixirc_runs_from_tree_test",
    +    size = "small",
    +    srcs = ["elixirc_runs_from_tree_test.sh"],
    +    args = ["$(rootpath :beam_files_with_tree)/where.txt"],
    +    data = [":beam_files_with_tree"],
    +)
    +
    +tree_test_suite(name = "tree_test")
    diff --git a/test/tree/elixirc_runs_from_tree_test.sh b/test/tree/elixirc_runs_from_tree_test.sh
    new file mode 100755
    index 0000000..92133e0
    --- /dev/null
    +++ b/test/tree/elixirc_runs_from_tree_test.sh
    @@ -0,0 +1,8 @@
    +#!/usr/bin/env bash
    +set -euo pipefail
    +
    +cat "$1"
    +grep -Eq '^elixirc=/.*/tree/fake_elixir/bin/elixirc$' "$1" ||
    +    { echo "elixirc did not run from the tree" >&2; exit 1; }
    +grep -Eq '^erl=/.*/tree/fake_otp/bin/erl$' "$1" ||
    +    { echo "erl is not taken from the tree" >&2; exit 1; }
    diff --git a/test/tree/fake_elixir/VERSION b/test/tree/fake_elixir/VERSION
    new file mode 100644
    index 0000000..69c5cbf
    --- /dev/null
    +++ b/test/tree/fake_elixir/VERSION
    @@ -0,0 +1 @@
    +0.0-fake
    diff --git a/test/tree/fake_elixir/bin/elixirc b/test/tree/fake_elixir/bin/elixirc
    new file mode 100755
    index 0000000..ed58c1c
    --- /dev/null
    +++ b/test/tree/fake_elixir/bin/elixirc
    @@ -0,0 +1,17 @@
    +#!/usr/bin/env bash
    +# Stand-in for elixirc from a relocatable elixir. Writes where it and erl
    +# ran from into the -o directory, so tests can check the derived homes.
    +set -euo pipefail
    +out=""
    +while [ $# -gt 0 ]; do
    +    if [ "$1" = "-o" ]; then
    +        out="$2"
    +        shift
    +    fi
    +    shift
    +done
    +mkdir -p "$out"
    +{
    +    echo "elixirc=$0"
    +    echo "erl=$(command -v erl)"
    +} > "$out/where.txt"
    diff --git a/test/tree/fake_otp/OTP_VERSION b/test/tree/fake_otp/OTP_VERSION
    new file mode 100644
    index 0000000..69c5cbf
    --- /dev/null
    +++ b/test/tree/fake_otp/OTP_VERSION
    @@ -0,0 +1 @@
    +0.0-fake
    diff --git a/test/tree/fake_otp/bin/erl b/test/tree/fake_otp/bin/erl
    new file mode 100755
    index 0000000..c640b7a
    --- /dev/null
    +++ b/test/tree/fake_otp/bin/erl
    @@ -0,0 +1,4 @@
    +#!/usr/bin/env bash
    +# Stand-in for erl from a relocatable erlang. Never run by these tests;
    +# fake elixirc only checks that it is first on PATH.
    +exit 0
    diff --git a/test/tree/lib.ex b/test/tree/lib.ex
    new file mode 100644
    index 0000000..0794c7f
    --- /dev/null
    +++ b/test/tree/lib.ex
    @@ -0,0 +1,2 @@
    +defmodule Tree do
    +end
    diff --git a/test/tree/test.exs b/test/tree/test.exs
    new file mode 100644
    index 0000000..869559e
    --- /dev/null
    +++ b/test/tree/test.exs
    @@ -0,0 +1 @@
    +ExUnit.start()
    diff --git a/test/tree/tree_test.bzl b/test/tree/tree_test.bzl
    new file mode 100644
    index 0000000..2fe6cfb
    --- /dev/null
    +++ b/test/tree/tree_test.bzl
    @@ -0,0 +1,127 @@
    +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
    +load("@rules_elixir//private:elixir_build.bzl", "ElixirInfo")
    +load("@rules_erlang//private:erlang_build.bzl", "OtpInfo")
    +
    +def _fake_tree_elixir_impl(ctx):
    +    return [
    +        OtpInfo(
    +            version = "0.0",
    +            release_dir_tar = None,
    +            install_path = None,
    +            # Rules must not use this when tree is set.
    +            erlang_home = "/fake_tree/erlang_home/not/used",
    +            version_file = ctx.file.otp_version_file,
    +            tree = depset(ctx.files.otp_srcs),
    +        ),
    +        ElixirInfo(
    +            release_dir = None,
    +            # Rules must not use this when tree is set.
    +            elixir_home = "/fake_tree/elixir_home/not/used",
    +            version_file = ctx.file.elixir_version_file,
    +            tree = depset(ctx.files.elixir_srcs),
    +        ),
    +    ]
    +
    +fake_tree_elixir = rule(
    +    implementation = _fake_tree_elixir_impl,
    +    attrs = {
    +        "elixir_srcs": attr.label_list(allow_files = True, mandatory = True),
    +        "elixir_version_file": attr.label(allow_single_file = True, mandatory = True),
    +        "otp_srcs": attr.label_list(allow_files = True, mandatory = True),
    +        "otp_version_file": attr.label(allow_single_file = True, mandatory = True),
    +    },
    +    provides = [OtpInfo, ElixirInfo],
    +)
    +
    +def _extra_toolchains_transition_impl(_settings, attr):
    +    return {"//command_line_option:extra_toolchains": [str(attr.toolchain)]}
    +
    +_extra_toolchains_transition = transition(
    +    implementation = _extra_toolchains_transition_impl,
    +    inputs = [],
    +    outputs = ["//command_line_option:extra_toolchains"],
    +)
    +
    +def _with_toolchain_impl(ctx):
    +    return [DefaultInfo(files = depset(ctx.files.target))]
    +
    +with_toolchain = rule(
    +    implementation = _with_toolchain_impl,
    +    attrs = {
    +        "target": attr.label(cfg = _extra_toolchains_transition),
    +        "toolchain": attr.label(),
    +    },
    +)
    +
    +_TREE = str(Label(":fake_tree_toolchain"))
    +_PKG = "tree/"
    +_TREE_FILES = [
    +    "fake_elixir/VERSION",
    +    "fake_elixir/bin/elixirc",
    +    "fake_otp/OTP_VERSION",
    +    "fake_otp/bin/erl",
    +]
    +_HOME = 'RULES_ERLANG_HOME="$(cd "$(dirname "{}fake_otp/OTP_VERSION")" && pwd)"'.format(_PKG)
    +
    +def _assert_tree(env, text, paths, what):
    +    asserts.true(env, _HOME in text, "erlang home is not derived:\n" + text)
    +    asserts.true(env, '"${RULES_ERLANG_HOME}"/bin' in text, "erlang home not on PATH:\n" + text)
    +    asserts.true(env, 'ABS_ELIXIR_HOME=$PWD/' + _PKG + "fake_elixir\n" in text, "elixir home is not the tree:\n" + text)
    +    asserts.false(env, "tar " in text, "tree toolchain unpacks a tar:\n" + text)
    +    asserts.false(env, "/fake_tree/" in text, "erlang_home or elixir_home used:\n" + text)
    +    for f in _TREE_FILES:
    +        asserts.true(env, _PKG + f in paths, "{} misses {}".format(what, f))
    +
    +def _elixirc_with_tree_test_impl(ctx):
    +    env = analysistest.begin(ctx)
    +    actions = [a for a in analysistest.target_actions(env) if a.mnemonic == "ELIXIRC"]
    +    asserts.equals(env, 1, len(actions), "expected one ELIXIRC action")
    +    if actions:
    +        _assert_tree(
    +            env,
    +            " ".join(actions[0].argv),
    +            [f.short_path for f in actions[0].inputs.to_list()],
    +            "action inputs",
    +        )
    +    return analysistest.end(env)
    +
    +elixirc_with_tree_test = analysistest.make(
    +    _elixirc_with_tree_test_impl,
    +    config_settings = {"//command_line_option:extra_toolchains": [_TREE]},
    +)
    +
    +def _ex_unit_with_tree_test_impl(ctx):
    +    env = analysistest.begin(ctx)
    +    target = analysistest.target_under_test(env)
    +    writes = [a for a in analysistest.target_actions(env) if a.mnemonic == "FileWrite"]
    +    asserts.equals(env, 1, len(writes), "expected one FileWrite action")
    +    if writes:
    +        _assert_tree(
    +            env,
    +            writes[0].content,
    +            [f.short_path for f in target[DefaultInfo].default_runfiles.files.to_list()],
    +            "runfiles",
    +        )
    +    return analysistest.end(env)
    +
    +ex_unit_with_tree_test = analysistest.make(
    +    _ex_unit_with_tree_test_impl,
    +    config_settings = {"//command_line_option:extra_toolchains": [_TREE]},
    +)
    +
    +def tree_test_suite(name):
    +    elixirc_with_tree_test(
    +        name = "elixirc_with_tree_test",
    +        target_under_test = ":beam_files",
    +    )
    +    ex_unit_with_tree_test(
    +        name = "ex_unit_with_tree_test",
    +        target_under_test = ":fake_ex_unit",
    +    )
    +    native.test_suite(
    +        name = name,
    +        tests = [
    +            ":elixirc_with_tree_test",
    +            ":ex_unit_with_tree_test",
    +        ],
    +    )
    -- 
    2.53.0

    Full series

    test/ output with PRs 1–4 applied
    $ bazelisk test //...
    //:app_targets_test                                             (cached) PASSED in 0.0s
    //:assertion_test                                               (cached) PASSED in 0.3s
    //:escript_test                                                 (cached) PASSED in 0.1s
    //:helped_test                                                  (cached) PASSED in 0.3s
    //:two_file_test                                                (cached) PASSED in 0.3s
    //app_deps:app_file_lists_deps_test                             (cached) PASSED in 0.0s
    //tree:elixirc_with_tree_test                                   (cached) PASSED in 0.0s
    //tree:ex_unit_with_tree_test                                   (cached) PASSED in 0.0s
    //:app_names_test                                                        PASSED in 0.0s
    //tree:elixirc_runs_from_tree_test                                       PASSED in 0.0s
    Executed 2 out of 10 tests: 10 tests pass.
    exit=0
    $ grep -E "Result: 2 passed|2 tests, 0 failures" bazel-testlogs/two_file_test/test.log
    Result: 2 passed
    exit=0
    $ bazelisk test //:failing_test
    exit=3 (expected non-zero)
    $ bazelisk test //:broken_test
    exit=3 (expected non-zero)
    $ bazelisk test //:empty_test
    exit=3 (expected non-zero)
    $ if bazelisk build //:incompatible_app --color=no --curses=no >/dev/null 2>&1; then echo "incompatible_app built (bad)"; else echo "//:incompatible_app failed as expected"; fi
    //:incompatible_app failed as expected
    exit=0
    $ for app in unused undef io_warn; do if bazelisk build //warnings:$app --color=no --curses=no 2> w_$app.log; then echo "//warnings:$app built (bad)"; else grep -c "Compilation failed due to warnings while using the --warnings-as-errors option" w_$app.log; fi; done; rm -f w_*.log
    1
    1
    1
    exit=0
    

    Coverage of hermetic_elixir.patch

    Every hunk is covered.

    Patch hunk PR
    elixir_app.bzl name, target_compatible_with, testonly, tags 1
    elixir_app.bzl deps = deps on app_file 2
    private/elixir_bytecode.bzl warnings-as-errors, drop set -x 3
    private/elixir_build.bzl tree, private/elixir_toolchain.bzl 4

    Intentional differences from the previous patch:

    • PR 1: the default name erlang_app keeps the old target names and output dirs. The old patch renamed them for every user. Non-default names are unchanged.
    • PR 3:
      • Runs bin/elixirc instead of elixir +elixirc.
      • Matches only lines that start with optional whitespace and warning:.
      • Skips the launcher's latin1 encoding warning.
      • Uses one awk call instead of a grep pipe.
    • PR 4:
      • TREE_ERLANG_HOME is loaded from rules_erlang instead of copying the RULES_ERLANG_HOME string.
      • erlang_dirs returns that constant.
      • The tree runfiles always include version_file.
      • ElixirInfo fields are documented.
      • The constructors in this repo pass tree = None.

    rules_dx side

    #4091 replaces hermetic_elixir.patch with the combined diff of these four PRs, without their tests. It is stacked on #4072, because PR 4 loads TREE_ERLANG_HOME, which #4072 adds to the rules_erlang patch.

    After each upstream merge:

    • Bump RULES_ELIXIR_COMMIT and RULES_ELIXIR_SHA256.
    • Delete that PR's hunks without editing them.
    • Run //elixir/tests/....

    When all four have merged, delete patches/rules_elixir/ and the patches attribute. When rules_dx pins a released Elixir that contains de015a25d4, PR 3's check is a no-op and can be dropped (or upstream can remove it).

  3. TrapsterDK commented on Oct 28, 2026

    @TrapsterDK
    CollaboratorAuthor

    Triage: blocked. Nothing is implementable now. The remaining steps wait on the maintainer submitting and landing four upstream bazelverse/rules_elixir PRs, which the issue forbids the worker from opening, and on rules_erlang PR 4 (#3743, still OPEN) for the tree PR. Not already fixed, since upstream has no equivalent change. Post-landing cleanup only: bump pins in elixir/toolchain/pins.bzl:15-19, delete landed hunks from hermetic_elixir.patch, then remove patches/rules_elixir/ and the repos.bzl patch label when empty. The warnings-as-errors hunk stays needed until Elixir with de015a25d4 is pinned. Read-only; no changes made.

  4. added
    blockedWaiting on upstream or a design decision
    model:opusAgent lane: needs planning or open design, Opus
    on Oct 28, 2026
  5. TrapsterDK commented on Nov 4, 2026

    @TrapsterDK
    CollaboratorAuthor

    Plan (agent): size S (after landing)
    Decision: keep blocked on one owner step. The four upstream PRs are drafted (comment of 2026-10-27) and hermetic_elixir.patch already matches them (514fb11, #4091). The only thing left is submitting them. Rejected: an agent opens the PRs (the issue body forbids it, the owner submits personally); splitting the local patch into four files now (churn, cleanup is the same one regen per landing); dropping the patch (consumers lose the fixes).
    Cause: nothing landed upstream. bazelverse/rules_elixir main is still 8e13ada (= RULES_ELIXIR_COMMIT, elixir/toolchain/pins.bzl:19) and its only PR is the release PR #1. The 2026-10-27 diffs still apply.
    Change (per PR, after it merges):

    1. elixir/toolchain/pins.bzl: bump RULES_ELIXIR_COMMIT, RULES_ELIXIR_SHA256.
    2. patches/rules_elixir/hermetic_elixir.patch: drop the landed hunks. When empty, delete it, its exports_files entry in patches/rules_elixir/BUILD.bazel and its label in elixir/toolchain/repos.bzl:180. toolchain_type_alias.patch is rules_dx only and stays.
    3. The warnings-as-errors hunk stays until upstream takes PR 3 or rules_dx pins an Elixir with de015a25d4.
      Tests: tools/dx test //elixir/...
      Host: none (the pin bump runs on the macos_arm64 cell in CI)
      Upstream: PR 2, PR 1, PR 3 to bazelverse/rules_elixir now. PR 4 after the rules_erlang tree PR (rules_erlang: split hermetic_otp.patch into four upstream PRs to bazelverse/rules_erlang #3743) lands.

    Question for the owner: will you submit PR 2, PR 1 and PR 3 from the 2026-10-27 comment to bazelverse/rules_elixir now and paste the PR URLs here? Recommended: yes. PR 4 waits on #3743. When a PR merges, an agent does the pin bump and patch trim above.

  6. TrapsterDK commented on Nov 4, 2026

    @TrapsterDK
    CollaboratorAuthor

    Owner decision: yes. Agents submit the drafted upstream PRs from this account (each ends with 'Assisted by Claude.'), then split our patch so each upstream merge is a pin bump plus deleting one patch file.

  7. removed
    blockedWaiting on upstream or a design decision
    on Nov 4, 2026
  8. TrapsterDK commented on Nov 4, 2026

    @TrapsterDK
    CollaboratorAuthor

    Owner: before submitting each upstream PR, take extra care that it fits upstream: read their CONTRIBUTING and recent merged PRs, search for existing issues/PRs on the same thing, match their style and commit format, keep the diff minimal and focused, add a regression test in their test layout, and run their tests locally. Get a review pass before opening. Open the upstream issue first if they expect one.

  9. TrapsterDK commented on Nov 4, 2026

    @TrapsterDK
    CollaboratorAuthor

    Opened upstream: bazelverse/rules_elixir#2 (deps to app_file), #3 (name/target_compatible_with/testonly/tags), #4 (warnings-as-errors). Each passes test/ //... plus CI negative checks locally on OTP 29.0 / Elixir 1.20.3. The tree PR waits on rules_erlang#18. Upstream issues are disabled. rules_dx patch split: #6702 (not merged). Part of #3744 remains: relocatable_tree PR and post-merge pin bumps.

  10. TrapsterDK commented on Nov 5, 2026

    @TrapsterDK
    CollaboratorAuthor

    Blocked: upstream bazelverse/rules_elixir PRs #2 (deps), #3 (name), #4 (warnings-as-errors) are all still OPEN, so no pin bump or patch deletion can land. Revisit after they merge.

  11. github-actions commented on Nov 5, 2026

    @github-actions

    Unblocked: #2, #3, #4 closed

  12. TrapsterDK commented on Nov 5, 2026

    @TrapsterDK
    CollaboratorAuthor

    Still blocked. The 2026-11-05 unblock was a false positive: the blocked-issues job read bazelverse/rules_elixir#2, #3, #4 as local issues #2, #3, #4 (old, closed). Upstream PRs #2, #3, #4 are still open, rules_elixir main is still 8e13ada, and rules_erlang#18 is open. Restored the blocked label and rewrote the Blocked on line with full URLs. Parser bug tracked in #7581.

  13. added
    blockedWaiting on upstream or a design decision
    and removed
    blockedWaiting on upstream or a design decision
    on Nov 6, 2026
  14. TrapsterDK commented on Nov 6, 2026

    @TrapsterDK
    CollaboratorAuthor

    Still blocked as of 2026-11-06: bazelverse/rules_elixir#2, /pull/3 and /pull/4 are open with no reviews. rules_elixir main is still 8e13ada, which equals RULES_ELIXIR_COMMIT. bazelverse/rules_erlang#18 is open. No Elixir release has de015a25d4, so warnings_as_errors.patch stays. Restored the blocked label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Improve and finish existing featuresblockedWaiting on upstream or a design decisionmodel:opusAgent lane: needs planning or open design, Opus

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions