193 lines
4.6 KiB
Elixir
193 lines
4.6 KiB
Elixir
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
|
|
|
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
|