Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,132 @@
# Changelog

## 3.20.0

Adds a third way to supply Erlang/OTP: an already-compiled binary release.

### Added

- **`prebuilt_erlang_from_http_archive` and `prebuilt_erlang_from_hex_builds`**
(`bzlmod/extensions.bzl`, `repositories/erlang_config.bzl`,
new `repositories/BUILD_prebuilt.tpl`, new `erlang_prebuilt` in
`private/erlang_build.bzl`).

Until now there were two ways to provide OTP, and neither covers a downloadable
prebuilt:

- `external_erlang_from_path` takes an absolute `erlang_home` that must already
exist on whatever machine runs the action. That is a host dependency, which is
exactly what a hermetic build is trying not to have, and it cannot be satisfied
on a remote executor without baking OTP into the executor image.
- `internal_erlang_from_http_archive` accepts any URL, but always feeds it to
`erlang_build`, which runs `./configure && make`. Pointing it at a binary
release does not skip the build; it fails, because there is nothing to
configure.

So a consumer who wanted a prebuilt OTP had to compile one anyway. The new tag
classes fetch an OTP *binary release* and stage it, with no configure or make
step.

Measured on the same target, same machine, same forced re-execution
(`--action_env=CACHEBUST=...` to defeat both caches), OTP 28.1 on linux/amd64:

| | |
|---|---|
| `internal_erlang_from_github_release` | still compiling after 5 min, abandoned |
| `prebuilt_erlang_from_hex_builds` | ~12s wall, staging action ~3-8s |

The download is 71 MB for a 171 MB installed tree.

`prebuilt_erlang_from_hex_builds` is a thin convenience over `builds.hex.pm`,
which publishes a release per `(arch, os)` and lists each archive's sha256 in
its `builds.txt`. The checksum a consumer pins is therefore one upstream already
publishes, rather than one produced by mirroring the archive somewhere to compute
it.

```python
erlang_config_ext.prebuilt_erlang_from_hex_builds(
name = "otp_28_1",
version = "28.1",
arch = "amd64", # or "arm64"
os = "ubuntu-24.04", # must match the runtime's libc
sha256 = "6f7a95250a83f999909cf64dc375fd08809eb057f61b24eecdf2b3b44fe621ab",
)
```

**On relocation.** An OTP installation is not relocatable: `ROOTDIR` is written
into the generated start scripts at install time. A binary release ships an
`Install` script precisely because of this, and `Install -cross <TARGET>` treats
the current directory as the staging location while writing `TARGET` into the
scripts. That is the split this rule needs, since staging happens in a sandbox
and the install path is fixed and absolute. The rule fails with an explicit
message if the archive has no executable `Install` at its root, because the
likely mistake is passing an `otp_src` tarball, and the error from doing that
otherwise surfaces deep inside the action.

**On toolchain registration.** A prebuilt installation registers under the
existing `:erlang_internal` constraint rather than a new one. It is hermetic in
the same sense an internally built one is -- fetched and staged by the build,
not discovered on the host -- and reusing the constraint keeps the generated
target names identical. Moving a repository between the source and prebuilt
paths is therefore a `MODULE.bazel` edit and nothing else: existing
`toolchain_resolution_overrides` and `//<name>:toolchain_major` references keep
resolving.

**On multiple architectures.** Both tag classes take `exec_compatible_with`,
whose values are appended to the generated toolchain's `exec_compatible_with`.
A repository can register one prebuilt per execution architecture and let
toolchain resolution choose, which is what makes a prebuilt OTP usable for
something other than the machine that happens to be building.

## 3.19.0

A single fix, for a bug 3.18.0 introduced.

### Fixed

- **`@platforms` no longer has to be visible from the consumer**
(`ct.bzl`, `eunit.bzl`, `eunit2.bzl`, `xref.bzl`, `xref2.bzl`, `dialyze.bzl`,
`shell.bzl`, new `private/is_windows.bzl`).

Nine `select()` keys named `@platforms//os:windows` as a bare string. A
`select()` key written as a string resolves against the repo mapping of the
package that *instantiates* it, and every one of these lives in a macro a
consumer calls from its own `BUILD.bazel`. So the key resolved in the
consumer's module, and any consumer that had not declared
`bazel_dep(name = "platforms")` of its own failed analysis:

```
ERROR: no such package '@@[unknown repo 'platforms' requested from @@]//os':
No repository visible as '@platforms' from main repository
ERROR: errors encountered resolving select() keys for //:xref
```

Calling `xref`, `dialyze`, `eunit`, `ct_suite` or `shell` was enough to trigger
it. The keys are now `Label`s, which resolve against this module's repo
mapping, and `platforms` is declared here where it belongs.

This was introduced in 3.18.0 by the migration away from
`@bazel_tools//src/conditions:host_windows`. That question could not arise for
the old key, because `bazel_tools` is an implicit dependency of every module
and is visible everywhere. It survived release because the only consumers on
hand both happened to depend on `platforms` already, this repository's own
`test` module among them, so nothing in CI could see it. The regression test
for it is a consumer that declares nothing but `rules_erlang`.

The condition now lives in one place. Nine copies of a three-line `select()` is
the shape of a fix that lands in some of them and not the rest, which is what
happened to the `allow_empty` glob fix in 3.18.0.

### Compatibility

Drop-in for 3.18.0 and 3.16.0. No rule, macro, provider or attribute changed.

A consumer that declared `bazel_dep(name = "platforms")` only to satisfy 3.18.0
can drop it, though keeping it is harmless. One that uses `@platforms` in its own
`BUILD` files still needs it, for its own reasons.

Requires Bazel 8 or newer. Tested on Bazel 8.7.0 and 9.2.0.

## 3.18.0

First release from <https://github.com/bazelverse/rules_erlang>.
Expand Down
13 changes: 10 additions & 3 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
module(
name = "rules_erlang",
version = "3.18.0",
version = "3.20.0",
bazel_compatibility = [">=8.0.0"],
)

# ct_suite, eunit, xref, dialyze and shell all select on @platforms//os:windows.
# See //private:is_windows.bzl for why the dependency belongs here rather than in
# every consumer's MODULE.bazel.
bazel_dep(
name = "platforms",
version = "1.1.0",
)
bazel_dep(
name = "rules_go",
version = "0.62.0",
Expand Down Expand Up @@ -38,16 +45,16 @@ erlang_package = use_extension(
)
erlang_package.hex_package(
name = "thoas_rules_erlang",
pkg = "thoas",
build_file = "@rules_erlang//:BUILD.thoas",
pkg = "thoas",
sha256 = "fc763185b932ecb32a554fb735ee03c3b6b1b31366077a2427d2a97f3bd26735",
version = "1.0.0",
)
use_repo(
erlang_package,
"getopt_src",
"xref_runner_src",
"thoas_rules_erlang",
"xref_runner_src",
)

erlang_config_extension = use_extension(
Expand Down
50 changes: 17 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Requires bzlmod; WORKSPACE support was removed in 3.18.0. GitHub CI builds with
the Bazel pinned in `.bazelversion`, and the Bazel Central Registry presubmit
covers 8.x and 9.x. Bazel 7 is not supported.

Nothing else is required of a consumer. In particular you do not need to declare
`bazel_dep(name = "platforms")` to use the test macros; 3.18.0 did, which
[3.19.0 fixes](./CHANGELOG.md).

## Status

This repository continues the 3.x line of
Expand All @@ -21,54 +25,34 @@ archived. `3.16.0` was its last release; its `main` branch then stopped at an
unreleased `4.0.0-beta.1`, which never reached the registry and is not continued
here.

**For a bzlmod consumer, 3.18.0 is a drop-in replacement for 3.16.0.** No rule,
macro, provider or attribute changed; every commit between the two tags is one
**For a bzlmod consumer, 3.19.0 is a drop-in replacement for 3.16.0.** No rule,
macro, provider or attribute changed; every commit between the tags is one
documented fix. The one removal is the WORKSPACE entry point, which no supported
Bazel can load anyway. See [CHANGELOG.md](./CHANGELOG.md).

## Installation

> **Git pre-release only.** Pick one of the two override methods below. Once the
> Bazel Central Registry entry lands, delete the override and keep the
> `bazel_dep`; nothing else changes.

### Pinned to a release tag

```starlark
bazel_dep(name = "rules_erlang", version = "3.18.0")

archive_override(
module_name = "rules_erlang",
urls = ["https://github.com/bazelverse/rules_erlang/archive/refs/tags/3.18.0.tar.gz"],
strip_prefix = "rules_erlang-3.18.0",
integrity = "sha256-...",
)
```

To get the `integrity` value, run the build once with the attribute omitted.
Bazel fetches the archive and prints a warning containing the hash it computed;
paste that in. A *wrong* value fails the build and names the expected one, so
either route gets you there. Do not leave it out permanently: without it the
archive is re-fetched unverified.

### Pinned to a commit
> **Git pre-release only.** The Bazel Central Registry entry is pending an
> ownership transfer, so `bazel_dep` alone will not resolve and you need an
> override. Once the entry lands, delete the override and keep the `bazel_dep`;
> nothing else changes.

```starlark
bazel_dep(name = "rules_erlang", version = "3.18.0")
bazel_dep(name = "rules_erlang", version = "3.19.0")

git_override(
module_name = "rules_erlang",
remote = "https://github.com/bazelverse/rules_erlang.git",
commit = "0000000000000000000000000000000000000000",
commit = "0000000000000000000000000000000000000000", # 3.19.0
)
```

`commit` takes a full SHA. Use this to track work that has no tag yet. Prefer
the tag form for anything you ship: it is a fixed archive plus a checksum,
rather than a repository that has to stay reachable and re-clone.
`commit` takes a full SHA, not a tag. Pin the commit a release tag points at
rather than the tag itself: a SHA cannot be moved, and it is the same thing the
registry will hand you later. `git rev-list -n1 3.19.0` prints it.

The `version` in `bazel_dep` is still required either way. It is what the module
reports to the rest of the graph; the override decides what is actually fetched.
The `version` in `bazel_dep` is still required. It is what the module reports to
the rest of the graph; the override decides what is actually fetched.

**Overrides only take effect in the root module.** If you depend on
`rules_erlang` indirectly, through
Expand Down
Loading
Loading