Skip to content

Commit

Permalink
refactor: move maybe_patch_elixir_erl_pass into compatibility module
Browse files Browse the repository at this point in the history
  • Loading branch information
zachallaun committed Aug 17, 2024
1 parent 60431e5 commit e5693ea
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 78 deletions.
22 changes: 11 additions & 11 deletions lib/lib_elixir/namespace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 5,7 @@ defmodule LibElixir.Namespace do

defstruct [
:module,
:source_dir,
:ebin_dir,
:target_dir,
:known_module_names,
:exclusion_names,
Expand All @@ -20,8 20,8 @@ defmodule LibElixir.Namespace do
Recursively namespaces modules in the `targets` list using `module` as
the namespace prefix, writing the transformed beams to `target_dir`.
"""
def transform!(module, targets, exclusions, source_dir, target_dir) do
ns = new(module, exclusions, source_dir, target_dir)
def transform!(module, targets, exclusions, ebin_dir, target_dir) do
ns = new(module, exclusions, ebin_dir, target_dir)

:ok = Namespace.App.rewrite(:elixir, ns)

Expand All @@ -44,9 44,9 @@ defmodule LibElixir.Namespace do
Calls `fun` with three arguments: `module, namespaced_module, target_path, binary`.
"""
def transform(module, targets, exclusions, source_dir, target_dir, fun)
def transform(module, targets, exclusions, ebin_dir, target_dir, fun)
when is_list(targets) and is_atom(module) and is_function(fun, 4) do
ns = new(module, exclusions, source_dir, target_dir)
ns = new(module, exclusions, ebin_dir, target_dir)
transform(targets, ns, fun)
end

Expand All @@ -59,7 59,7 @@ defmodule LibElixir.Namespace do
raise ArgumentError, message: "cannot transform targets: #{inspect(invalid)}"
end

Namespace.Abstract.maybe_patch_elixir_erl_pass!(ns.version)
Namespace.Compatibility.maybe_patch_elixir_erl_pass!(ns.version)
fan_out_transform(targets, ns, fun)
end

Expand Down Expand Up @@ -100,9 100,9 @@ defmodule LibElixir.Namespace do
|> fan_out_transform(ns, fun, transformed)
end

def new(module, exclusions, source_dir, target_dir) do
def new(module, exclusions, ebin_dir, target_dir) do
known_module_names =
source_dir
ebin_dir
|> Path.join("*")
|> Path.wildcard()
|> Enum.map(fn path ->
Expand All @@ -114,17 114,17 @@ defmodule LibElixir.Namespace do

%__MODULE__{
module: module,
source_dir: source_dir,
ebin_dir: ebin_dir,
target_dir: target_dir,
known_module_names: known_module_names,
exclusion_names: exclusion_names,
version: Namespace.Versions.fetch_version!(source_dir)
version: Namespace.Compatibility.fetch_version!(ebin_dir)
}
end

def source_path(%__MODULE__{} = ns, module, ext \\ "beam") do
module_name = to_string(module)
ns.source_dir |> Path.join("#{module_name}.#{ext}") |> Path.expand()
ns.ebin_dir |> Path.join("#{module_name}.#{ext}") |> Path.expand()
end

def target_path(%__MODULE__{} = ns, namespaced_module, ext \\ "beam") do
Expand Down
52 changes: 0 additions & 52 deletions lib/lib_elixir/namespace/abstract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -130,56 130,4 @@ defmodule LibElixir.Namespace.Abstract do
{module, deps}
end
end

# handles a change to the representation of bitstring
# modifiers in debug info from {:binary, meta, []} to
# {:binary, meta, nil} that occurred on Elixir 1.15
@doc false
def maybe_patch_elixir_erl_pass!(target_version) do
if should_patch_elixir_erl_pass?(target_version) do
patch_elixir_erl_pass!()
end
end

@doc false
def patch_elixir_erl_pass! do
:persistent_term.put({__MODULE__, :patched?}, true)

Patch.Supervisor.start_link()

Patch.patch(:elixir_erl_pass, :extract_bit_type, fn any, list ->
try_extract_bit_type(any, list)
end)

:ok
end

defp try_extract_bit_type({name, meta, args}, list) do
extract_bit_type({name, meta, args}, list)
rescue
_ ->
try do
case args do
[] -> extract_bit_type({name, meta, nil}, list)
nil -> extract_bit_type({name, meta, []}, list)
end
rescue
e ->
IO.inspect({args, list}, label: "failed on")
reraise e, __STACKTRACE__
end
end

defp extract_bit_type(x, list) do
Patch.Mock.Naming.original(:elixir_erl_pass).extract_bit_type(x, list)
end

defp should_patch_elixir_erl_pass?(target_version) do
already_patched? = :persistent_term.get({__MODULE__, :patched?}, false)
current_version = Version.parse!(System.version())

not already_patched? and
((current_version.minor < 15 and target_version.minor >= 15) or
(current_version.minor >= 15 and target_version.minor < 15))
end
end
76 changes: 76 additions & 0 deletions lib/lib_elixir/namespace/compatibility.ex
Original file line number Diff line number Diff line change
@@ -0,0 1,76 @@
defmodule LibElixir.Namespace.Compatibility do
@moduledoc false

@doc """
Fetches the Elixir version from the `VERSION` file relative to `ebin_dir`.
"""
def fetch_version!(ebin_dir) do
[ebin_dir, "..", "..", "..", "VERSION"]
|> Path.join()
|> File.read!()
|> String.trim()
|> Version.parse!()
end

@doc """
Handles a change to the representation of bitstring modifiers in the
debug_info chunk.
In Elixir 1.15, bitstring modifiers changed their normalized AST
representation from `<<x::integer()>>` to `<<x::integer>>`, which
means `{modifier, meta, []}` became `{modifier, meta, nil}`. So, if
we're compiling an Elixir > 1.15 on a runtime < 1.15, we wind up with
function clause errors.
This function monkey-patches `:elixir_erl_pass.extract_bit_type/2` if
necessary to coerce the arguments into the correct form.
Relevant PR: https://github.com/elixir-lang/elixir/pull/12055
"""
def maybe_patch_elixir_erl_pass!(target_version) do
if should_patch_elixir_erl_pass?(target_version) do
patch_elixir_erl_pass!()
end
end

defp patch_elixir_erl_pass! do
:persistent_term.put({__MODULE__, :patched?}, true)

Patch.Supervisor.start_link()

Patch.patch(:elixir_erl_pass, :extract_bit_type, fn any, list ->
try_extract_bit_type(any, list)
end)

:ok
end

defp try_extract_bit_type({name, meta, args}, list) do
extract_bit_type({name, meta, args}, list)
rescue
_ ->
try do
case args do
[] -> extract_bit_type({name, meta, nil}, list)
nil -> extract_bit_type({name, meta, []}, list)
end
rescue
e ->
IO.inspect({args, list}, label: "failed on")
reraise e, __STACKTRACE__
end
end

defp extract_bit_type(x, list) do
Patch.Mock.Naming.original(:elixir_erl_pass).extract_bit_type(x, list)
end

defp should_patch_elixir_erl_pass?(target_version) do
already_patched? = :persistent_term.get({__MODULE__, :patched?}, false)
current_version = Version.parse!(System.version())

not already_patched? and
((current_version.minor < 15 and target_version.minor >= 15) or
(current_version.minor >= 15 and target_version.minor < 15))
end
end
14 changes: 0 additions & 14 deletions lib/lib_elixir/namespace/versions.ex

This file was deleted.

2 changes: 1 addition & 1 deletion test/lib_elixir/namespace_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 8,7 @@ defmodule LibElixir.NamespaceTest do

setup_all do
start_supervised!(Patch.Supervisor)
Patch.patch(Namespace.Versions, :fetch_version!, Version.parse!("1.17.2"))
Patch.patch(Namespace.Compatibility, :fetch_version!, Version.parse!("1.17.2"))
[namespace: Namespace.new(Test.LibElixir, [], @beams, Path.join(@beams, "target"))]
end

Expand Down

0 comments on commit e5693ea

Please sign in to comment.