Changelog
August 14, 2026 ยท View on GitHub
1.3.0
Adds a third way to supply Elixir: an already-compiled distribution.
Added
-
prebuilt_elixir_from_http_archiveandprebuilt_elixir_from_hex_builds(bzlmod/extensions.bzl,repositories/elixir_config.bzl, newrepositories/BUILD_prebuilt.tpl, newelixir_prebuiltinprivate/elixir_build.bzl).The mirror of what rules_erlang 3.20.0 added for OTP, and for the same reason: neither existing path can consume a downloadable prebuilt.
external_elixir_from_pathneeds anelixir_homethat already exists on the machine running the action, andinternal_elixir_from_http_archivealways feeds its URL toelixir_build, which runsmake.elixir_buildextracts a source archive, runsmake, and copies the resultingbin/andlib/into the release directory. A precompiled distribution is those two directories, so staging one is extraction plus the same validation step -- runiex --versionand record what it reports.elixir_config_ext.prebuilt_elixir_from_hex_builds( name = "elixir_1_19_4", version = "1.19.4", otp_major = "28", sha256 = "8fd7b5705b756c0e1ec71f9e8281b4b75801b9564f0205b5035319e8505ad2b4", )Unlike OTP, no relocation step is needed. An OTP installation bakes ROOTDIR into its start scripts and ships an
Installscript to repoint it; Elixir's launchers resolve their own root relative toargv[0], so a precompiled distribution works from wherever it is unpacked.Also unlike OTP, one archive serves every platform. Elixir compiles to BEAM bytecode, which is architecture-independent -- the published archive contains no native objects -- which is why
prebuilt_elixir_from_hex_buildstakes anotp_majorrather than anarch/ospair, and why there is noexec_compatible_withattribute here.The rule accepts both zip and tar archives, because hex.pm publishes a zip while other distributions are tarballs, and GNU tar cannot read zip. It fails with an explicit message when
unzipis absent for a zip input, and when the extracted archive has nobin/andlib/-- the likely mistake being an Elixir source archive, whose failure would otherwise surface as a confusing missing-file error after extraction.As with the OTP change, a prebuilt installation registers under the existing
:elixir_internalconstraint and keeps the:elixir_buildtarget name, so moving a repository between the source and prebuilt paths is aMODULE.bazeledit and nothing else.
1.2.0
First release from https://github.com/bazelverse/rules_elixir.
Continues from the v1.1.0 tag of the unmaintained upstream, carrying the
fixes that were being kept downstream as a vendored copy. Requires
rules_erlang 3.18.0.
Every change is a separate commit, so git diff v1.1.0..v1.2.0 is exactly this
list and nothing else.
Fixed
-
Skip system Elixir (
repositories/elixir_config.bzl). HonourRULES_ELIXIR_SKIP_SYSTEM=1, so a build can use a hermetic toolchain instead of probing the host for an Elixir install. -
Portable tarball extraction (
private/elixir_build.bzl). The same GNU-tar assumption fixed in rules_erlang 3.18.0.--transformdoes not exist in the bsdtar that macOS ships, so every Elixir target failed on macOS before compiling anything. -
Allow an empty
LICENSEglob (elixir_app.bzl).elixir_apphard-failed on any package without aLICENSEfile. -
Compile-time data (
elixir_app.bzl,private/elixir_bytecode.bzl).elixir_bytecodecould not declare compile-time file inputs, so a package reading a file during compilation, or declaring@external_resource, failed in the sandbox. -
ExUnit test headers (
private/ex_unit_test.bzl).ex_unit_teststaged its dependencies without theirinclude/directories, so a test usingRecord.extract(from_lib: ...)died before running a case. Compilation rules stage headers; tests now do too. -
ExUnit workspace layout (
private/ex_unit_test.bzl).ex_unit_testflattenedsrcsanddataby stripping the package prefix, which breaks any test resolving a repository-relative path off__DIR__. -
Stage test inputs into
TEST_TMPDIR(private/ex_unit_test.bzl).ex_unit_testcopied everysrcsanddatafile intoTEST_UNDECLARED_OUTPUTS_DIRand ran there. Bazel treats that directory as artifacts the test produced, so it stats and mime-types every entry to build the manifest, then uploads them. Each target shipped a few thousand of its own inputs to the CAS per run. On a remote executor carrying nofile(1)it also emitted roughly 2,150 "command not found" lines per test. Nothing was ever collected from there on purpose. -
Migrate off the deprecated Windows condition (
ex_unit_test.bzl).@bazel_tools//src/conditions:host_windowsis deprecated and warned on everyex_unit_testtarget; it is now the@platforms//os:windowsconstraint. This is also a semantic correction. The old key matched the host, while the flag decides whether the generated runner is a batch file or a shell script, a property of the platform the test executes on. For a test rule that is the target platform.The key is a
Label, not a bare string. Aselect()key written as a string resolves against the repo mapping of the package that instantiates the macro, so a string would have obliged every consumer to addbazel_dep(name = "platforms")of their own and failed with "No repository visible as@platforms" if they did not. The question never arose while the key was@bazel_tools//..., becausebazel_toolsis an implicit dependency of every module and is visible from everywhere. -
ExUnit result detection under Elixir 1.20 (
private/ex_unit_test.bzl). The runner asserted on the summary text, matching0 failureand[0-9] test. Elixir 1.20 replacedN tests, M failureswithResult: N passed, so under it every passing suite failed its own assertion.Failure detection now rests on the exit code, which the
set -eo pipefailat the top of the runner already carried through thetee, and which does not change between releases.One assertion on the summary text remains, because there is exactly one thing the exit code cannot express: a suite that executed no tests reports success, which would let a target whose sources stopped matching any test pass forever. It fires only when nothing ran and nothing was excluded. A target that filters by tag and excludes everything it has is doing what it was asked to, and is a normal way to shard a suite, so an exclusion count means the summary is honest and the target passes. Both spellings are read while the supported window spans Elixir 1.19 and 1.20.
This is the one behavioural difference from 1.1.0. A suite that defines no tests at all, and excludes none, passed before and now fails. That is the case
//:empty_testhas always asserted should fail.
Added
-
mix_archive_build(mix_archive_build.bzl,private/mix_archive_build.bzl): build a.ezMix archive. The rules could already compile an Elixir app but could not produce an archive, andmix archive.installis the only way to give Mix something it needs before it can resolve a project. Adapted from rabbitmq-server'sbazel/elixir/mix_archive_build.bzl(MPL-2.0), the same lineage as this ruleset. -
hexmodule extension (bzlmod/extensions.bzl): builds Hex itself from source as a Mix archive, exported as@hex//:archive.Every consumer needs this and cannot discover it. Mix aborts with "Could not find an SCM for dependency" on any dependency entry in a
mix.exsunless Hex is installed as an archive, including in a build that supplies all dependencies from Bazel and passes--no-deps-check. Hex has no dependencies of its own, so it bootstraps with an empty dependency graph. The version defaults toDEFAULT_HEX_VERSIONand is overridable with thefrom_github_releasetag, mirroringinternal_elixir_from_github_release.
Removed
- Bazel 7 support, declared as
bazel_compatibility = [">=8.0.0"]. This followsrules_erlang3.18.0, which requires Bazel 8 and which this release requires in turn, so 7 was already unreachable in practice.
Changed
-
Dependencies moved to current stable.
from to rules_erlang 3.16.0 3.18.0 bazel_skylib 1.7.1 1.9.2 platforms - 1.1.0 (new) platformsis new becauseex_unit_testselects on@platforms//os:windows; see the Windows condition entry above for why the dependency belongs here rather than in every consumer. -
sh_testloads from@rules_shell(test/BUILD.bazel,test/MODULE.bazel). It stopped being a native rule in Bazel 8, so the test module gained both the load statement and arules_shelldependency. -
@platforms//host:hostreplaces@local_config_platform//:host(examples/internal-elixir). Bazel 8 removed the autoconfiguredlocal_config_platformrepository. The example's platforms also register themselves as execution platforms, which Bazel 9 requires of any target platform that has to run tests. -
The
internal-elixirexample builds OTP 29.0.5 and Elixir 1.20.3, up from OTP 26.2.5 and Elixir 1.16.1. It is the only from-source coverage there is, and it was pinned two OTP majors behind the supported window. -
CI covers the latest two Elixir minors against the latest two OTP majors, currently 1.20 on OTP 29 and 1.19 on OTP 28. Elixir supports a moving window of OTP majors, so the matrix tests pairs rather than crossing the two lists. The Bazel matrix lives in the registry presubmit, which covers 8.x and 9.x.
-
The registry presubmit installs Elixir, which it never did. It built
@rules_elixir//...with only Erlang on the machine, leaving the external Elixir toolchain nothing to resolve. It also moves from theplatforms:mapping to thetasks:schema, and activates the kerl build so the Elixir archive finds an ERTS to run on.The structure here comes from rabbitmq/rules_elixir#7, merged from upstream's final state; the versions are this project's.
-
Fix the registry source template's archive name (
.bcr/source.template.json). It asked forrules_elixir-{TAG}.tar.gz, but the release workflow uploadsrules_elixir-{VERSION}.tar.gz, and this repository tags with a leadingv. The two are only equal when the tag carries no prefix, so the published URL would have beenrules_elixir-v1.2.0.tar.gzand returned a 404. -
Publishing targets the Bazel Central Registry. The workflow that pushed to
rabbitmq/bazel-central-registry@erlang-packages, a private registry fork this project cannot write to, is replaced bybazel-contrib/publish-to-bcr. The registry entry for that fork is also dropped from every.bazelrc; it servedrules_erlang3.15.x and nothing reads it now.
Compatibility
For a bzlmod consumer, 1.2.0 is a drop-in replacement for 1.1.0: no rule, macro,
provider or attribute changed. The rules_erlang dependency moves from 3.16.0 to
3.18.0, which is itself a drop-in replacement.
One behaviour differs, described under the ExUnit entry above: an ex_unit_test
that runs no tests and excludes none now fails rather than passes. A target that
excludes everything by tag is unaffected.
Requires Bazel 8 or newer. Tested on Bazel 8.7.0 and 9.2.0.
Neither module is on the Bazel Central Registry yet, so a consumer needs an
override for rules_elixir and one for rules_erlang. Overrides only take
effect in the root module, which is why both are needed even though only one is a
direct dependency. See the Installation section of the README.