From 26d4949dcd50520f1b8bcce010d38951afe7f445 Mon Sep 17 00:00:00 2001 From: Martin Kuntz Jacobsen 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 //.... --- .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