From 7e2f1a8086b458fd6b2ca1fa78c15b498c8f1295 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 16 Jun 2026 22:54:12 +0000 Subject: [PATCH 1/8] add elixir_prebuilt rule (stage a precompiled Elixir tree) --- defs.bzl | 4 ++ private/elixir_build.bzl | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/defs.bzl b/defs.bzl index 7b11e86..aa6429c 100644 --- a/defs.bzl +++ b/defs.bzl @@ -10,6 +10,7 @@ load( "//private:elixir_build.bzl", _elixir_build = "elixir_build", _elixir_external = "elixir_external", + _elixir_prebuilt = "elixir_prebuilt", ) load("//private:elixir_release.bzl", _elixir_release = "elixir_release") load("//private:elixir_release_bundle.bzl", _elixir_release_bundle = "elixir_release_bundle") @@ -135,6 +136,9 @@ def ex_unit_test(**kwargs): def elixir_external(**kwargs): return _elixir_external(**kwargs) +def elixir_prebuilt(**kwargs): + return _elixir_prebuilt(**kwargs) + def elixir_toolchain(**kwargs): return _elixir_toolchain(**kwargs) diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl index d3ac6b5..86d5293 100644 --- a/private/elixir_build.bzl +++ b/private/elixir_build.bzl @@ -180,3 +180,98 @@ elixir_external = rule( ), }, ) + +def _archive_root(files): + """Longest common directory prefix of all paths -- the extracted archive root. + + Stripping it makes a prebuilt Elixir's bin/ and lib/ land at the release_dir + root. Robust regardless of glob ordering (unlike using the first file's dir). + """ + segs = files[0].path.split("/") + for f in files[1:]: + other = f.path.split("/") + n = 0 + for i in range(min(len(segs), len(other))): + if segs[i] != other[i]: + break + n += 1 + segs = segs[:n] + return "/".join(segs) + +def _elixir_prebuilt_impl(ctx): + otp_info = ctx.attr.otp[OtpInfo] + release_dir = ctx.actions.declare_directory("elixir_release") + version_file = ctx.actions.declare_file("elixir_version") + + runfiles = otp_runfiles(ctx, otp_info) + + # Stage the prebuilt Elixir release (bin/ + lib/) into a relocatable tree + # artifact -- like elixir_build, but extract instead of `make`. + ctx.actions.run_shell( + inputs = ctx.files.srcs, + outputs = [release_dir], + command = """set -euo pipefail + +ABS_RELEASE_DIR=$PWD/{release_path} +for src in {source_files}; do + rel="${{src#{archive_root}/}}" + dest="$ABS_RELEASE_DIR/$rel" + mkdir -p "$(dirname "$dest")" + cp "$src" "$dest" +done +""".format( + release_path = release_dir.path, + source_files = " ".join([f.path for f in ctx.files.srcs]), + archive_root = _archive_root(ctx.files.srcs) if ctx.files.srcs else ".", + ), + mnemonic = "ELIXIRPREBUILT", + progress_message = "Staging prebuilt Elixir", + ) + + ctx.actions.run_shell( + inputs = depset( + direct = [release_dir], + transitive = [runfiles.files], + ), + outputs = [version_file], + command = """set -euo pipefail + +{erl_rootdir_setup} + +export PATH="{erlang_home}"/bin:${{PATH}} + +"{elixir_home}"/bin/iex --version > {version_file} +""".format( + erl_rootdir_setup = otp_rootdir_setup(otp_info), + erlang_home = erlang_home(otp_info), + elixir_home = release_dir.path, + version_file = version_file.path, + ), + mnemonic = "ELIXIRVERSION", + progress_message = "Validating prebuilt elixir", + ) + + return [ + DefaultInfo(files = depset([release_dir, version_file])), + otp_info, + ElixirInfo( + release_dir = release_dir, + elixir_home = None, + version_file = version_file, + ), + ] + +elixir_prebuilt = rule( + implementation = _elixir_prebuilt_impl, + attrs = { + "srcs": attr.label_list( + allow_files = True, + doc = "Extracted prebuilt Elixir release tree (bin/, lib/).", + ), + "otp": attr.label( + mandatory = True, + providers = [OtpInfo], + doc = "An OTP target providing the installation to validate against.", + ), + }, +) From ac4f5e0c5ac8f7e833e3313b9862f65bb348b664 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 16 Jun 2026 22:55:12 +0000 Subject: [PATCH 2/8] elixir_config: add internal_elixir_from_prebuilt tag class --- bzlmod/extensions.bzl | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/bzlmod/extensions.bzl b/bzlmod/extensions.bzl index ab8ecb5..1b150a0 100644 --- a/bzlmod/extensions.bzl +++ b/bzlmod/extensions.bzl @@ -109,6 +109,40 @@ elixir_build( otp = "@@{erlang_config_repo}//{otp}:otp-{otp}", visibility = ["//visibility:public"], ) +""".format( + erlang_config_repo = erlang_config_repo, + otp = elixir.otp, + ), + ) + + for elixir in mod.tags.internal_elixir_from_prebuilt: + types[elixir.name] = INSTALLATION_TYPE_INTERNAL + versions[elixir.name] = elixir.version + urls[elixir.name] = elixir.url + strip_prefixs[elixir.name] = elixir.strip_prefix + sha256s[elixir.name] = elixir.sha256 + target_compatible_withs[elixir.name] = [str(l) for l in elixir.target_compatible_with] + exec_compatible_withs[elixir.name] = [str(l) for l in elixir.exec_compatible_with] + if elixir.erlang_version: + erlang_versions[elixir.name] = elixir.erlang_version + + # Fetch a *precompiled* Elixir release and stage it (no source build) + # via elixir_prebuilt. The target stays named "elixir_build" so the + # generated internal toolchain BUILD (BUILD_internal.tpl) resolves. + http_archive( + name = "elixir_source_{}".format(elixir.name), + url = elixir.url, + sha256 = elixir.sha256, + strip_prefix = elixir.strip_prefix, + build_file_content = """ +load("@rules_elixir//private:elixir_build.bzl", "elixir_prebuilt") + +elixir_prebuilt( + name = "elixir_build", + srcs = glob(["**/*"]), + otp = "@@{erlang_config_repo}//{otp}:otp-{otp}", + visibility = ["//visibility:public"], +) """.format( erlang_config_repo = erlang_config_repo, otp = elixir.otp, @@ -179,12 +213,33 @@ internal_elixir_from_github_release = tag_class(attrs = { "target_compatible_with": attr.label_list(default = []), }) +internal_elixir_from_prebuilt = tag_class(attrs = { + "name": attr.string(), + "version": attr.string(), + "url": attr.string( + doc = "URL of a precompiled Elixir release archive (bin/ + lib/ at its root).", + ), + "strip_prefix": attr.string(), + "sha256": attr.string(), + "otp": attr.string( + mandatory = True, + doc = "Name of an erlang_config installation to validate the prebuilt Elixir against.", + ), + "erlang_version": attr.string( + doc = "OTP version string (e.g. '26.3') for combined platform constraints. " + + "Must match the version of the OTP installation named by the 'otp' attr.", + ), + "exec_compatible_with": attr.label_list(default = []), + "target_compatible_with": attr.label_list(default = []), +}) + elixir_config = module_extension( implementation = _elixir_config, tag_classes = { "external_elixir_from_path": external_elixir_from_path, "internal_elixir_from_http_archive": internal_elixir_from_http_archive, "internal_elixir_from_github_release": internal_elixir_from_github_release, + "internal_elixir_from_prebuilt": internal_elixir_from_prebuilt, }, ) From 1514480f93fbe3bdb6028a0ba14df93eda7f7c5a Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 7 Jul 2026 18:43:24 +0000 Subject: [PATCH 3/8] do not break elixir runtime into individual files --- private/elixir_build.bzl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl index 86d5293..6a8704f 100644 --- a/private/elixir_build.bzl +++ b/private/elixir_build.bzl @@ -213,15 +213,11 @@ def _elixir_prebuilt_impl(ctx): command = """set -euo pipefail ABS_RELEASE_DIR=$PWD/{release_path} -for src in {source_files}; do - rel="${{src#{archive_root}/}}" - dest="$ABS_RELEASE_DIR/$rel" - mkdir -p "$(dirname "$dest")" - cp "$src" "$dest" -done +# cp -rp, not a tar -h pipe (cf. erlang_release_archive): compiled Elixir has no +# symlinks that need resolving. archive_root holds bin/ + lib/; /. copies contents. +cp -rp "{archive_root}/." "$ABS_RELEASE_DIR/" """.format( release_path = release_dir.path, - source_files = " ".join([f.path for f in ctx.files.srcs]), archive_root = _archive_root(ctx.files.srcs) if ctx.files.srcs else ".", ), mnemonic = "ELIXIRPREBUILT", From 9a40e9cf33fa0ed57c88e32d781e3b2a46eb2bd7 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 7 Jul 2026 20:50:51 +0000 Subject: [PATCH 4/8] better handling for single-file archives --- private/elixir_build.bzl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl index 6a8704f..433c7e7 100644 --- a/private/elixir_build.bzl +++ b/private/elixir_build.bzl @@ -187,7 +187,11 @@ def _archive_root(files): Stripping it makes a prebuilt Elixir's bin/ and lib/ land at the release_dir root. Robust regardless of glob ordering (unlike using the first file's dir). """ - segs = files[0].path.split("/") + # NOTE: Get the dirname of the first file to calculate the lowest common + # path prefix between all files. Chopping off the last element ensures we + # don't return the file itself, if we only have one file. + # We always provide `files.srcs` here, so we should never be called with a directory. + segs = files[0].path.split("/")[:-1] for f in files[1:]: other = f.path.split("/") n = 0 From aea46625f04ce06391e2926c3a7aa8e42a4e3c3d Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 7 Jul 2026 22:53:18 +0000 Subject: [PATCH 5/8] ensure we provide correct env vars to prebuilt runtimes --- test/escript_test.sh | 14 +++++++++++++- tools/elixir_vars.bzl | 16 ++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/test/escript_test.sh b/test/escript_test.sh index bf64d00..2b6445d 100755 --- a/test/escript_test.sh +++ b/test/escript_test.sh @@ -12,7 +12,19 @@ if [[ -n "${ERLANG_RELEASE_DIR_SHORT_PATH:-}" ]]; then fi fi -PATH="$ELIXIR_HOME/bin:$ERLANG_HOME/bin:$PATH" +# Relocatable Elixir: ELIXIR_HOME is "", so resolve it from the staged tree +# (same short_path anchoring as ERL_ROOTDIR above). +if [[ -n "${ELIXIR_RELEASE_DIR_SHORT_PATH:-}" ]]; then + if [[ -n "${TEST_SRCDIR:-}" ]]; then + ELIXIR_HOME="$TEST_SRCDIR/$TEST_WORKSPACE/$ELIXIR_RELEASE_DIR_SHORT_PATH" + else + ELIXIR_HOME="$PWD/$ELIXIR_RELEASE_DIR_SHORT_PATH" + fi +fi + +# ERLANG_HOME is the literal "$ERL_ROOTDIR" for relocatable OTP (doesn't +# re-expand in a plain ref), so prefer the already-exported ERL_ROOTDIR. +PATH="$ELIXIR_HOME/bin:${ERL_ROOTDIR:-$ERLANG_HOME}/bin:$PATH" ./basic hello there | tee out.log diff --git a/tools/elixir_vars.bzl b/tools/elixir_vars.bzl index 10612e7..1e47a62 100644 --- a/tools/elixir_vars.bzl +++ b/tools/elixir_vars.bzl @@ -18,23 +18,31 @@ ELIXIR_VARS_ENV_MAP = { ELIXIR_VARS_ENV_MAP_INTERNAL = ELIXIR_VARS_ENV_MAP | { "ERLANG_RELEASE_DIR_PATH": "$(ERLANG_RELEASE_DIR_PATH)", "ERLANG_RELEASE_DIR_SHORT_PATH": "$(ERLANG_RELEASE_DIR_SHORT_PATH)", + "ELIXIR_RELEASE_DIR_PATH": "$(ELIXIR_RELEASE_DIR_PATH)", + "ELIXIR_RELEASE_DIR_SHORT_PATH": "$(ELIXIR_RELEASE_DIR_SHORT_PATH)", } def _impl(ctx): otpinfo = ctx.toolchains["//:toolchain_type"].otpinfo elixirinfo = ctx.toolchains["//:toolchain_type"].elixirinfo + # Always define every key any env map references; "" for the inapplicable + # half (external vs relocatable). None would fail TemplateVariableInfo, and + # "" keeps the maps' $(...) refs expandable regardless of internal-ness. vars = { "OTP_VERSION": otpinfo.version, "OTP_VERSION_FILE_PATH": otpinfo.version_file.path, "OTP_VERSION_FILE_SHORT_PATH": otpinfo.version_file.short_path, "ERLANG_HOME": erlang_home(otpinfo), - "ELIXIR_HOME": elixirinfo.elixir_home, + # External Elixir: absolute home. Internal/prebuilt: "" -- consumers + # resolve it from ELIXIR_RELEASE_DIR_* (tree artifact, no analysis-time path). + "ELIXIR_HOME": elixirinfo.elixir_home or "", "ELIXIR_VERSION_FILE_PATH": elixirinfo.version_file.path, "ELIXIR_VERSION_FILE_SHORT_PATH": elixirinfo.version_file.short_path, + "ERLANG_RELEASE_DIR_PATH": otpinfo.release_dir.path if otpinfo.release_dir else "", + "ERLANG_RELEASE_DIR_SHORT_PATH": otpinfo.release_dir.short_path if otpinfo.release_dir else "", + "ELIXIR_RELEASE_DIR_PATH": elixirinfo.release_dir.path if elixirinfo.release_dir else "", + "ELIXIR_RELEASE_DIR_SHORT_PATH": elixirinfo.release_dir.short_path if elixirinfo.release_dir else "", } - if otpinfo.release_dir != None: - vars["ERLANG_RELEASE_DIR_PATH"] = otpinfo.release_dir.path - vars["ERLANG_RELEASE_DIR_SHORT_PATH"] = otpinfo.release_dir.short_path return [ platform_common.TemplateVariableInfo(vars), From 5bd93b9a005a4770d90c6f1dee54be716a602ca3 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Tue, 7 Jul 2026 23:31:52 +0000 Subject: [PATCH 6/8] expose prebuilt elixir tarball --- bzlmod/extensions.bzl | 24 +++++++++++++++++++++-- defs.bzl | 10 ++++++++++ private/elixir_build.bzl | 41 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/bzlmod/extensions.bzl b/bzlmod/extensions.bzl index 1b150a0..4e176fd 100644 --- a/bzlmod/extensions.bzl +++ b/bzlmod/extensions.bzl @@ -62,7 +62,7 @@ def _elixir_config(ctx): sha256 = elixir.sha256, strip_prefix = elixir.strip_prefix, build_file_content = """ -load("@rules_elixir//private:elixir_build.bzl", "elixir_build") +load("@rules_elixir//private:elixir_build.bzl", "elixir_build", "elixir_prebuilt_tarball") elixir_build( name = "elixir_build", @@ -70,9 +70,19 @@ elixir_build( otp = "@@{erlang_config_repo}//{otp}:otp-{otp}", visibility = ["//visibility:public"], ) + +# Package the built runtime as a hostable tarball -- bazel build +# @elixir_source_{name}//:elixir_tarball -- for use with +# internal_elixir_from_prebuilt elsewhere, avoiding recompilation. +elixir_prebuilt_tarball( + name = "elixir_tarball", + elixir = ":elixir_build", + visibility = ["//visibility:public"], +) """.format( erlang_config_repo = erlang_config_repo, otp = elixir.otp, + name = elixir.name, ), ) @@ -101,7 +111,7 @@ elixir_build( sha256 = elixir.sha256, strip_prefix = strip_prefix, build_file_content = """ -load("@rules_elixir//private:elixir_build.bzl", "elixir_build") +load("@rules_elixir//private:elixir_build.bzl", "elixir_build", "elixir_prebuilt_tarball") elixir_build( name = "elixir_build", @@ -109,9 +119,19 @@ elixir_build( otp = "@@{erlang_config_repo}//{otp}:otp-{otp}", visibility = ["//visibility:public"], ) + +# Package the built runtime as a hostable tarball -- bazel build +# @elixir_source_{name}//:elixir_tarball -- for use with +# internal_elixir_from_prebuilt elsewhere, avoiding recompilation. +elixir_prebuilt_tarball( + name = "elixir_tarball", + elixir = ":elixir_build", + visibility = ["//visibility:public"], +) """.format( erlang_config_repo = erlang_config_repo, otp = elixir.otp, + name = elixir.name, ), ) diff --git a/defs.bzl b/defs.bzl index aa6429c..18fc99b 100644 --- a/defs.bzl +++ b/defs.bzl @@ -11,6 +11,7 @@ load( _elixir_build = "elixir_build", _elixir_external = "elixir_external", _elixir_prebuilt = "elixir_prebuilt", + _elixir_prebuilt_tarball = "elixir_prebuilt_tarball", ) load("//private:elixir_release.bzl", _elixir_release = "elixir_release") load("//private:elixir_release_bundle.bzl", _elixir_release_bundle = "elixir_release_bundle") @@ -139,6 +140,15 @@ def elixir_external(**kwargs): def elixir_prebuilt(**kwargs): return _elixir_prebuilt(**kwargs) +def elixir_prebuilt_tarball(**kwargs): + """Packages a built Elixir runtime (bin/ + lib/) into a hostable .tar.gz. + + Consume the result with elixir_config's internal_elixir_from_prebuilt tag, + which fetches it via http_archive and stages it through elixir_prebuilt -- + closing the produce/consume loop for pre-built Elixir runtimes. + """ + return _elixir_prebuilt_tarball(**kwargs) + def elixir_toolchain(**kwargs): return _elixir_toolchain(**kwargs) diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl index 433c7e7..323723f 100644 --- a/private/elixir_build.bzl +++ b/private/elixir_build.bzl @@ -275,3 +275,44 @@ elixir_prebuilt = rule( ), }, ) + +def _elixir_prebuilt_tarball_impl(ctx): + info = ctx.attr.elixir[ElixirInfo] + if info.release_dir == None: + fail("elixir_prebuilt_tarball requires a relocatable Elixir (release_dir set); " + + "external installs (elixir_home) cannot be packaged.") + + tarball = ctx.actions.declare_file(ctx.label.name + ".tar.gz") + + ctx.actions.run_shell( + inputs = [info.release_dir], + outputs = [tarball], + command = """set -euo pipefail + +# -h dereferences symlinks so the archive has none (RBE-robust, matches +# rules_erlang's erlang_build). -C . puts bin/ + lib/ at the tar root, +# which is what internal_elixir_from_prebuilt / elixir_prebuilt expect. +tar -czhf "$PWD/{out}" -C "{release_dir}" . +""".format( + out = tarball.path, + release_dir = info.release_dir.path, + ), + use_default_shell_env = True, + mnemonic = "ELIXIRTARBALL", + progress_message = "Packaging prebuilt Elixir tarball", + ) + + return [DefaultInfo(files = depset([tarball]))] + +elixir_prebuilt_tarball = rule( + implementation = _elixir_prebuilt_tarball_impl, + attrs = { + "elixir": attr.label( + mandatory = True, + providers = [ElixirInfo], + doc = "A target providing ElixirInfo (e.g. an elixir_build/elixir_prebuilt " + + "target such as @elixir_source_//:elixir_build). Its release_dir " + + "(bin/ + lib/) is packaged into a .tar.gz for internal_elixir_from_prebuilt.", + ), + }, +) From 5099f4168cd724c791a947639869c284918c9047 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Thu, 9 Jul 2026 18:38:08 +0000 Subject: [PATCH 7/8] dedupe elixir version action --- private/elixir_build.bzl | 102 ++++++++++++++------------------ private/elixir_source_build.bzl | 28 +++------ 2 files changed, 52 insertions(+), 78 deletions(-) diff --git a/private/elixir_build.bzl b/private/elixir_build.bzl index 323723f..d036489 100644 --- a/private/elixir_build.bzl +++ b/private/elixir_build.bzl @@ -26,6 +26,32 @@ ElixirInfo = provider( ], ) +def elixir_version_action(ctx, otp_info, elixir_home, version_file, inputs, mnemonic = "ELIXIRVERSION", progress_message = "Validating elixir"): + """Run `iex --version` to validate an Elixir install and capture its version. + + Shared by elixir_build / elixir_external / elixir_prebuilt / elixir_source_build: + the command is identical; callers vary only in inputs, elixir_home, and labels. + """ + ctx.actions.run_shell( + inputs = inputs, + outputs = [version_file], + command = """set -euo pipefail + +{erl_rootdir_setup} + +export PATH="{erlang_home}"/bin:${{PATH}} + +"{elixir_home}"/bin/iex --version > {version_file} +""".format( + erl_rootdir_setup = otp_rootdir_setup(otp_info), + erlang_home = erlang_home(otp_info), + elixir_home = elixir_home, + version_file = version_file.path, + ), + mnemonic = mnemonic, + progress_message = progress_message, + ) + def _elixir_build_impl(ctx): otp_info = ctx.attr.otp[OtpInfo] release_dir = ctx.actions.declare_directory("elixir_release") @@ -80,27 +106,12 @@ cp -r lib $ABS_RELEASE_DIR/ progress_message = "Building Elixir from source", ) - ctx.actions.run_shell( - inputs = depset( - direct = [release_dir], - transitive = [runfiles.files], - ), - outputs = [version_file], - command = """set -euo pipefail - -{erl_rootdir_setup} - -export PATH="{erlang_home}"/bin:${{PATH}} - -"{elixir_home}"/bin/iex --version > {version_file} -""".format( - erl_rootdir_setup = otp_rootdir_setup(otp_info), - erlang_home = erlang_home(otp_info), - elixir_home = release_dir.path, - version_file = version_file.path, - ), - mnemonic = "ELIXIRVERSION", - progress_message = "Validating elixir", + elixir_version_action( + ctx, + otp_info, + release_dir.path, + version_file, + depset(direct = [release_dir], transitive = [runfiles.files]), ) return [ @@ -136,22 +147,12 @@ def _elixir_external_impl(ctx): runfiles = otp_runfiles(ctx, otp_info) - ctx.actions.run_shell( - inputs = runfiles.files, - outputs = [version_file], - command = """set -euo pipefail - -{erl_rootdir_setup} - -export PATH="{erlang_home}"/bin:${{PATH}} - -"{elixir_home}"/bin/iex --version > {version_file} -""".format( - erl_rootdir_setup = otp_rootdir_setup(otp_info), - erlang_home = erlang_home(otp_info), - elixir_home = elixir_home, - version_file = version_file.path, - ), + elixir_version_action( + ctx, + otp_info, + elixir_home, + version_file, + runfiles.files, mnemonic = "ELIXIR", progress_message = "Validating elixir at {}".format(elixir_home), ) @@ -187,6 +188,7 @@ def _archive_root(files): Stripping it makes a prebuilt Elixir's bin/ and lib/ land at the release_dir root. Robust regardless of glob ordering (unlike using the first file's dir). """ + # NOTE: Get the dirname of the first file to calculate the lowest common # path prefix between all files. Chopping off the last element ensures we # don't return the file itself, if we only have one file. @@ -228,26 +230,12 @@ cp -rp "{archive_root}/." "$ABS_RELEASE_DIR/" progress_message = "Staging prebuilt Elixir", ) - ctx.actions.run_shell( - inputs = depset( - direct = [release_dir], - transitive = [runfiles.files], - ), - outputs = [version_file], - command = """set -euo pipefail - -{erl_rootdir_setup} - -export PATH="{erlang_home}"/bin:${{PATH}} - -"{elixir_home}"/bin/iex --version > {version_file} -""".format( - erl_rootdir_setup = otp_rootdir_setup(otp_info), - erlang_home = erlang_home(otp_info), - elixir_home = release_dir.path, - version_file = version_file.path, - ), - mnemonic = "ELIXIRVERSION", + elixir_version_action( + ctx, + otp_info, + release_dir.path, + version_file, + depset(direct = [release_dir], transitive = [runfiles.files]), progress_message = "Validating prebuilt elixir", ) diff --git a/private/elixir_source_build.bzl b/private/elixir_source_build.bzl index a098ec2..0743ce6 100644 --- a/private/elixir_source_build.bzl +++ b/private/elixir_source_build.bzl @@ -11,6 +11,7 @@ load( load( ":elixir_build.bzl", "ElixirInfo", + "elixir_version_action", ) def _elixir_source_build_impl(ctx): @@ -67,27 +68,12 @@ cp -r lib $ABS_RELEASE_DIR/ progress_message = "Building Elixir from source", ) - ctx.actions.run_shell( - inputs = depset( - direct = [release_dir], - transitive = [runfiles.files], - ), - outputs = [version_file], - command = """set -euo pipefail - -{erl_rootdir_setup} - -export PATH="{erlang_home}"/bin:${{PATH}} - -"{elixir_home}"/bin/iex --version > {version_file} -""".format( - erl_rootdir_setup = otp_rootdir_setup(otp_info), - erlang_home = erlang_home(otp_info), - elixir_home = release_dir.path, - version_file = version_file.path, - ), - mnemonic = "ELIXIRVERSION", - progress_message = "Validating elixir", + elixir_version_action( + ctx, + otp_info, + release_dir.path, + version_file, + depset(direct = [release_dir], transitive = [runfiles.files]), ) return [ From 0f25b2eba8cc4bc9e3cfc93dd87036adac55bc58 Mon Sep 17 00:00:00 2001 From: Denbeigh Stevens Date: Thu, 9 Jul 2026 20:16:00 +0000 Subject: [PATCH 8/8] add todo comment --- bzlmod/extensions.bzl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bzlmod/extensions.bzl b/bzlmod/extensions.bzl index 4e176fd..3dc6bfd 100644 --- a/bzlmod/extensions.bzl +++ b/bzlmod/extensions.bzl @@ -237,10 +237,15 @@ internal_elixir_from_prebuilt = tag_class(attrs = { "name": attr.string(), "version": attr.string(), "url": attr.string( - doc = "URL of a precompiled Elixir release archive (bin/ + lib/ at its root).", + doc = "URL of a precompiled Elixir release archive.", ), "strip_prefix": attr.string(), "sha256": attr.string(), + # NOTE: we currently depend on both of these so that we can populate our + # templated BUILD file correctly, and don't have access to OtpInfo here + # (unless we accept a label, and give up lazy loading). + # TODO(denbeigh): i believe i can remove this with a more coherent + # toolchain aliasing strategy in rules_erlang "otp": attr.string( mandatory = True, doc = "Name of an erlang_config installation to validate the prebuilt Elixir against.",