fix: Capture IO automatically on execution
This commit is contained in:
parent
7170010c6b
commit
65a976b54f
5 changed files with 46 additions and 34 deletions
13
README.md
13
README.md
|
@ -13,7 +13,7 @@ Add mash to your dependencies like so:
|
||||||
```elixir
|
```elixir
|
||||||
def deps do
|
def deps do
|
||||||
[
|
[
|
||||||
{:mash, "~> 0.1.0"}
|
{:mash, "~> 0.1.1"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
@ -36,6 +36,12 @@ defmodule MashConfig do
|
||||||
name: :credo,
|
name: :credo,
|
||||||
run: mix("credo", ["--all"])
|
run: mix("credo", ["--all"])
|
||||||
},
|
},
|
||||||
|
%{
|
||||||
|
name: :function_example,
|
||||||
|
run: fn _io_pid ->
|
||||||
|
IO.puts("Hurray!")
|
||||||
|
end
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -47,7 +53,10 @@ Each entry in the jobs list must, at a minimum, have two keys:
|
||||||
- `:run` -- a function which defines the work for the job to do. We recommend using the helper functions from Mash.Helpers to define your run function, as they ensure that your function handles setup and teardown correctly for many common scenarios, e.g.:
|
- `:run` -- a function which defines the work for the job to do. We recommend using the helper functions from Mash.Helpers to define your run function, as they ensure that your function handles setup and teardown correctly for many common scenarios, e.g.:
|
||||||
- `Mash.Helpers.mix(task_name, args)` -- execute a mix task with the given `task_name` and `args`.
|
- `Mash.Helpers.mix(task_name, args)` -- execute a mix task with the given `task_name` and `args`.
|
||||||
- `Mash.Helpers.shell(command, args)` -- run an executable/script named `command` with `args`.
|
- `Mash.Helpers.shell(command, args)` -- run an executable/script named `command` with `args`.
|
||||||
- `Mash.Helpers.fun(fn -> ... end)` -- execute the Elixir function given. This function must return `:ok` or `{:ok, value}` to indicate success, or return `{:error, error}` or `raise` to indicate job failure.
|
- Or you can pass a function with arity 1.
|
||||||
|
- The function should return `:ok` or `{:ok, value}` if the job succeeds, or `{:error, error}` if the job fails.
|
||||||
|
- The argument passed to your function will be the PID of the IO device which is capturing IO. Since we make this PID the "group leader" for your job automatically, you can probably ignore this argument, but you may need to for interfacing with certain libraries that
|
||||||
|
require you to pass an io device PID directly.
|
||||||
|
|
||||||
Optionally, a job may also contain a key `needs` whose value is an array of other jobs that must pass before this job will be run.
|
Optionally, a job may also contain a key `needs` whose value is an array of other jobs that must pass before this job will be run.
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ defmodule Mash.Execution do
|
||||||
|
|
||||||
task =
|
task =
|
||||||
Task.async(fn ->
|
Task.async(fn ->
|
||||||
|
Process.group_leader(self(), io_pid)
|
||||||
|
|
||||||
{%{job | io_pid: io_pid}, run.(io_pid)}
|
{%{job | io_pid: io_pid}, run.(io_pid)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
@ -3,30 +3,48 @@ defmodule Mash.Helpers do
|
||||||
Helpers that provide a simple way to write Mash configs.
|
Helpers that provide a simple way to write Mash configs.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def mix(task, args \\ []) do
|
@doc "Create a run function for a job that executes a mix task with name `task_name` and the arguments `args`"
|
||||||
preferred_mix_env = Mix.Task.preferred_cli_env(task) || Mix.env()
|
@spec mix(String.t(), [String.t()]) :: function()
|
||||||
|
def mix(task_name, args \\ []) do
|
||||||
|
preferred_mix_env = Mix.Task.preferred_cli_env(task_name) || Mix.env()
|
||||||
|
|
||||||
fn io_pid ->
|
fn io_pid ->
|
||||||
cmd("mix", [task | args], io_pid, [{"MIX_ENV", Atom.to_string(preferred_mix_env)}])
|
cmd("mix", [task_name | args], io_pid, [{"MIX_ENV", Atom.to_string(preferred_mix_env)}])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Create a run function which executes the script/binary named `command` with the arguments `args`.
|
||||||
|
"""
|
||||||
|
@spec shell(String.t(), [String.t()]) :: function()
|
||||||
def shell(command, args \\ []) do
|
def shell(command, args \\ []) do
|
||||||
fn io_pid ->
|
fn io_pid ->
|
||||||
cmd(command, args, io_pid)
|
cmd(command, args, io_pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Save a cache file as a gzipped tar archive. We use tar because it preserves timestamps which are used
|
||||||
|
by Elixir for determining "staleness" of compilation.
|
||||||
|
|
||||||
|
- `name` is the base name of the archive to save (default: `".mash-cache"`)
|
||||||
|
- `files` is the list of files/directories to save in the archive (default:` ["deps", "_build"]`)
|
||||||
|
"""
|
||||||
|
@spec save_cache(String.t(), [String.t()]) :: function()
|
||||||
def save_cache(name \\ ".mash-cache", files \\ ["deps", "_build"]) do
|
def save_cache(name \\ ".mash-cache", files \\ ["deps", "_build"]) do
|
||||||
fn io_pid ->
|
fn io_pid ->
|
||||||
cmd("tar", ["-czpf", "#{name}.tar.gz" | files], io_pid)
|
cmd("tar", ["-czpf", "#{name}.tar.gz" | files], io_pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Restore a cache file.
|
||||||
|
|
||||||
|
- `name` is the base name of the archive to save (default: ".mash-cache")
|
||||||
|
"""
|
||||||
|
@spec restore_cache(String.t()) :: function()
|
||||||
def restore_cache(name \\ ".mash-cache") do
|
def restore_cache(name \\ ".mash-cache") do
|
||||||
fn io_pid ->
|
fn io_pid ->
|
||||||
Process.group_leader(self(), io_pid)
|
|
||||||
|
|
||||||
path = "#{name}.tar.gz"
|
path = "#{name}.tar.gz"
|
||||||
|
|
||||||
if File.exists?(path) do
|
if File.exists?(path) do
|
||||||
|
@ -41,30 +59,6 @@ defmodule Mash.Helpers do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def fun(fun) do
|
|
||||||
fn io_pid ->
|
|
||||||
Process.group_leader(self(), io_pid)
|
|
||||||
|
|
||||||
try do
|
|
||||||
case fun.() do
|
|
||||||
:ok ->
|
|
||||||
{[], 0}
|
|
||||||
|
|
||||||
{:ok, _any} ->
|
|
||||||
{[], 0}
|
|
||||||
|
|
||||||
other ->
|
|
||||||
IO.puts(IO.ANSI.red() <> "** (fun) failed: #{inspect(other)}" <> IO.ANSI.reset())
|
|
||||||
{[], 1}
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
e ->
|
|
||||||
IO.puts(IO.ANSI.red() <> "** (fun) failed: #{Exception.message(e)}" <> IO.ANSI.reset())
|
|
||||||
{[], 1}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp cmd(command, args, io_pid, env \\ []) do
|
defp cmd(command, args, io_pid, env \\ []) do
|
||||||
exec_string = "-c #{command} #{Enum.join(args, " ")}"
|
exec_string = "-c #{command} #{Enum.join(args, " ")}"
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Mash.Runner do
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
@type job_state :: :to_run | :running | :passed | :failed | :skipped
|
@type job_state :: :to_run | :running | :passed | :failed | :skipped
|
||||||
|
@type result :: term()
|
||||||
|
|
||||||
def tick do
|
def tick do
|
||||||
GenServer.cast(self(), :tick)
|
GenServer.cast(self(), :tick)
|
||||||
|
@ -107,12 +108,18 @@ defmodule Mash.Runner do
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp interpret_result({_stream_data, 0}), do: :passed
|
||||||
|
defp interpret_result({_stream_data, exit_code}) when exit_code > 0, do: :failed
|
||||||
|
defp interpret_result(:ok), do: :passed
|
||||||
|
defp interpret_result({:ok, _}), do: :passed
|
||||||
|
defp interpret_result(_), do: :failed
|
||||||
|
|
||||||
@impl GenServer
|
@impl GenServer
|
||||||
def handle_info({_ref, {job, {_io_stream, exit_code}}}, {parent, jobs}) do
|
def handle_info({_ref, {job, result}}, {parent, jobs}) do
|
||||||
%{name: job_name} = job
|
%{name: job_name} = job
|
||||||
|
|
||||||
StringIO.close(job.io_pid)
|
StringIO.close(job.io_pid)
|
||||||
new_state = if exit_code == 0, do: :passed, else: :failed
|
new_state = interpret_result(result)
|
||||||
IO.puts("Finishing #{job_name} -- #{new_state}")
|
IO.puts("Finishing #{job_name} -- #{new_state}")
|
||||||
|
|
||||||
new_job = %{
|
new_job = %{
|
||||||
|
|
2
mix.exs
2
mix.exs
|
@ -4,7 +4,7 @@ defmodule Mash.MixProject do
|
||||||
def project do
|
def project do
|
||||||
[
|
[
|
||||||
app: :mash,
|
app: :mash,
|
||||||
version: "0.1.0",
|
version: "0.1.1",
|
||||||
elixir: "~> 1.14",
|
elixir: "~> 1.14",
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
|
|
Loading…
Reference in a new issue