Current section

Files

Jump to
igniter installer lib private deps_compile.ex
Raw

installer/lib/private/deps_compile.ex

defmodule Igniter.Util.DepsCompile do
@moduledoc false
# Copied from core mix task deps.compile, with small alterations
# the primary difference is reenabling the mix tasks
# Compiles dependencies.
# By default, this task attempts to compile all dependencies.
# A list of dependencies can be given to compile multiple
# dependencies in order.
# This task attempts to detect if the project contains one of
# the following files and act accordingly:
# * `mix.exs` - invokes `mix compile`
# * `rebar.config` - invokes `rebar compile`
# * `Makefile.win`- invokes `nmake /F Makefile.win` (only on Windows)
# * `Makefile` - invokes `gmake` on DragonFlyBSD, FreeBSD, NetBSD, and OpenBSD,
# invokes `make` on any other operating system (except on Windows)
# The compilation can be customized by passing a `compile` option
# in the dependency:
# {:some_dependency, "0.1.0", compile: "command to compile"}
# If a list of dependencies is given, Mix will attempt to compile
# them as is. For example, if project `a` depends on `b`, calling
# `mix deps.compile a` will compile `a` even if `b` is out of
# date. This is to allow parts of the dependency tree to be
# recompiled without propagating those changes upstream. To ensure
# `b` is included in the compilation step, pass `--include-children`.
# ## Command line options
# * `--force` - force compilation of deps
# * `--skip-umbrella-children` - skips umbrella applications from compiling
# * `--skip-local-deps` - skips non-remote dependencies, such as path deps, from compiling
def run do
Mix.Project.get!()
deps = Mix.Dep.load_and_cache()
opts = [include_children: true]
compile(filter_available_and_local_deps(deps), opts)
end
@doc false
def compile(deps, options \\ []) do
shell = Mix.shell()
config = Mix.Project.deps_config()
Mix.Task.run("deps.precompile")
igniter_needs_compiling? =
not Code.ensure_loaded?(Igniter)
compiled =
deps
|> then(fn deps ->
if igniter_needs_compiling? do
deps
else
Enum.reject(deps, &(&1.app == :igniter))
end
end)
|> Enum.map(fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
check_unavailable!(app, scm, status)
maybe_clean(dep, options)
compiled? =
cond do
not is_nil(opts[:compile]) ->
do_compile(dep, config)
Mix.Dep.mix?(dep) ->
do_mix(dep, config)
Mix.Dep.make?(dep) ->
do_make(dep, config)
dep.manager == :rebar3 ->
do_rebar3(dep, config)
true ->
shell.error(
"Could not compile #{inspect(app)}, no \"mix.exs\", \"rebar.config\" or \"Makefile\" " <>
"(pass :compile as an option to customize compilation, set it to \"false\" to do nothing)"
)
false
end
# We should touch fetchable dependencies even if they
# did not compile otherwise they will always be marked
# as stale, even when there is nothing to do.
fetchable? = touch_fetchable(scm, opts[:build])
compiled? and fetchable?
end)
if true in compiled, do: Mix.Task.run("will_recompile"), else: :ok
end
defp maybe_clean(dep, opts) do
# If a dependency was marked as fetched or with an out of date lock
# or missing the app file, we always compile it from scratch.
if Keyword.get(opts, :force, false) or Mix.Dep.compilable?(dep) do
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
end
end
defp touch_fetchable(scm, path) do
if scm.fetchable?() do
path = Path.join(path, ".mix")
File.mkdir_p!(path)
File.touch!(Path.join(path, "compile.fetch"))
true
else
false
end
end
defp check_unavailable!(app, scm, {:unavailable, path}) do
if scm.fetchable?() do
Mix.raise(
"Cannot compile dependency #{inspect(app)} because " <>
"it isn't available, run \"mix deps.get\" first"
)
else
Mix.raise(
"Cannot compile dependency #{inspect(app)} because " <>
"it isn't available, please ensure the dependency is at " <>
inspect(Path.relative_to_cwd(path))
)
end
end
defp check_unavailable!(_, _, _) do
:ok
end
defp do_mix(dep, _config) do
Mix.Dep.in_dependency(dep, fn _ ->
config = Mix.Project.config()
if req = old_elixir_req(config) do
Mix.shell().error(
"warning: the dependency #{inspect(dep.app)} requires Elixir #{inspect(req)} " <>
"but you are running on v#{System.version()}"
)
end
try do
options = [
"--from-mix-deps-compile",
"--no-warnings-as-errors",
"--no-code-path-pruning",
"--ignore-module-conflict"
]
reenable_tasks()
res =
Mix.Task.run("compile", options)
reenable_tasks()
match?({:ok, _}, res)
catch
kind, reason ->
app = dep.app
Mix.shell().error(
"could not compile dependency #{inspect(app)}, \"mix compile\" failed. " <>
deps_compile_feedback(app)
)
:erlang.raise(kind, reason, __STACKTRACE__)
end
end)
end
# this is pretty wild, but works for now
# we need to figure out a "blessed" way of doing
# what we're trying to do here :/
# enabling all of the compilers for a given project
# is (for some reason) resulting in the process
# simply existing with no information
defp reenable_tasks do
["compile.elixir", "compile", "compile.all"]
|> Enum.each(&Mix.Task.reenable/1)
end
defp do_rebar3(%Mix.Dep{opts: opts} = dep, config) do
dep_path = opts[:dest]
build_path = opts[:build]
File.mkdir_p!(build_path)
# For Rebar3, we need to copy the source/ebin to the target/ebin
# before we run the command given that REBAR_BARE_COMPILER_OUTPUT_DIR
# writes directly to _build.
File.cp_r(Path.join(dep_path, "ebin"), Path.join(build_path, "ebin"))
# Now establish symlinks to the remaining sources
for dir <- ~w(include priv src) do
Mix.Utils.symlink_or_copy(Path.join(dep_path, dir), Path.join(build_path, dir))
end
# Build the rebar config and setup the command line
config_path = Path.join(build_path, "mix.rebar.config")
lib_path = Path.join(config[:deps_build_path], "lib/*/ebin")
File.write!(config_path, rebar_config(dep))
env = [
# REBAR_BARE_COMPILER_OUTPUT_DIR is only honored by rebar3 >= 3.14
{"REBAR_BARE_COMPILER_OUTPUT_DIR", build_path},
{"REBAR_CONFIG", config_path},
{"REBAR_PROFILE", "prod"},
{"TERM", "dumb"}
]
cmd = "#{rebar_cmd(dep)} bare compile --paths #{escape_path(lib_path)}"
do_command(dep, config, cmd, false, env)
# Check if we have any new symlinks after compilation
for dir <- ~w(include priv src),
File.exists?(Path.join(dep_path, dir)) and not File.exists?(Path.join(build_path, dir)) do
Mix.Utils.symlink_or_copy(Path.join(dep_path, dir), Path.join(build_path, dir))
end
Code.prepend_path(Path.join(build_path, "ebin"), cache: true)
true
end
defp escape_path(path) do
escape = if match?({:win32, _}, :os.type()), do: "^ ", else: "\\ "
String.replace(path, " ", escape)
end
defp rebar_config(dep) do
dep.extra
|> Mix.Rebar.dependency_config()
|> Mix.Rebar.serialize_config()
end
defp rebar_cmd(%Mix.Dep{manager: manager} = dep) do
Mix.Rebar.rebar_cmd(manager) || handle_rebar_not_found(dep)
end
defp handle_rebar_not_found(%Mix.Dep{app: app, manager: manager}) do
shell = Mix.shell()
shell.info(
"Could not find \"#{manager}\", which is needed to build dependency #{inspect(app)}"
)
install_question =
"Shall I install #{manager}? (if running non-interactively, " <>
"use \"mix local.rebar --force\")"
unless shell.yes?(install_question) do
error_message =
"Could not find \"#{manager}\" to compile " <>
"dependency #{inspect(app)}, please ensure \"#{manager}\" is available"
Mix.raise(error_message)
end
Mix.Tasks.Local.Rebar.run(["--force"])
Mix.Rebar.local_rebar_cmd(manager) || Mix.raise("\"#{manager}\" installation failed")
end
defp do_make(dep, config) do
command = make_command(dep)
do_command(dep, config, command, true, [{"IS_DEP", "1"}])
build_structure(dep, config)
true
end
defp make_command(dep) do
makefile_win? = makefile_win?(dep)
command =
case :os.type() do
{:win32, _} when makefile_win? ->
"nmake /F Makefile.win"
{:unix, type} when type in [:dragonfly, :freebsd, :netbsd, :openbsd] ->
"gmake"
_ ->
"make"
end
if erlang_mk?(dep) do
"#{command} clean && #{command}"
else
command
end
end
defp do_compile(%Mix.Dep{opts: opts} = dep, config) do
if command = opts[:compile] do
do_command(dep, config, command, true)
build_structure(dep, config)
true
else
false
end
end
defp do_command(dep, config, command, print_app?, env \\ []) do
%Mix.Dep{app: app, system_env: system_env, opts: opts} = dep
env = [{"ERL_LIBS", Path.join(config[:deps_build_path], "lib")} | system_env] ++ env
if Mix.shell().cmd(command, env: env, print_app: print_app?, cd: opts[:dest]) != 0 do
Mix.raise(
"Could not compile dependency #{inspect(app)}, \"#{command}\" command failed. " <>
deps_compile_feedback(app)
)
end
true
end
defp build_structure(%Mix.Dep{opts: opts}, config) do
config = Keyword.put(config, :deps_app_path, opts[:build])
Mix.Project.build_structure(config, symlink_ebin: true, source: opts[:dest])
Code.prepend_path(Path.join(opts[:build], "ebin"), cache: true)
end
defp old_elixir_req(config) do
req = config[:elixir]
if req && not Version.match?(System.version(), req) do
req
end
end
defp erlang_mk?(%Mix.Dep{opts: opts}) do
File.regular?(Path.join(opts[:dest], "erlang.mk"))
end
defp makefile_win?(%Mix.Dep{opts: opts}) do
File.regular?(Path.join(opts[:dest], "Makefile.win"))
end
defp filter_available_and_local_deps(deps) do
Enum.filter(deps, fn dep ->
Mix.Dep.available?(dep) or not dep.scm.fetchable?()
end)
end
defp deps_compile_feedback(app) do
"Errors may have been logged above. You can recompile this dependency with " <>
"\"mix deps.compile #{app} --force\", update it with \"mix deps.update #{app}\" or " <>
"clean it with \"mix deps.clean #{app}\""
end
end