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
6 changes: 6 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ string_flag(
visibility = ["//visibility:public"],
)

string_flag(
name = "elixir_platform",
build_setting_default = "",
visibility = ["//visibility:public"],
)

toolchain_type(
name = "toolchain_type",
visibility = ["//visibility:public"],
Expand Down
14 changes: 11 additions & 3 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ archive_override(
url = "https://github.com/discord/rules_erlang/archive/b4b01911b99944fa0c9efb66bf3d9030ee784fa9.tar.gz",
)

# erlang_config is needed so @erlang_config is visible in repos created
# by the elixir_config extension (for wiring the OTP build dependency).
erlang_config = use_extension(
"@rules_erlang//bzlmod:extensions.bzl",
"erlang_config",
)
use_repo(
erlang_config,
"erlang_config",
)

elixir_config = use_extension(
"@rules_elixir//bzlmod:extensions.bzl",
"elixir_config",
Expand All @@ -27,9 +38,6 @@ use_repo(
"elixir_config",
)

register_toolchains(
"@elixir_config//external:toolchain",
)

# Use elixir_packages extension to get hex_pm
elixir_packages = use_extension(
Expand Down
72 changes: 66 additions & 6 deletions bzlmod/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,45 @@ DEFAULT_ELIXIR_VERSION = "1.15.0"
DEFAULT_ELIXIR_SHA256 = "0f4df7574a5f300b5c66f54906222cd46dac0df7233ded165bc8e80fd9ffeb7a"

def _elixir_config(ctx):
# Resolve the canonical repo name for @erlang_config so we can reference
# it from generated http_archive repos (which don't have it in their
# repo mapping). We use Label().repo_name per Bazel docs rather than
# hardcoding the canonical name format.
erlang_config_repo = Label("@erlang_config").repo_name

types = {}
versions = {}
urls = {}
strip_prefixs = {}
sha256s = {}
elixir_homes = {}
target_compatible_withs = {}
exec_compatible_withs = {}
erlang_versions = {}

for mod in ctx.modules:
for elixir in mod.tags.external_elixir_from_path:
types[elixir.name] = INSTALLATION_TYPE_EXTERNAL
versions[elixir.name] = elixir.version
elixir_homes[elixir.name] = elixir.elixir_home
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]


for elixir in mod.tags.internal_elixir_from_http_archive:
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

# Create repository for downloading and building Elixir source
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

# Create repository for downloading and building Elixir source.
# The http_archive repo doesn't have @erlang_config in its repo
# mapping, so we use the canonical name resolved above.
http_archive(
name = "elixir_source_{}".format(elixir.name),
url = elixir.url,
Expand All @@ -49,9 +67,13 @@ load("@rules_elixir//private:elixir_build.bzl", "elixir_build")
elixir_build(
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,
),
)

for elixir in mod.tags.internal_elixir_from_github_release:
Expand All @@ -65,8 +87,14 @@ elixir_build(
urls[elixir.name] = url
strip_prefixs[elixir.name] = strip_prefix
sha256s[elixir.name] = elixir.sha256

# Create repository for downloading and building Elixir source
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

# Create repository for downloading and building Elixir source.
# The http_archive repo doesn't have @erlang_config in its repo
# mapping, so we use the canonical name resolved above.
http_archive(
name = "elixir_source_{}".format(elixir.name),
url = url,
Expand All @@ -78,9 +106,13 @@ load("@rules_elixir//private:elixir_build.bzl", "elixir_build")
elixir_build(
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,
),
)

_elixir_config_rule(
Expand All @@ -91,12 +123,19 @@ elixir_build(
strip_prefixs = strip_prefixs,
sha256s = sha256s,
elixir_homes = elixir_homes,
exec_compatible_withs = exec_compatible_withs,
target_compatible_withs = target_compatible_withs,
erlang_versions = erlang_versions,
)

external_elixir_from_path = tag_class(attrs = {
"name": attr.string(),
"version": attr.string(),
"elixir_home": attr.string(),
# It doesn't...really make sense to have an exec_compatible_with for an
# extenral toolchain that's already on the host?
"exec_compatible_with": attr.label_list(default = []),
"target_compatible_with": attr.label_list(default = []),
})

internal_elixir_from_http_archive = tag_class(attrs = {
Expand All @@ -105,6 +144,16 @@ internal_elixir_from_http_archive = tag_class(attrs = {
"url": attr.string(),
"strip_prefix": attr.string(),
"sha256": attr.string(),
"otp": attr.string(
mandatory = True,
doc = "Name of an erlang_config installation to use for building Elixir (e.g. '25_bootstrap').",
),
"erlang_version": attr.string(
doc = "OTP version string (e.g. '26.3') for generating 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 = []),
})

internal_elixir_from_github_release = tag_class(attrs = {
Expand All @@ -117,6 +166,17 @@ internal_elixir_from_github_release = tag_class(attrs = {
"sha256": attr.string(
default = DEFAULT_ELIXIR_SHA256,
),
"otp": attr.string(
mandatory = True,
doc = "Name of an erlang_config installation to use for building Elixir (e.g. '25_bootstrap').",
),
"erlang_version": attr.string(
doc = "OTP version string (e.g. '26.3') for generating combined platform constraints. " +
"Must match the version of the OTP installation named by the 'otp' attr.",
),
# NOTE: these should default to the host platform
"exec_compatible_with": attr.label_list(default = []),
"target_compatible_with": attr.label_list(default = []),
})

elixir_config = module_extension(
Expand Down
10 changes: 4 additions & 6 deletions defs.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,17 @@ load(
load("//private:elixir_sys_config.bzl", _elixir_sys_config = "elixir_sys_config")

# mix-backed targets. Generally fairly reliable, and set-and-forget.
def mix_library(*args, **kwargs):
def mix_library(name, app_name, deps = [], **kwargs):
"""Compiles an Elixir library using Mix.

Automatically adds dependencies for hex unless the app_name is "hex",
because hex is required for mix to evaluate if a dev/runtime/test/etc
dependency is relevant in the current compilation context (even though
we invoke in offline mode)
"""
deps = kwargs.pop("deps", [])
if kwargs.get("app_name") != "hex":
deps.append(Label("@hex_pm//:lib"))
kwargs['deps'] = deps
_mix_library(*args, **kwargs)
if app_name != "hex":
deps = deps + [Label("@hex_pm//:lib")]
_mix_library(name = name, app_name = app_name, deps = deps, **kwargs)

def mix_release(*args, **kwargs):
_mix_release(*args, **kwargs)
Expand Down
42 changes: 42 additions & 0 deletions private/beam_transitions.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Configuration transitions for platform-independent BEAM compilation.

BEAM bytecode (.beam) and .app files are platform-independent artifacts.
This transition normalizes platform-related settings so that compilation
actions get the same configuration hash regardless of target platform,
enabling remote cache hits across platforms.

When //:elixir_platform is set, the transition uses that platform (which
should carry both elixir_version and erlang_version constraints) for
correct toolchain resolution with multiple versions.

Falls back to @rules_erlang//:erlang_platform if only the Erlang flag
is set (covers transitive Erlang rule usage).

When both flags are empty (default), --platforms is cleared to [] —
identical to the previous behavior, safe for single-version setups.
"""

# Cross-repo reference needs Label() for canonicalization. The str() result
# is used as both the inputs entry and the settings dict key.
_ERLANG_PLATFORM_LABEL = str(Label("@rules_erlang//:erlang_platform"))

def _platform_independent_impl(settings, attr):
elixir_platform = settings["//:elixir_platform"]
if elixir_platform:
return {"//command_line_option:platforms": [elixir_platform]}

erlang_platform = settings[_ERLANG_PLATFORM_LABEL]
if erlang_platform:
return {"//command_line_option:platforms": [erlang_platform]}

return {"//command_line_option:platforms": []}

platform_independent_transition = transition(
implementation = _platform_independent_impl,
inputs = [
"//command_line_option:platforms",
"//:elixir_platform",
_ERLANG_PLATFORM_LABEL,
],
outputs = ["//command_line_option:platforms"],
)
62 changes: 47 additions & 15 deletions private/elixir_build.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ load(
"http_archive",
)
load(
"@rules_erlang//tools:erlang_toolchain.bzl",
"erlang_dirs",
"maybe_install_erlang",
"@rules_erlang//private:erlang_build.bzl",
"OtpInfo",
)

ElixirInfo = provider(
Expand All @@ -21,11 +20,34 @@ ElixirInfo = provider(
],
)

def otp_install_snippet(otp_info):
"""Generate shell snippet to extract OTP release tar, like maybe_install_erlang."""
if otp_info.release_dir_tar == None:
return ""
return """\
mkdir -p $(dirname "{install_path}")
if mkdir "{install_path}"; then
tar --extract --no-same-owner \\
--directory "{install_path}" \\
--file {release_tar}
fi""".format(
release_tar = otp_info.release_dir_tar.path,
install_path = otp_info.install_path,
)

def otp_runfiles(ctx, otp_info):
"""Build runfiles depset for OTP, like erlang_dirs."""
if otp_info.release_dir_tar != None:
return ctx.runfiles([otp_info.release_dir_tar, otp_info.version_file])
else:
return ctx.runfiles([otp_info.version_file])

def _elixir_build_impl(ctx):
otp_info = ctx.attr.otp[OtpInfo]
release_dir = ctx.actions.declare_directory("elixir_release")
version_file = ctx.actions.declare_file("elixir_version")

(erlang_home, _, runfiles) = erlang_dirs(ctx)
runfiles = otp_runfiles(ctx, otp_info)

ctx.actions.run_shell(
inputs = depset(
Expand Down Expand Up @@ -63,8 +85,8 @@ make
cp -r bin $ABS_RELEASE_DIR/
cp -r lib $ABS_RELEASE_DIR/
""".format(
maybe_install_erlang = maybe_install_erlang(ctx),
erlang_home = erlang_home,
maybe_install_erlang = otp_install_snippet(otp_info),
erlang_home = otp_info.erlang_home,
release_path = release_dir.path,
source_files = " ".join([f.path for f in ctx.files.srcs]),
first_source_file = ctx.files.srcs[0].path if ctx.files.srcs else "",
Expand All @@ -88,8 +110,8 @@ export PATH="{erlang_home}"/bin:${{PATH}}

"{elixir_home}"/bin/iex --version > {version_file}
""".format(
maybe_install_erlang = maybe_install_erlang(ctx),
erlang_home = erlang_home,
maybe_install_erlang = otp_install_snippet(otp_info),
erlang_home = otp_info.erlang_home,
elixir_home = release_dir.path,
version_file = version_file.path,
),
Expand All @@ -99,7 +121,7 @@ export PATH="{erlang_home}"/bin:${{PATH}}

return [
DefaultInfo(files = depset([release_dir, version_file])),
ctx.toolchains["@rules_erlang//tools:toolchain_type"].otpinfo,
otp_info,
ElixirInfo(
release_dir = release_dir,
elixir_home = None,
Expand All @@ -111,18 +133,24 @@ elixir_build = rule(
implementation = _elixir_build_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
"otp": attr.label(
mandatory = True,
providers = [OtpInfo],
doc = "An erlang_build target to use for compiling Elixir.",
),
},
toolchains = ["@rules_erlang//tools:toolchain_type"],
)

def _elixir_external_impl(ctx):
otp_info = ctx.attr.otp[OtpInfo]

elixir_home = ctx.attr.elixir_home
if elixir_home == "":
elixir_home = ctx.attr._elixir_home[BuildSettingInfo].value

version_file = ctx.actions.declare_file(ctx.label.name + "_version")

(erlang_home, _, runfiles) = erlang_dirs(ctx)
runfiles = otp_runfiles(ctx, otp_info)

ctx.actions.run_shell(
inputs = runfiles.files,
Expand All @@ -135,8 +163,8 @@ export PATH="{erlang_home}"/bin:${{PATH}}

"{elixir_home}"/bin/iex --version > {version_file}
""".format(
maybe_install_erlang = maybe_install_erlang(ctx),
erlang_home = erlang_home,
maybe_install_erlang = otp_install_snippet(otp_info),
erlang_home = otp_info.erlang_home,
elixir_home = elixir_home,
version_file = version_file.path,
),
Expand All @@ -148,7 +176,7 @@ export PATH="{erlang_home}"/bin:${{PATH}}
DefaultInfo(
files = depset([version_file]),
),
ctx.toolchains["@rules_erlang//tools:toolchain_type"].otpinfo,
otp_info,
ElixirInfo(
release_dir = None,
elixir_home = elixir_home,
Expand All @@ -161,6 +189,10 @@ elixir_external = rule(
attrs = {
"_elixir_home": attr.label(default = Label("//:elixir_home")),
"elixir_home": attr.string(),
"otp": attr.label(
mandatory = True,
providers = [OtpInfo],
doc = "An erlang_build target providing the OTP installation.",
),
},
toolchains = ["@rules_erlang//tools:toolchain_type"],
)
Loading
Loading