From 65a976b54f59ad6c9748310c07441d5905c128fb Mon Sep 17 00:00:00 2001 From: Robert Prehn <3952444+prehnRA@users.noreply.github.com> Date: Thu, 10 Aug 2023 10:29:27 +0000 Subject: [PATCH] fix: Capture IO automatically on execution --- README.md | 13 +++++++++-- lib/mash/execution.ex | 2 ++ lib/mash/helpers.ex | 52 +++++++++++++++++++------------------------ lib/mash/runner.ex | 11 +++++++-- mix.exs | 2 +- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index b46f19a..50c99cd 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/lib/mash/execution.ex b/lib/mash/execution.ex index 49853aa..c51f919 100644 --- a/lib/mash/execution.ex +++ b/lib/mash/execution.ex @@ -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) diff --git a/lib/mash/helpers.ex b/lib/mash/helpers.ex index 2397538..437424a 100644 --- a/lib/mash/helpers.ex +++ b/lib/mash/helpers.ex @@ -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, " ")}" diff --git a/lib/mash/runner.ex b/lib/mash/runner.ex index dd45d94..2de7d81 100644 --- a/lib/mash/runner.ex +++ b/lib/mash/runner.ex @@ -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 = %{ diff --git a/mix.exs b/mix.exs index 4f32747..f926947 100644 --- a/mix.exs +++ b/mix.exs @@ -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(),