fix: Capture IO automatically on execution

This commit is contained in:
Robert Prehn 2023-08-10 10:29:27 +00:00
parent 7170010c6b
commit 65a976b54f
No known key found for this signature in database
5 changed files with 46 additions and 34 deletions

View file

@ -13,7 +13,7 @@ Add mash to your dependencies like so:
```elixir
def deps do
[
{:mash, "~> 0.1.0"}
{:mash, "~> 0.1.1"}
]
end
```
@ -36,6 +36,12 @@ defmodule MashConfig do
name: :credo,
run: mix("credo", ["--all"])
},
%{
name: :function_example,
run: fn _io_pid ->
IO.puts("Hurray!")
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.:
- `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.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.

View file

@ -8,6 +8,8 @@ defmodule Mash.Execution do
task =
Task.async(fn ->
Process.group_leader(self(), io_pid)
{%{job | io_pid: io_pid}, run.(io_pid)}
end)

View file

@ -3,30 +3,48 @@ defmodule Mash.Helpers do
Helpers that provide a simple way to write Mash configs.
"""
def mix(task, args \\ []) do
preferred_mix_env = Mix.Task.preferred_cli_env(task) || Mix.env()
@doc "Create a run function for a job that executes a mix task with name `task_name` and the arguments `args`"
@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 ->
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
@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
fn io_pid ->
cmd(command, args, io_pid)
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
fn io_pid ->
cmd("tar", ["-czpf", "#{name}.tar.gz" | files], io_pid)
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
fn io_pid ->
Process.group_leader(self(), io_pid)
path = "#{name}.tar.gz"
if File.exists?(path) do
@ -41,30 +59,6 @@ defmodule Mash.Helpers do
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
exec_string = "-c #{command} #{Enum.join(args, " ")}"

View file

@ -5,6 +5,7 @@ defmodule Mash.Runner do
use GenServer
@type job_state :: :to_run | :running | :passed | :failed | :skipped
@type result :: term()
def tick do
GenServer.cast(self(), :tick)
@ -107,12 +108,18 @@ defmodule Mash.Runner do
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
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
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}")
new_job = %{

View file

@ -4,7 +4,7 @@ defmodule Mash.MixProject do
def project do
[
app: :mash,
version: "0.1.0",
version: "0.1.1",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps(),