Skip to content

Add a :make compiler #4134

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions lib/mix/lib/mix/tasks/compile.make.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
defmodule Mix.Tasks.Compile.Make do
use Mix.Task

@shortdoc "Runs `make` in the current project"

@moduledoc """
Runs `make` in the current project.

This task runs `make` in the current project; any output coming from `make` is
printed in real-time on stdout. `make` will be called without specifying a
Makefile, so there has to be a `Makefile` in the current working directory.

## Configuration

* `:make_executable` - it's a binary. It's the executable to use as the
`make` program. By default, it's `"nmake"` on Windows, `"gmake"` on
FreeBSD and OpenBSD, and `"make"` on everything else.

* `:make_makefile` - it's a binary. It's the Makefile to use. Defaults to
`"Makefile"` for Unix systems and `"Makefile.win"` for Windows systems.

* `:make_targets` - it's a list of binaries. It's the list of Make targets
that should be run. Defaults to `[]`, meaning `make` will run the first
target.

* `:make_cwd` - it's a binary. It's the directory where `make` will be run,
relative to the root of the project.

* `:make_error_message` - it's a binary. It's a custom error message that
can be used to give instructions as of how to fix the error (e.g., it can
be used to suggest installing `gcc` if you're compiling a C dependency).

"""

@spec run(OptionParser.argv) :: :ok | no_return
def run(_args) do
config = Mix.Project.config()
build(config)
Mix.Project.build_structure
:ok
end

defp build(config) do
exec = Keyword.get(config, :make_executable, executable_for_current_os())
makefile = Keyword.get(config, :make_makefile, :default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also expose make_executable? The default is the OS lookup we perform. :)

targets = Keyword.get(config, :make_targets, [])
cwd = Keyword.get(config, :make_cwd, ".")
error_msg = Keyword.get(config, :make_error_message, nil)

args = args_for_makefile(exec, makefile) ++ targets

case cmd(exec, args, cwd) do
0 ->
:ok
exit_status ->
raise_build_error(exec, exit_status, error_msg)
end
end

# Runs `exec [args]` in `cwd` and prints the stdout and stderr in real time,
# as soon as `exec` prints them (using `IO.Stream`).
defp cmd(exec, args, cwd) do
opts = [into: IO.stream(:stdio, :line),
stderr_to_stdout: true,
cd: cwd]

{%IO.Stream{}, status} = System.cmd(executable(exec), args, opts)
status
end

defp executable(exec) do
System.find_executable(exec) || raise_executable_not_found(exec)
end

defp raise_executable_not_found(exec) do
Mix.raise "`#{exec}` not found in the current path"
end

defp raise_build_error(exec, exit_status, error_msg) do
msg = "Could not compile with `#{exec}`"

if error_msg do
msg = msg <> ".\n" <> error_msg
end

Mix.raise msg
end

defp executable_for_current_os() do
case :os.type() do
{:win32, _} -> "nmake"
{:unix, type} when type in [:freebsd, :openbsd] -> "gmake"
_ -> "make"
end
end

# Returns a list of command-line args to pass to make (or nmake/gmake) in
# order to specify the makefile to use.
defp args_for_makefile("nmake", :default), do: ["/F", "Makefile.win"]
defp args_for_makefile("nmake", makefile), do: ["/F", makefile]
defp args_for_makefile(_, :default), do: []
defp args_for_makefile(_, makefile), do: ["-f", makefile]
end
Empty file.
109 changes: 109 additions & 0 deletions lib/mix/test/mix/tasks/compile.make_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
Code.require_file "../../test_helper.exs", __DIR__

defmodule Mix.Tasks.Compile.MakeTest do
use MixTest.Case
import ExUnit.CaptureIO

setup do
Mix.Project.push MixTest.Case.Sample
:ok
end

test "running with a specific executable" do
in_fixture "compile_make", fn ->
with_project_config [make_executable: "nonexistentmake"], fn ->
assert_raise Mix.Error, "`nonexistentmake` not found in the current path", fn ->
run()
end
end
end
end

test "running without a makefile" do
msg = ~r/\ACould not compile with/

in_fixture "compile_make", fn ->
File.rm_rf!("Makefile")

capture_io fn ->
assert_raise Mix.Error, msg, fn -> run() end
end
end
end

test "running with a makefile" do
in_fixture "compile_make", fn ->
File.write! "Makefile", """
target:
\t@echo "hello"
"""

assert capture_io(fn -> run() end) =~ "hello\n"
end
end

test "specifying targets" do
in_fixture "compile_make", fn ->
File.write! "Makefile", """
useless_target:
\t@echo "nope"
target:
\t@echo "target"
other_target:
\t@echo "other target"
"""

with_project_config [make_targets: ~w(target other_target)], fn ->
output = capture_io(fn -> run() end)
assert output =~ "target\n"
assert output =~ "other target\n"
refute output =~ "nope"
end
end
end

test "specifying a cwd" do
in_fixture "compile_make", fn ->
File.mkdir_p!("subdir")
File.write! "subdir/Makefile", """
all:
\t@echo "subdir"
"""

with_project_config [make_cwd: "subdir"], fn ->
assert capture_io(fn -> run() end) == "subdir\n"
end
end
end

test "specifying a makefile" do
in_fixture "compile_make", fn ->
File.write "MyMakefile", """
all:
\t@echo "my makefile"
"""

with_project_config [make_makefile: "MyMakefile"], fn ->
assert capture_io(fn -> run() end) == "my makefile\n"
end
end
end

test "specifying a custom error message" do
in_fixture "compile_make", fn ->
with_project_config [make_error_message: "try harder"], fn ->
capture_io fn ->
assert_raise Mix.Error, ~r/try harder/, fn -> run() end
end
end
end
end

defp with_project_config(config, fun) do
Mix.Project.in_project(:sample, ".", config, fn(_) -> fun.() end)
end

defp run(args \\ []) do
Mix.Tasks.Compile.Make.run(args)
end
end