Current section
Files
Jump to
Current section
Files
lib/pi/target/compiler.ex
defmodule Pi.Target.Compiler do
@moduledoc "Compiles target code while preserving the last known-good build on failure."
alias Pi.Project.Context
alias Pi.Target.{Connection, Supervisor}
def compile(opts \\ []) do
context = Context.current(opts)
profile = Keyword.get(opts, :profile, :project)
with {:ok, connection} <- Supervisor.connection(context, profile),
{:ok, handshake} <- Connection.request(connection, :ping, %{}),
app when is_atom(app) <- handshake[:app] do
compile_with_backup(context, app, opts)
else
nil -> {:error, %{kind: :project_app_unknown}}
error -> error
end
end
defp compile_with_backup(context, app, opts) do
app_ebin = Path.join([context.build_path, "lib", Atom.to_string(app), "ebin"])
manifest = Path.join(context.build_path, ".mix")
before_hashes = beam_hashes(app_ebin)
backup = backup_paths([app_ebin, manifest])
args = if Keyword.get(opts, :force, false), do: ["compile", "--force"], else: ["compile"]
try do
{output, status} =
Context.command(context, "mix", args,
stderr_to_stdout: true,
env: [{"MIX_ENV", context.mix_env}]
)
if status == 0 do
changed = changed_beams(before_hashes, beam_hashes(app_ebin))
reloads = reload_connections(context, changed)
{:ok,
%{
app: app,
output: output,
changed_beams: changed,
reloaded: reloads,
last_good_preserved: true
}}
else
restore_backup(backup)
{:error,
%{
kind: :compile_error,
status: status,
output: output,
last_good_preserved: true
}}
end
after
remove_backup(backup)
end
end
defp reload_connections(_context, []), do: []
defp reload_connections(context, beams) do
for profile <- [:project, :application],
[{pid, _value}] <- [Registry.lookup(Pi.Target.Registry, {context.root, profile})] do
%{profile: profile, result: Connection.request(pid, :reload, %{beams: beams})}
end
end
defp beam_hashes(path) do
path
|> Path.join("*.beam")
|> Path.wildcard()
|> Map.new(fn beam -> {beam, beam |> File.read!() |> sha256()} end)
end
defp changed_beams(before, after_hashes) do
after_hashes
|> Enum.filter(fn {path, hash} -> Map.get(before, path) != hash end)
|> Enum.map(&elem(&1, 0))
|> Enum.sort()
end
defp backup_paths(paths) do
root = Path.join(System.tmp_dir!(), "pi-target-build-#{System.unique_integer([:positive])}")
File.mkdir_p!(root)
entries =
paths
|> Enum.with_index()
|> Enum.map(fn {source, index} ->
target = Path.join(root, Integer.to_string(index))
if File.exists?(source) do
{:ok, _files} = File.cp_r(source, target)
%{source: source, target: target, existed: true}
else
%{source: source, target: target, existed: false}
end
end)
%{root: root, entries: entries}
end
defp restore_backup(%{entries: entries}) do
Enum.each(entries, fn entry ->
File.rm_rf!(entry.source)
if entry.existed do
File.mkdir_p!(Path.dirname(entry.source))
{:ok, _files} = File.cp_r(entry.target, entry.source)
end
end)
end
defp remove_backup(%{root: root}), do: File.rm_rf(root)
defp sha256(binary), do: :crypto.hash(:sha256, binary)
end