From 1aabccfdf09170ba6c11718fe6e9f00b68b10c56 Mon Sep 17 00:00:00 2001 From: Jon Carstens Date: Fri, 30 Jun 2023 09:54:05 -0600 Subject: [PATCH 1/2] Prevent bad vm.args in Nerves firmware With Elixir 1.15, the way the Elixir IEx shell is started on device has changed and using the old way will break things. This adds a new check during the release (firmware) initialization and stops the firmware build if the old method is still there --- lib/nerves/release.ex | 78 +++++++++++++++++++++++ test/fixtures/release_app/mix.exs | 1 + test/fixtures/release_app/rel/vm.args.eex | 1 + test/nerves/release_test.exs | 52 +++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 test/fixtures/release_app/rel/vm.args.eex diff --git a/lib/nerves/release.ex b/lib/nerves/release.ex index 42cb490f..1090a545 100644 --- a/lib/nerves/release.ex +++ b/lib/nerves/release.ex @@ -16,6 +16,8 @@ defmodule Nerves.Release do steps: release.steps ++ [&Nerves.Release.finalize/1] } + check_vm_args_compatibility!(release) + _ = File.rm_rf!(release.path) if Code.ensure_loaded?(Shoehorn.Release) do @@ -167,4 +169,80 @@ defmodule Nerves.Release do {to_string(app), to_string(opts[:vsn]), Path.expand(opts[:path] || "")} end end + + @elixir_1_15_opts ["-user elixir", "-run elixir start_iex"] + @legacy_elixir_opts ["-user Elixir.IEx.CLI"] + defp check_vm_args_compatibility!(release) do + Mix.shell().info([:yellow, "* [Nerves] ", :reset, "validating vm.args"]) + + {exclusions, inclusions} = + if Version.match?(System.version(), ">= 1.15.0") do + {@legacy_elixir_opts, @elixir_1_15_opts} + else + {@elixir_1_15_opts, @legacy_elixir_opts} + end + + vm_args = File.read!(vm_args_path) + + errors = + [] + |> check_vm_args_inclusions(vm_args, inclusions, vm_args_path) + |> check_vm_args_exclusions(vm_args, exclusions, vm_args_path) + + if length(errors) > 0 do + errs = IO.ANSI.format(errors) |> IO.chardata_to_string() + + Mix.raise(""" + Incompatible vm.args.eex + + The procedure for starting IEx changed in Elixir 1.15. The rel/vm.args.eex for + this project starts IEx in an incompatible way for the version of Elixir you're + using and won't work. + + To fix this, either change the version of Elixir that you're using or make the + following changes to vm.args.eex: + #{errs} + """) + else + :ok + end + end + + defp check_vm_args_exclusions(errors, vm_args, exclusions, vm_args_path) do + String.split(vm_args, "\n") + |> Enum.with_index(1) + |> Enum.filter(fn {line, _} -> Enum.any?(exclusions, &String.contains?(line, &1)) end) + |> case do + [] -> + [] + + lines -> + [ + "\nPlease remove the following lines:\n\n", + Enum.map(lines, fn {line, line_num} -> + ["* ", vm_args_path, ":", to_string(line_num), ":\n ", :red, line, "\n"] + end) + | errors + ] + end + end + + defp check_vm_args_inclusions(errors, vm_args, inclusions, vm_args_path) do + case Enum.reject(inclusions, &String.contains?(vm_args, &1)) do + [] -> + [] + + lines -> + [ + [ + "\nPlease ensure the following lines are in ", + vm_args_path, + ":\n", + :green, + Enum.map(lines, &[" ", &1, "\n"]) + ] + | errors + ] + end + end end diff --git a/test/fixtures/release_app/mix.exs b/test/fixtures/release_app/mix.exs index 4d934bbf..5647f063 100644 --- a/test/fixtures/release_app/mix.exs +++ b/test/fixtures/release_app/mix.exs @@ -27,6 +27,7 @@ defmodule ReleaseApp.Fixture do [ overwrite: true, steps: [&Nerves.Release.init/1, :assemble], + rel_templates_path: System.get_env("REL_TEMPLATES_PATH", "rel"), strip_beams: true ] end diff --git a/test/fixtures/release_app/rel/vm.args.eex b/test/fixtures/release_app/rel/vm.args.eex new file mode 100644 index 00000000..d7a6c8e7 --- /dev/null +++ b/test/fixtures/release_app/rel/vm.args.eex @@ -0,0 +1 @@ +# File required by Nerves diff --git a/test/nerves/release_test.exs b/test/nerves/release_test.exs index 39a6e35f..9dfd1da4 100644 --- a/test/nerves/release_test.exs +++ b/test/nerves/release_test.exs @@ -26,4 +26,56 @@ defmodule Nerves.ReleaseTest do |> File.read!() |> String.starts_with?(expected) end + + @tag :tmp_dir + @tag :release + test "fails if vm.args has incompatible shell setting", %{tmp_dir: tmp} do + {path, env} = compile_fixture!("release_app", tmp, [], []) + + rel_templates_path = Path.join(tmp, "bad_rel") + assert :ok = File.mkdir_p(rel_templates_path) + bad_vm_args = Path.join(rel_templates_path, "vm.args.eex") + + expected = + if Version.match?(System.version(), ">= 1.15.0") do + assert :ok = File.write(bad_vm_args, "# test.vm.args\n-user Elixir.IEx.CLI") + + ~r""" + Please remove the following lines: + + \* #{bad_vm_args}:2: + -user Elixir.IEx.CLI + + Please ensure the following lines are in #{bad_vm_args}: + -user elixir + -run elixir start_iex + """ + else + assert :ok = + File.write(bad_vm_args, "# test.vm.args\n-user elixir\n-run elixir start_iex") + + ~r""" + Please remove the following lines: + + \* #{bad_vm_args}:2: + -user elixir + \* #{bad_vm_args}:3: + -run elixir start_iex + + Please ensure the following lines are in #{bad_vm_args}: + -user Elixir.IEx.CLI + """ + end + + opts = [ + cd: path, + env: [{"MIX_ENV", "prod"}, {"REL_TEMPLATES_PATH", rel_templates_path} | env], + stderr_to_stdout: true + ] + + {output, 1} = System.cmd("mix", ["release"], opts) + + assert output =~ "Incompatible vm.args" + assert output =~ expected + end end From 5d16111b7f1d881cf183fb1962a92456673af977 Mon Sep 17 00:00:00 2001 From: Jon Carstens Date: Fri, 30 Jun 2023 10:05:48 -0600 Subject: [PATCH 2/2] Fail if `vm.args.eex` is missing from the release build This file is required by Nerves, but has gone previously unchecked. Without it, things would be very sad. This adds a check in to prevent firmware being built if there is no vm.args.eex file. In the future, we may want to validate several pieces of it --- lib/nerves/release.ex | 5 +++++ test/nerves/release_test.exs | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/nerves/release.ex b/lib/nerves/release.ex index 1090a545..704e52e4 100644 --- a/lib/nerves/release.ex +++ b/lib/nerves/release.ex @@ -174,6 +174,11 @@ defmodule Nerves.Release do @legacy_elixir_opts ["-user Elixir.IEx.CLI"] defp check_vm_args_compatibility!(release) do Mix.shell().info([:yellow, "* [Nerves] ", :reset, "validating vm.args"]) + vm_args_path = Mix.Release.rel_templates_path(release, "vm.args.eex") + + if not File.exists?(vm_args_path) do + Mix.raise("Missing required #{vm_args_path}") + end {exclusions, inclusions} = if Version.match?(System.version(), ">= 1.15.0") do diff --git a/test/nerves/release_test.exs b/test/nerves/release_test.exs index 9dfd1da4..c83817a1 100644 --- a/test/nerves/release_test.exs +++ b/test/nerves/release_test.exs @@ -27,6 +27,22 @@ defmodule Nerves.ReleaseTest do |> String.starts_with?(expected) end + @tag :tmp_dir + @tag :release + test "requires vm.args.eex", %{tmp_dir: tmp} do + {path, env} = compile_fixture!("release_app", tmp, [], []) + + opts = [ + cd: path, + env: [{"MIX_ENV", "prod"}, {"REL_TEMPLATES_PATH", Path.join(tmp, "no-rel")} | env], + stderr_to_stdout: true + ] + + assert {output, 1} = System.cmd("mix", ["release"], opts) + + assert output =~ ~r/Missing required .*vm\.args\.eex/ + end + @tag :tmp_dir @tag :release test "fails if vm.args has incompatible shell setting", %{tmp_dir: tmp} do