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 ```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.

View file

@ -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)

View file

@ -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, " ")}"

View file

@ -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 = %{

View file

@ -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(),