Skip to content

Setup Igniter Tasks #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions lib/mix/tasks/vintage_net_wifi.add_network.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# SPDX-FileCopyrightText: 2025 Jonatan Männchen <[email protected]>
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Mix.Tasks.VintageNetWifi.AddNetwork.Docs do
@moduledoc false

@spec short_doc() :: String.t()
def short_doc() do
"Add network configuration to VintageNet wifi interface."
end

@spec example() :: String.t()
def example() do
~S"""
mix vintage_net_wifi.add_network \
wlan0 \
--ssid "MySSID" \
--key-mgmt "wpa_psk" \
--psk "MySecretPassword"
"""
end

@spec long_doc() :: String.t()
def long_doc() do
"""
#{short_doc()}

## Example

```bash
#{example()}
```

## Options

* `--ssid` or `-s` - The SSID of the network to add.
* `--key-mgmt` or `-m` - The key management type to use.
Currently only `wpa_psk` and `none` are supported.
* `--psk` or `-p` - The pre-shared key to use for the network.
"""
end
end

if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.VintageNetWifi.AddNetwork do
@shortdoc "#{__MODULE__.Docs.short_doc()}"

@moduledoc __MODULE__.Docs.long_doc()

use Igniter.Mix.Task

alias Igniter.Code.Common
alias Igniter.Project.Config

@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
# Groups allow for overlapping arguments for tasks by the same author
# See the generators guide for more.
group: :vintage_net_wifi,
# *other* dependencies to add
# i.e `{:foo, "~> 2.0"}`
adds_deps: [],
# *other* dependencies to add and call their associated installers, if they exist
# i.e `{:foo, "~> 2.0"}`
installs: [],
# An example invocation
example: __MODULE__.Docs.example(),
# a list of positional arguments, i.e `[:file]`
positional: [
:interface
],
# Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
# This ensures your option schema includes options from nested tasks
composes: [
"vintage_net.install"
],
# `OptionParser` schema
schema: [
ssid: :string,
key_mgmt: :string,
psk: :string
],
# Default values for the options in the `schema`
defaults: [],
# CLI aliases
aliases: [
s: :ssid,
m: :key_mgmt
],
# A list of options in the schema that are required
required: [
:ssid,
:key_mgmt
]
}
end

@impl Igniter.Mix.Task
def igniter(igniter) do
igniter
|> Igniter.compose_task(
"vintage_net_wifi.install",
igniter.args.argv ++ ["--interface", igniter.args.positional.interface]
)
|> add_interface(igniter.args.positional.interface, igniter.args.options)
end

@spec add_interface(igniter :: Igniter.t(), interface :: String.t(), options :: Keyword.t()) ::
Igniter.t()
defp add_interface(igniter, interface, options) do
options = options_ast(options)

Config.configure(
igniter,
"target.exs",
:vintage_net,
[:config],
:unused_is_aways_an_update,
updater: fn zipper ->
{:ok, zipper} =
Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
with true <- Igniter.Code.Tuple.tuple?(zipper),
{:ok, first} <- Igniter.Code.Tuple.tuple_elem(zipper, 0) do
Common.nodes_equal?(first, interface)
else
_ ->
false
end
end)

{:ok, zipper} = Igniter.Code.Tuple.tuple_elem(zipper, 1)

Igniter.Code.Map.set_map_key(
zipper,
:networks,
[options],
&Igniter.Code.List.append_to_list(&1, options)
)
end
)
end

@spec options_ast(options :: Keyword.t()) :: Macro.t()
defp options_ast(options) do
options
|> Keyword.take(~w(ssid key_mgmt psk)a)
|> Keyword.update!(:key_mgmt, fn
"wpa_psk" ->
:wpa_psk

"none" ->
:none

key_mgmt ->
Mix.raise("Invalid key_mgmt: #{key_mgmt}, only 'wpa_psk' and 'none' are supported.")
end)
|> Enum.sort()
|> Enum.map(fn {key, value} ->
{{:__block__, [format: :keyword], [key]}, {:__block__, [], [value]}}
end)
|> then(&{:%{}, [], &1})
end
end
else
defmodule Mix.Tasks.VintageNetWifi.AddNetwork do
@shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use"

@moduledoc __MODULE__.Docs.long_doc()

use Mix.Task

@impl Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'vintage_net_wifi.add_network' requires igniter. Please install igniter and try again.

For more information, see: https://hexdocs.pm/igniter/readme.html#installation
""")

exit({:shutdown, 1})
end
end
end
186 changes: 186 additions & 0 deletions lib/mix/tasks/vintage_net_wifi.install.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# SPDX-FileCopyrightText: 2025 Jonatan Männchen <[email protected]>
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Mix.Tasks.VintageNetWifi.Install.Docs do
@moduledoc false

@spec short_doc() :: String.t()
def short_doc() do
"Install & Setup VintageNetWiFi"
end

@spec example() :: String.t()
def example() do
~S"""
mix vintage_net_wifi.install \
--regulatory-domain 00"
"""
end

@spec long_doc() :: String.t()
def long_doc() do
"""
#{short_doc()}

## Example

```bash
#{example()}
```

## Options

* `--regulatory-domain` or `-r` - Regulatory domains to use for the wifi
module. This is a string of 2 characters, e.g. "US" for the United
States or "00" for the world. This is required for the wifi module
to work.
"""
end
end

if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.VintageNetWifi.Install do
@shortdoc "#{__MODULE__.Docs.short_doc()}"

@moduledoc __MODULE__.Docs.long_doc()

use Igniter.Mix.Task

alias Igniter.Project.Config

@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
# Groups allow for overlapping arguments for tasks by the same author
# See the generators guide for more.
group: :vintage_net_wifi,
# *other* dependencies to add
# i.e `{:foo, "~> 2.0"}`
adds_deps: [],
# *other* dependencies to add and call their associated installers, if they exist
# i.e `{:foo, "~> 2.0"}`
installs: [{:vintage_net, "~> 0.13"}],
# An example invocation
example: __MODULE__.Docs.example(),
# A list of environments that this should be installed in.
only: nil,
# a list of positional arguments, i.e `[:file]`
positional: [],
# Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
# This ensures your option schema includes options from nested tasks
composes: [],
# `OptionParser` schema
schema: [
interface: :string,
regulatory_domain: :string
],
# Default values for the options in the `schema`
defaults: [
interface: "wlan0"
],
# CLI aliases
aliases: [
i: :interface,
r: :regulatory_domain
],
# A list of options in the schema that are required
required: []
}
end

@impl Igniter.Mix.Task
def igniter(igniter) do
igniter
|> add_regulatory_domain(igniter.args.options)
|> add_wifi_interface(igniter.args.options[:interface])
end

@spec add_regulatory_domain(igniter :: Igniter.t(), options :: Keyword.t()) :: Igniter.t()
defp add_regulatory_domain(igniter, options) do
exists? =
Config.configures_key?(
igniter,
"target.exs",
:vintage_net,
[:regulatory_domain]
)

if exists? do
igniter
else
regulatory_domain =
options[:regulatory_domain] ||
Mix.shell().prompt("Regulatory domain (e.g. 'US' or '00')?")

Config.configure(
igniter,
"target.exs",
:vintage_net,
[:regulatory_domain],
regulatory_domain
)
end
end

@spec add_wifi_interface(igniter :: Igniter.t(), interface :: String.t()) :: Igniter.t()
defp add_wifi_interface(igniter, interface) do
Config.configure(
igniter,
"target.exs",
:vintage_net,
[:config],
{:code,
fix_ast(
quote do
[{unquote(interface), %{type: VintageNetWiFi}}]
end
)},
updater: fn config_zipper ->
type_config =
Sourceror.Zipper.find(
config_zipper,
&match?({{:__block__, _, [:type]}, {:__aliases__, _, [:VintageNetWiFi]}}, &1)
)

case type_config do
nil ->
Igniter.Code.List.append_to_list(
config_zipper,
fix_ast(
quote do
{unquote(interface), %{type: VintageNetWiFi}}
end
)
)

_ ->
{:ok, config_zipper}
end
end
)
end

@spec fix_ast(ast :: Macro.t()) :: Macro.t()
defp fix_ast(ast), do: ast |> Sourceror.to_string() |> Sourceror.parse_string!()
end
else
defmodule Mix.Tasks.VintageNetWifi.Install do
@shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use"

@moduledoc __MODULE__.Docs.long_doc()

use Mix.Task

@impl Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'vintage_net_wifi.install' requires igniter. Please install igniter and try again.

For more information, see: https://hexdocs.pm/igniter/readme.html#installation
""")

exit({:shutdown, 1})
end
end
end
6 changes: 5 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ defmodule VintageNetWiFi.MixProject do

defp deps do
[
# TODO: runtime: false (Dialyzer...)
# TODO: Use released igniter version
{:igniter, "~> 0.5", optional: true, runtime: true, github: "ash-project/igniter"},
{:vintage_net, "~> 0.12.0 or ~> 0.13.0"},
{:credo, "~> 1.2", only: :test, runtime: false},
{:credo_binary_patterns, "~> 0.2.2", only: :test, runtime: false},
Expand All @@ -82,7 +85,8 @@ defmodule VintageNetWiFi.MixProject do
defp dialyzer() do
[
flags: [:missing_return, :extra_return, :unmatched_returns, :error_handling, :underspecs],
plt_file: {:no_warn, "_build/plts/dialyzer.plt"}
plt_file: {:no_warn, "_build/plts/dialyzer.plt"},
plt_add_apps: [:igniter, :sourceror, :mix]
]
end

Expand Down
Loading