improvement: add positional_args!/1 macro for use in tasks

fix: recompile igniter in `ingiter.install`
This commit is contained in:
Zach Daniel 2024-07-26 13:30:41 -04:00
parent ef79a5c3ba
commit 29e492002a
6 changed files with 120 additions and 11 deletions

View file

@ -37,11 +37,13 @@ defmodule Igniter.Util.DepsCompile do
# * `--skip-umbrella-children` - skips umbrella applications from compiling
# * `--skip-local-deps` - skips non-remote dependencies, such as path deps, from compiling
def run do
def run(opts \\ []) do
Mix.Project.get!()
deps = Mix.Dep.load_and_cache()
opts = [include_children: true, force: true]
opts =
[include_children: true, force: true]
|> Keyword.put(:recompile_igniter?, Keyword.get(opts, :recompile_igniter?))
compile(filter_available_and_local_deps(deps), opts)
end
@ -53,7 +55,11 @@ defmodule Igniter.Util.DepsCompile do
Mix.Task.run("deps.precompile")
igniter_needs_compiling? =
not Code.ensure_loaded?(Igniter)
if options[:recompile_igniter?] do
true
else
not Code.ensure_loaded?(Igniter)
end
compiled =
deps

View file

@ -53,8 +53,8 @@ defmodule Igniter.Project.Deps do
Mix.shell().yes?("""
Dependency #{name} is already in mix.exs. Should we replace it?
Desired: `#{inspect desired}`
Found: `#{inspect current}`
Desired: `#{inspect(desired)}`
Found: `#{inspect(current)}`
""") do
igniter
|> remove_dependency(name)

View file

@ -27,7 +27,7 @@ defmodule Igniter.Util.Info do
{igniter, Enum.map(Enum.uniq(acc), &"#{&1}.install"), validate!(argv, schema, task_name)}
installs ->
schema = %{schema | installs: []}
schema = %{schema | installs: []}
install_names = Keyword.keys(installs)
igniter

View file

@ -221,7 +221,7 @@ defmodule Igniter.Util.Install do
|> File.read!()
|> Code.eval_string([], file: Path.expand("mix.exs"))
Igniter.Util.DepsCompile.run()
Igniter.Util.DepsCompile.run(recompile_igniter?: true)
exit_code ->
Mix.shell().info("""

View file

@ -16,13 +16,22 @@ defmodule Igniter.Mix.Task do
## Configurable Keys
* `schema` - The option schema for this task, in the format given to `OptionParser`, i.e `[name: :string]`
* `positional` - A list of positional arguments that this task accepts. A list of atoms, or a keyword list with the option and config.
See the positional arguments section for more.
* `aliases` - A map of aliases to the schema keys.
* `composes` - A list of tasks that this task might compose.
* `installs` - A list of dependencies that should be installed before continuing.
* `adds_deps` - A list of dependencies that should be added to the `mix.exs`, but do not need to be installed before continuing.
* `extra_args?` - Whether or not to allow extra arguments. This forces all tasks that compose this task to allow extra args as well.
* `example` - An example usage of the task. This is used in the help output.
Your task should *always* use `switches` and not `strict` to validate provided options!
## Positonal Arguments
Each positional argument can provide the following options:
* `:optional` - Whether or not the argument is optional. Defaults to `false`.
"""
@global_options [
@ -40,14 +49,18 @@ defmodule Igniter.Mix.Task do
composes: [],
installs: [],
adds_deps: [],
positional: [],
example: nil,
extra_args?: false
@type t :: %__MODULE__{
schema: Keyword.t(),
aliases: Keyword.t(),
composes: [String.t()],
positional: list(atom | {atom, [{:optional, boolean()}, {:example, String.t()}]}),
installs: [{atom(), String.t()}],
adds_deps: [{atom(), String.t()}],
example: String.t() | nil,
extra_args?: boolean()
}
@ -59,6 +72,9 @@ defmodule Igniter.Mix.Task do
This info will be used to validate arguments in composed tasks.
Use the `positional_args!(argv)` to get your positional arguments according to your `info.positional`, and the remaining unused args.
Use the `options!(argv)` macro to get your parsed options according to your `info.schema`.
## Important Limitations
* Each task still must parse its own argv in `igniter/2` and *must* ignore any unknown options.
@ -74,7 +90,7 @@ defmodule Igniter.Mix.Task do
quote do
use Mix.Task
@behaviour Igniter.Mix.Task
import Igniter.Mix.Task, only: [options!: 1]
import Igniter.Mix.Task, only: [options!: 1, positional_args!: 1]
@impl Mix.Task
def run(argv) do
@ -122,14 +138,97 @@ defmodule Igniter.Mix.Task do
end
end
@doc "Parses the options for the task based on its info."
@spec options!(argv :: term()) :: term() | no_return
defmacro options!(argv) do
quote do
argv = unquote(argv)
info = info(argv, Mix.Task.task_name(__MODULE__))
{parsed, _} = OptionParser.parse!(argv, switches: info.schema, aliases: info.aliases)
task_name = Mix.Task.task_name(__MODULE__)
info = info(argv, task_name)
{parsed, _} = OptionParser.parse!(argv, switches: info.schema, aliases: info.aliases)
parsed
end
end
defmacro positional_args!(argv) do
quote do
argv = unquote(argv)
task_name = Mix.Task.task_name(__MODULE__)
info = info(argv, task_name)
desired =
Enum.map(info.positional, fn
value when is_atom(value) ->
{value, []}
other ->
other
end)
{rest, positional} = Enum.split_with(argv, &String.starts_with?(&1, "-"))
{remaining_desired, got} =
Enum.reduce(positional, {desired, []}, fn
_arg, {[], got} ->
{[], got}
arg, {desired, got} ->
{name, _config} =
Enum.find(desired, fn {_name, config} ->
!config[:optional]
end) || Enum.at(desired, 0)
{Keyword.delete(desired, name), Keyword.put(got, name, arg)}
end)
case Enum.find(remaining_desired, fn {_arg, config} -> !config[:optional] end) do
{name, _config} ->
raise ArgumentError, """
Required positional argument `#{name}` was not supplied.
Command: `#{Igniter.Mix.Task.call_structure(task_name, desired)}`
#{Igniter.Mix.Task.call_example(info)}
Run `mix help #{task_name}` for more information.
"""
_ ->
{Map.new(got), rest}
end
end
end
@doc false
def call_example(info) do
if info.example do
"""
Example:
#{indent(info.example)}
"""
end
end
defp indent(example) do
example
|> String.split("\n")
|> Enum.map_join("\n", &" #{&1}")
end
@doc false
def call_structure(name, desired) do
call =
Enum.map_join(desired, " ", fn {name, config} ->
if config[:optional] do
"[#{name}]"
else
name
end
end)
"mix #{name} #{call}"
end
end

View file

@ -30,7 +30,11 @@ defmodule Igniter.Mix.Tasks.InstallTest do
test "rerunning the same installer lets you know the dependency was not changed" do
_ = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
output = cmd!("mix", ["igniter.install", "jason", "--yes"], cd: "test_project")
assert String.contains?(output, "Dependency jason is already in mix.exs with the desired version. Skipping.")
assert String.contains?(
output,
"Dependency jason is already in mix.exs with the desired version. Skipping."
)
end
end