1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
|
defmodule Diff do
@moduledoc """
Utilities for comparing build artifacts.
"""
@atom_chunks ~w(
atoms
attributes
compile_info
debug_info
exports
labeled_exports
imports
indexed_imports
locals
labeled_locals
)a
@binary_chunks ~w(
Attr
AtU8
CInf
Dbgi
Docs
ExCk
ExpT
ImpT
LocT
)c
@doc """
Compares the build artifacts of two build directories.
"""
@spec compare_dirs(Path.t(), Path.t()) ::
{
only1_paths :: list(Path.t()),
only2_paths :: list(Path.t()),
diff :: list({Path.t(), diff :: String.t()})
}
def compare_dirs(dir1, dir2) do
dir1 = Path.expand(dir1)
dir2 = Path.expand(dir2)
assert_dir!(dir1)
assert_dir!(dir2)
dir1_paths = relative_paths(dir1)
dir2_paths = relative_paths(dir2)
only1_paths = dir1_paths -- dir2_paths
only2_paths = dir2_paths -- dir1_paths
common_paths = dir1_paths -- only1_paths
common_files = Enum.reject(common_paths, &File.dir?/1)
diff =
Enum.flat_map(common_files, fn path ->
file1 = Path.join(dir1, path)
file2 = Path.join(dir2, path)
case compare_files(file1, file2) do
:eq -> []
{:diff, diff} -> [{path, diff}]
end
end)
{only1_paths, only2_paths, diff}
end
@doc """
Compares the contents of two files.
If the files are BEAM files, it performs a more human-friendly
"BEAM-diff".
"""
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
def compare_files(file1, file2) do
content1 = File.read!(file1)
content2 = File.read!(file2)
if content1 == content2 do
:eq
else
diff =
if String.ends_with?(file1, ".beam") do
beam_diff(file1, content1, file2, content2)
else
file_diff(file1, file2)
end
{:diff, diff}
end
end
defp beam_diff(file1, content1, file2, content2) do
chunk_diff(content1, content2, @atom_chunks, &inspect(&1, pretty: true, limit: :infinity)) ||
chunk_diff(content1, content2, @binary_chunks, &(&1 |> write_tmp() |> xxd_dump())) ||
(
tmp_file1 =
file1
|> xxd_dump()
|> write_tmp()
tmp_file2 =
file2
|> xxd_dump()
|> write_tmp()
file_diff(tmp_file1, tmp_file2)
)
end
defp chunk_diff(content1, content2, names, formatter) do
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, names),
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, names),
true <- chunks1 != chunks2 do
if length(chunks1) != length(chunks2) do
"""
Different chunks:
* #{inspect(chunks1)}
* #{inspect(chunks2)}
"""
else
for {{name1, chunk1}, {name2, chunk2}} <- Enum.zip(chunks1, chunks2),
true = name1 == name2,
chunk1 != chunk2 do
tmp_file1 = chunk1 |> formatter.() |> write_tmp()
tmp_file2 = chunk2 |> formatter.() |> write_tmp()
[to_string(name1), ?\n, file_diff(tmp_file1, tmp_file2)]
end
end
else
_ -> nil
end
end
defp xxd_dump(file) do
{dump, _} = System.cmd("xxd", [file])
dump
end
defp file_diff(file1, file2) do
{diff, _} = System.cmd("diff", ["--suppress-common-lines", file1, file2])
diff
end
defp relative_paths(dir) do
dir
|> Path.join("**")
|> Path.wildcard()
|> Enum.map(&Path.relative_to(&1, dir))
end
defp assert_dir!(dir) do
if not File.dir?(dir) do
raise ArgumentError, "#{inspect(dir)} is not a directory"
end
end
defp write_tmp(content) do
filename = "tmp-#{System.unique_integer([:positive])}"
File.mkdir_p!("tmp")
File.write!(Path.join("tmp", filename), content)
Path.join("tmp", filename)
end
end
if :deterministic not in :compile.env_compiler_options() do
IO.puts("Cannot validate if reproducible without setting ERL_COMPILER_OPTIONS=deterministic")
System.halt(1)
end
case System.argv() do
[dir1, dir2] ->
case Diff.compare_dirs(dir1, dir2) do
{[], [], []} ->
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
{only1, only2, diff} ->
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
System.halt(1)
end
_ ->
IO.puts("Please, provide two directories as arguments")
System.halt(1)
end
|