diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e505c2..bd711c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,3 +66,51 @@ jobs: - name: Run tests run: mix test --warnings-as-errors + + generated-project: + name: Generate and deploy a grisp_new project + runs-on: ubuntu-24.04 + + steps: + - name: Check out repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Install Erlang and Elixir + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + otp-version: "29" + elixir-version: "1.20" + + - name: Restore GRiSP OTP package cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ~/.cache/grisp + key: ${{ runner.os }}-grisp2-otp-29 + + - name: Install Hex, Rebar, and grisp_new + run: | + mix local.hex --force + mix local.rebar --force + mix archive.install hex grisp_new 1.0.0 --force + + - name: Generate project + run: mix grisp.new --no-interactive --name ci_grisp_project --otp-version 29 + + - name: Use the checked-out mix_grisp plugin + run: | + sed -i 's|{:mix_grisp, "~> 1.0", runtime: false}|{:mix_grisp, path: "..", runtime: false}|' ci_grisp_project/mix.exs + grep -Fq '{:mix_grisp, path: "..", runtime: false}' ci_grisp_project/mix.exs + + - name: Fetch generated project dependencies + working-directory: ci_grisp_project + run: mix deps.get + + - name: Build deployment bundle + working-directory: ci_grisp_project + run: mix grisp.deploy --tar + + - name: Verify deployment bundle + working-directory: ci_grisp_project + run: test -s _grisp/deploy/grisp2.ci_grisp_project.0.1.0.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md index f746b4b..ff6e66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,9 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Port the `rebar3_grisp` workflow to Mix with tasks for building custom OTP systems, deploying releases, generating firmware, creating software update packages, listing pre-built packages, and collecting bug reports. -- Add `mix grisp.configure` to create GRiSP-ready Elixir projects, including - release, Ethernet, optional Wi-Fi, distributed Erlang, and GRiSP.io - configuration. Existing project files are preserved in interactive mode. +- Add support for projects created by the separately distributed `grisp_new` + archive, including release, Ethernet, optional Wi-Fi, distributed Erlang, + and GRiSP.io configuration. - Add release selection, archive deployment, custom destinations, pre- and post-deployment scripts, and forwarding of options to `mix release`. - Add support for local and Docker GRiSP toolchains, custom OTP builds, and diff --git a/README.md b/README.md index 2a304a1..59532b5 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,24 @@ mix grisp.version ## Create a new application -The configure task creates a supervised Mix application with release and GRiSP -configuration, plus optional networking files: +Install the dependency-free `grisp_new` archive to make the project generator +available globally: ```console -mix grisp.configure +mix archive.install hex grisp_new +``` + +Its configure task creates a supervised Mix application with release and +GRiSP configuration, plus optional networking files: + +```console +mix grisp.new ``` For non-interactive use: ```console -mix grisp.configure --no-interactive --name my_grisp_app \ +mix grisp.new --no-interactive --name my_grisp_app \ --ssid mywifi --psk wifipsk ``` diff --git a/lib/mix/tasks/grisp/configure.ex b/lib/mix/tasks/grisp/configure.ex deleted file mode 100644 index 671ca1a..0000000 --- a/lib/mix/tasks/grisp/configure.ex +++ /dev/null @@ -1,69 +0,0 @@ -defmodule Mix.Tasks.Grisp.Configure do - use Mix.Task - - @shortdoc "Creates and configures a new GRiSP Mix application" - @moduledoc """ - Creates a new Mix project configured for GRiSP. - - mix grisp.configure [options] - - Interactive mode is enabled by default. If the target directory exists, - confirmation is required and existing files are preserved. Use - `--no-interactive` for scripts. - """ - - @switches [ - interactive: :boolean, - name: :string, - otp_version: :string, - jit: :string, - dest: :string, - desc: :string, - copyright_year: :string, - author_name: :string, - author_email: :string, - wifi: :boolean, - ssid: :string, - psk: :string, - grisp_io: :boolean, - grisp_io_linking: :string, - epmd: :boolean, - cookie: :string - ] - - @aliases [ - i: :interactive, - o: :otp_version, - d: :dest, - w: :wifi, - g: :grisp_io, - l: :grisp_io_linking, - e: :epmd, - c: :cookie - ] - - @impl Mix.Task - def run(args) do - {options, []} = MixGrisp.CLI.parse!(args, @switches, @aliases) - options = parse_jit(options) - result = MixGrisp.Configure.run(options) - Enum.each(result.created, &Mix.shell().info("Created #{&1}")) - Mix.shell().info("Configured GRiSP Mix project #{result.name}") - end - - defp parse_jit(options) do - case Keyword.fetch(options, :jit) do - {:ok, "true"} -> - Keyword.put(options, :jit, true) - - {:ok, "false"} -> - Keyword.put(options, :jit, false) - - {:ok, value} -> - Mix.raise("Invalid value for --jit: #{inspect(value)} (expected true or false)") - - :error -> - options - end - end -end diff --git a/lib/mix_grisp/configure.ex b/lib/mix_grisp/configure.ex deleted file mode 100644 index f1a5d95..0000000 --- a/lib/mix_grisp/configure.ex +++ /dev/null @@ -1,428 +0,0 @@ -defmodule MixGrisp.Configure do - @moduledoc false - - @defaults [ - interactive: true, - name: "robot", - otp_version: "29", - jit: true, - dest: "/path/to/SD-card", - desc: "A GRiSP application", - copyright_year: nil, - author_name: "Anonymous", - author_email: "anonymous@example.org", - wifi: false, - grisp_io: false, - grisp_io_linking: nil, - epmd: false, - cookie: "grisp" - ] - - def run(options) do - options = infer_options(options) - supplied = options |> Keyword.keys() |> MapSet.new() - - config = - options - |> Keyword.merge(@defaults, fn _key, supplied, _default -> supplied end) - |> Keyword.update!(:copyright_year, &(&1 || Integer.to_string(Date.utc_today().year))) - |> prompt(supplied) - - validate!(config) - root = Path.expand(config[:name]) - project_exists? = File.exists?(root) - - if project_exists? do - confirm_existing!(root, config[:interactive]) - else - Mix.Task.reenable("new") - Mix.Tasks.New.run([root, "--app", config[:name], "--sup"]) - end - - created = - [ - write_elixir(Path.join(root, "mix.exs"), mix_exs(config), project_exists?), - write_elixir( - Path.join(root, "config/config.exs"), - app_config(config), - project_exists? - ), - write(Path.join(root, "README.md"), project_readme(config), project_exists?), - write(Path.join(root, "LICENSE"), license(config), project_exists?) - ] ++ network_files(root, config, project_exists?) - - created = Enum.reject(created, &is_nil/1) - - if project_exists? do - Mix.shell().info( - "The project directory already existed; existing files were not overwritten. " <> - "Review the project configuration before continuing." - ) - end - - %{name: config[:name], root: root, config: config, created: created} - end - - defp prompt(config, supplied) do - if config[:interactive] do - config - |> ask_valid_name_unless_supplied(supplied) - |> ask_unless_supplied(supplied, :otp_version, "Erlang/OTP version", &identity/1) - |> ask_bool_unless_supplied(supplied, :jit, "Enable the Arm32 JIT patches?") - |> ask_unless_supplied(supplied, :dest, "SD card path", &identity/1) - |> prompt_features(supplied) - else - config - end - end - - defp prompt_features(config, supplied) do - config - |> ask_bool_unless_supplied(supplied, :wifi, "Use Wi-Fi?") - |> prompt_wifi(supplied) - |> ask_bool_unless_supplied(supplied, :grisp_io, "Enable GRiSP.io integration?") - |> prompt_grisp_io(supplied) - |> ask_bool_unless_supplied(supplied, :epmd, "Enable distributed Erlang?") - |> prompt_epmd(supplied) - end - - defp prompt_wifi(config, supplied) do - if config[:wifi] do - config - |> ask_unless_supplied(supplied, :ssid, "Wi-Fi SSID", &identity/1) - |> ask_unless_supplied(supplied, :psk, "Wi-Fi password", &identity/1) - else - config - end - end - - defp prompt_grisp_io(config, supplied) do - if config[:grisp_io] do - ask_unless_supplied( - config, - supplied, - :grisp_io_linking, - "Device linking token (leave empty to skip linking)", - &String.trim/1 - ) - else - config - end - end - - defp prompt_epmd(config, supplied) do - if config[:epmd], - do: ask_unless_supplied(config, supplied, :cookie, "Erlang cookie", &identity/1), - else: config - end - - defp ask_unless_supplied(config, supplied, key, label, normalize) do - if MapSet.member?(supplied, key), do: config, else: ask(config, key, label, normalize) - end - - defp ask_bool_unless_supplied(config, supplied, key, label) do - if MapSet.member?(supplied, key), do: config, else: ask_bool(config, key, label) - end - - defp ask_valid_name_unless_supplied(config, supplied) do - if MapSet.member?(supplied, :name) and valid_name?(config[:name]) do - config - else - ask_valid_name(config) - end - end - - defp ask_valid_name(config) do - config = ask(config, :name, "App name", &identity/1) - - if valid_name?(config[:name]) do - config - else - Mix.shell().error(name_error()) - ask_valid_name(Keyword.put(config, :name, nil)) - end - end - - defp ask(config, key, label, normalize) do - default = config[key] - answer = Mix.shell().prompt("#{label} [#{default || ""}]: ") |> String.trim() - Keyword.put(config, key, if(answer == "", do: default, else: normalize.(answer))) - end - - defp ask_bool(config, key, label) do - default = config[key] - hint = if default, do: "Y/n", else: "y/N" - answer = Mix.shell().prompt("#{label} [#{hint}]: ") |> String.trim() |> String.downcase() - - case answer do - "" -> - Keyword.put(config, key, default) - - value when value in ["y", "yes", "true"] -> - Keyword.put(config, key, true) - - value when value in ["n", "no", "false"] -> - Keyword.put(config, key, false) - - _ -> - Mix.shell().error("Expected yes or no") - ask_bool(config, key, label) - end - end - - defp validate!(config) do - unless valid_name?(config[:name]), do: Mix.raise(name_error()) - - if config[:grisp_io_linking] && not config[:grisp_io], - do: Mix.raise("--grisp-io-linking requires --grisp-io") - end - - defp network_files(root, config, preserve?) do - base = Path.join(root, "grisp/grisp2/common/deploy/files") - files = [write(Path.join(base, "grisp.ini.mustache"), grisp_ini(config), preserve?)] - - files = - if config[:wifi], - do: - files ++ - [write(Path.join(base, "wpa_supplicant.conf"), wpa(config), preserve?)], - else: files - - files ++ [write(Path.join(base, "erl_inetrc"), inetrc(), preserve?)] - end - - defp write(path, contents, preserve?) - - defp write(path, contents, true) when is_binary(path) do - if File.exists?(path), do: nil, else: write(path, contents, false) - end - - defp write(path, contents, false) do - File.mkdir_p!(Path.dirname(path)) - File.write!(path, contents) - path - end - - defp write_elixir(path, contents, preserve?) do - formatted = contents |> Code.format_string!() |> IO.iodata_to_binary() - write(path, formatted <> "\n", preserve?) - end - - defp mix_exs(config) do - module = Macro.camelize(config[:name]) - included_epmd = if config[:epmd], do: ", included_applications: [:epmd]", else: "" - - epmd_dep = - if config[:epmd], - do: - "\n {:epmd, git: \"https://github.com/erlang/epmd\", ref: \"4d1a59\", runtime: false},", - else: "" - - grisp_io_deps = - if config[:grisp_io], - do: - "\n {:certifi, \">= 0.0.0\"},\n {:grisp_cryptoauth, \"~> 2.6\"},\n {:grisp_updater_grisp2, \"~> 1.0\", runtime: false},\n {:grisp_connect, \"~> 3.0.0\"},", - else: "" - - grisp_dep = - if config[:grisp_io], - do: "{:grisp, \"~> 2.12\", override: true}", - else: "{:grisp, \"~> 2.12\"}" - - grisp_io_release_apps = - if config[:grisp_io], - do: "\n applications: [sasl: :permanent, grisp_updater_grisp2: :load],", - else: "" - - """ - defmodule #{module}.MixProject do - use Mix.Project - - def project do - [ - app: :#{config[:name]}, - version: "0.1.0", - elixir: "~> 1.20", - description: #{inspect(config[:desc])}, - package: [ - licenses: ["Apache-2.0"], - maintainers: [#{inspect("#{config[:author_name]} <#{config[:author_email]}>")}] - ], - start_permanent: Mix.env() == :prod, - deps: deps(), - grisp: grisp(), - releases: releases() - ] - end - - def application do - [extra_applications: [:logger]#{included_epmd}, mod: {#{module}.Application, []}] - end - - defp deps do - [#{epmd_dep}#{grisp_io_deps} - #{grisp_dep}, - {:mix_grisp, "~> 1.0", runtime: false} - ] - end - - defp grisp do - [ - platform: :grisp2, - otp: [version: #{inspect(config[:otp_version])}, jit: #{config[:jit]}], - deploy: [destination: #{inspect(config[:dest])}] - ] - end - - defp releases do - [ - #{config[:name]}: [ - overwrite: true, - cookie: #{inspect(config[:cookie])},#{grisp_io_release_apps} - include_erts: &MixGrisp.Release.erts/0, - steps: [&MixGrisp.Release.init/1, :assemble], - include_executables_for: [], - strip_beams: Mix.env() == :prod - ] - ] - end - end - """ - end - - defp app_config(config) do - connect_config = - if config[:grisp_io_linking] do - "config :grisp_connect,\n device_linking_token: #{inspect(config[:grisp_io_linking])}\n" - else - "" - end - - base = "import Config\n" - - if config[:grisp_io] do - rendered = - "config/grisp_io.exs" - |> template() - |> String.replace("__GRISP_CONNECT_CONFIG__", connect_config) - - base <> "\n" <> rendered - else - base - end - end - - defp grisp_ini(config) do - wifi = if config[:wifi], do: "wlan=enable\nwpa=wpa_supplicant.conf\n", else: "" - grisp_io = if config[:grisp_io], do: " -kernel logger_level notice", else: "" - - epmd = - if config[:epmd], - do: " -internal_epmd epmd_sup -sname #{config[:name]} -setcookie #{config[:cookie]}", - else: "" - - "grisp/grisp2/common/deploy/files/grisp.ini.mustache" - |> template() - |> String.replace("__EPMD_ARGS__", epmd) - |> String.replace("__GRISP_IO_ARGS__", grisp_io) - |> String.replace("__WIFI_CONFIG__", wifi) - end - - defp wpa(config) do - "grisp/grisp2/common/deploy/files/wpa_supplicant.conf" - |> template() - |> String.replace("__WLAN_SSID__", inspect(config[:ssid] || "WLAN_SSID")) - |> String.replace("__WLAN_PASSWORD__", inspect(config[:psk] || "WLAN_PASSWORD")) - end - - defp inetrc, do: template("grisp/grisp2/common/deploy/files/erl_inetrc") - - defp template(relative_path) do - :mix_grisp - |> Application.app_dir("priv/templates") - |> Path.join(relative_path) - |> File.read!() - end - - defp project_readme(config) do - """ - # #{config[:name]} - - #{config[:desc]} - - ## Build - - mix compile - - ## Deploy - - mix grisp.deploy --relname #{config[:name]} --relvsn 0.1.0 - """ - end - - defp license(config) do - """ - Copyright #{config[:copyright_year]}, #{config[:author_name]} <#{config[:author_email]}>. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - """ - end - - defp identity(value), do: value - - defp infer_options(options) do - options = - case Keyword.fetch(options, :grisp_io_linking) do - {:ok, token} -> Keyword.put(options, :grisp_io_linking, String.trim(token)) - :error -> options - end - - options - |> enable_when(:wifi, Keyword.has_key?(options, :ssid) or Keyword.has_key?(options, :psk)) - |> enable_when(:grisp_io, present?(options[:grisp_io_linking])) - end - - defp enable_when(options, key, true), do: Keyword.put(options, key, true) - defp enable_when(options, _key, false), do: options - - defp present?(value), do: is_binary(value) and value != "" - - defp valid_name?(name) when is_binary(name), do: Regex.match?(~r/^[a-z][a-z0-9_]*$/, name) - defp valid_name?(_name), do: false - - defp name_error, - do: "Application name must contain lowercase letters, numbers, and underscores" - - defp confirm_existing!(root, false), do: Mix.raise("Project directory already exists: #{root}") - - defp confirm_existing!(root, true) do - answer = - Mix.shell().prompt( - "Project directory #{root} already exists. Continue without overwriting files? [y/N]: " - ) - |> String.trim() - |> String.downcase() - - case answer do - value when value in ["y", "yes", "true"] -> - :ok - - value when value in ["", "n", "no", "false"] -> - Mix.raise("Project directory already exists: #{root}") - - _ -> - Mix.shell().error("Expected yes or no") - confirm_existing!(root, true) - end - end -end diff --git a/mix.exs b/mix.exs index 2005a61..76b2ab5 100644 --- a/mix.exs +++ b/mix.exs @@ -31,7 +31,7 @@ defmodule MixGrisp.MixProject do defp package() do [ - files: ~w(lib priv .formatter.exs mix.exs README* CHANGELOG* LICENSE*), + files: ~w(lib .formatter.exs mix.exs README* CHANGELOG* LICENSE*), licenses: ["Apache-2.0"], links: %{ "GitHub" => @source_url, diff --git a/priv/templates/config/grisp_io.exs b/priv/templates/config/grisp_io.exs deleted file mode 100644 index ab9d7d2..0000000 --- a/priv/templates/config/grisp_io.exs +++ /dev/null @@ -1,17 +0,0 @@ -config :logger, - level: :notice - -config :grisp_keychain, - api_module: :grisp_cryptoauth - -config :grisp_cryptoauth, - tls_server_trusted_certs_cb: {:certifi, :cacerts, []} - -__GRISP_CONNECT_CONFIG__ - -config :grisp_updater, - system: {:grisp_updater_grisp2, %{}}, - sources: [ - {:grisp_updater_tarball, %{}}, - {:grisp_updater_http, %{backend: {:grisp_updater_grisp2, %{}}}} - ] diff --git a/priv/templates/grisp/grisp2/common/deploy/files/erl_inetrc b/priv/templates/grisp/grisp2/common/deploy/files/erl_inetrc deleted file mode 100644 index 429dac8..0000000 --- a/priv/templates/grisp/grisp2/common/deploy/files/erl_inetrc +++ /dev/null @@ -1,13 +0,0 @@ -%--- Erlang Inet Configuration ------------------------------------------------- - -% Add hosts -% {host, {X,X,X,X}, ["Host"]}. - -% Do not monitor the hosts file -{hosts_file, ""}. - -% Disable caching -{cache_size, 0}. - -% Specify lookup method -{lookup, [file, dns]}. diff --git a/priv/templates/grisp/grisp2/common/deploy/files/grisp.ini.mustache b/priv/templates/grisp/grisp2/common/deploy/files/grisp.ini.mustache deleted file mode 100644 index 0a80a0b..0000000 --- a/priv/templates/grisp/grisp2/common/deploy/files/grisp.ini.mustache +++ /dev/null @@ -1,9 +0,0 @@ -[erlang] -args = erl.rtems -C multi_time_warp -fnu -- -mode embedded -home . -pa . -root {{release_name}} -bindir {{release_name}}/erts-{{erts_vsn}}/bin -boot {{release_name}}/releases/{{release_version}}/start -boot_var RELEASE_LIB {{release_name}}/lib -config {{release_name}}/releases/{{release_version}}/sys.config -kernel inetrc "./erl_inetrc"__GRISP_IO_ARGS____EPMD_ARGS__ -user elixir -extra +iex --no-halt -shell = none -on_exit = reboot -on_crash = reboot - -[network] -ip_self=dhcp -__WIFI_CONFIG__hostname=GRISP_HOSTNAME diff --git a/priv/templates/grisp/grisp2/common/deploy/files/wpa_supplicant.conf b/priv/templates/grisp/grisp2/common/deploy/files/wpa_supplicant.conf deleted file mode 100644 index f4dfe5d..0000000 --- a/priv/templates/grisp/grisp2/common/deploy/files/wpa_supplicant.conf +++ /dev/null @@ -1,5 +0,0 @@ -network={ - ssid=__WLAN_SSID__ - key_mgmt=WPA-PSK - psk=__WLAN_PASSWORD__ -} diff --git a/test/mix_grisp/configure_test.exs b/test/mix_grisp/configure_test.exs deleted file mode 100644 index 48db3c6..0000000 --- a/test/mix_grisp/configure_test.exs +++ /dev/null @@ -1,217 +0,0 @@ -defmodule MixGrisp.ConfigureTest do - use ExUnit.Case, async: false - - setup do - previous_shell = Mix.shell() - Mix.shell(Mix.Shell.Process) - - root = - System.tmp_dir!() - |> Path.join("mix_grisp_configure_#{System.unique_integer([:positive])}") - - File.mkdir_p!(root) - - on_exit(fn -> - File.rm_rf!(root) - Mix.shell(previous_shell) - end) - - {:ok, root: root} - end - - test "does not prompt again for values supplied on the command line", %{root: root} do - File.cd!(root, fn -> - MixGrisp.Configure.run( - interactive: true, - name: "demo", - otp_version: "29", - jit: true, - dest: "/tmp/grisp", - wifi: false, - grisp_io: false, - epmd: false - ) - end) - - refute_received {:mix_shell, :prompt, _message} - end - - test "accepts true and false as explicit jit values", %{root: root} do - File.cd!(root, fn -> - Mix.Task.reenable("grisp.configure") - - Mix.Tasks.Grisp.Configure.run([ - "--no-interactive", - "--name", - "jit_enabled", - "--jit", - "true" - ]) - - Mix.Task.reenable("grisp.configure") - - Mix.Tasks.Grisp.Configure.run([ - "--no-interactive", - "--name", - "jit_disabled", - "--jit", - "false" - ]) - end) - - assert File.read!(Path.join(root, "jit_enabled/mix.exs")) =~ "jit: true" - assert File.read!(Path.join(root, "jit_disabled/mix.exs")) =~ "jit: false" - end - - test "re-prompts until a valid application name is entered", %{root: root} do - send(self(), {:mix_shell_input, :prompt, ""}) - send(self(), {:mix_shell_input, :prompt, "valid_name"}) - - result = - File.cd!(root, fn -> - MixGrisp.Configure.run( - interactive: true, - name: "", - otp_version: "29", - jit: true, - dest: "/tmp/grisp", - wifi: false, - grisp_io: false, - epmd: false - ) - end) - - assert result.name == "valid_name" - assert_received {:mix_shell, :error, [_message]} - end - - test "re-prompts for invalid yes or no answers", %{root: root} do - send(self(), {:mix_shell_input, :prompt, "maybe"}) - send(self(), {:mix_shell_input, :prompt, "n"}) - - File.cd!(root, fn -> - MixGrisp.Configure.run( - interactive: true, - name: "demo", - otp_version: "29", - jit: true, - dest: "/tmp/grisp", - grisp_io: false, - epmd: false - ) - end) - - assert_received {:mix_shell, :error, ["Expected yes or no"]} - end - - test "preserves files when the user accepts an existing project directory", %{root: root} do - project = Path.join(root, "demo") - File.mkdir_p!(project) - File.write!(Path.join(project, "mix.exs"), "# existing project\n") - send(self(), {:mix_shell_input, :prompt, "y"}) - - File.cd!(root, fn -> - MixGrisp.Configure.run( - interactive: true, - name: "demo", - otp_version: "29", - jit: true, - dest: "/tmp/grisp", - wifi: false, - grisp_io: false, - epmd: false - ) - end) - - assert File.read!(Path.join(project, "mix.exs")) == "# existing project\n" - assert File.exists?(Path.join(project, "config/config.exs")) - end - - test "generates an Ethernet configuration without enabling WLAN", %{root: root} do - result = - File.cd!(root, fn -> - MixGrisp.Configure.run( - interactive: false, - name: "demo" - ) - end) - - grisp_ini = - File.read!(Path.join(result.root, "grisp/grisp2/common/deploy/files/grisp.ini.mustache")) - - refute grisp_ini =~ "wlan=enable" - assert grisp_ini =~ ~s(-kernel inetrc "./erl_inetrc") - assert grisp_ini =~ "-user elixir -extra +iex --no-halt" - assert grisp_ini =~ "on_exit = reboot" - assert grisp_ini =~ "on_crash = reboot" - end - - test "enables Wi-Fi when either credential is supplied", %{root: root} do - for {name, option} <- [{"ssid_only", {:ssid, "test-network"}}, {"psk_only", {:psk, "secret"}}] do - result = - File.cd!(root, fn -> - MixGrisp.Configure.run([{:interactive, false}, {:name, name}, option]) - end) - - files = Path.join(result.root, "grisp/grisp2/common/deploy/files") - grisp_ini = File.read!(Path.join(files, "grisp.ini.mustache")) - - assert grisp_ini =~ "wlan=enable" - assert grisp_ini =~ "wpa=wpa_supplicant.conf" - assert File.exists?(Path.join(files, "wpa_supplicant.conf")) - end - end - - test "generates a bootable GRiSP.io release configuration", %{root: root} do - File.cd!(root, fn -> - Mix.Task.reenable("grisp.configure") - - Mix.Tasks.Grisp.Configure.run([ - "--no-interactive", - "--name", - "connected", - "--ssid", - "test-network", - "--psk", - "secret", - "--grisp-io-linking", - "link-token" - ]) - end) - - project = Path.join(root, "connected") - mix_exs = File.read!(Path.join(project, "mix.exs")) - config_exs = File.read!(Path.join(project, "config/config.exs")) - files = Path.join(project, "grisp/grisp2/common/deploy/files") - grisp_ini = File.read!(Path.join(files, "grisp.ini.mustache")) - - for dependency <- ~w(certifi grisp_cryptoauth grisp_updater_grisp2 grisp_connect) do - assert mix_exs =~ ":#{dependency}" - end - - assert mix_exs =~ ~s({:grisp_cryptoauth, "~> 2.6"}) - assert mix_exs =~ ~s({:grisp_connect, "~> 3.0.0"}) - assert mix_exs =~ ~s({:grisp_updater_grisp2, "~> 1.0", runtime: false}) - assert mix_exs =~ ~s({:grisp, "~> 2.12", override: true}) - assert mix_exs =~ "applications: [sasl: :permanent, grisp_updater_grisp2: :load]" - - assert config_exs =~ "config :grisp_keychain" - assert config_exs =~ "config :grisp_cryptoauth" - assert config_exs =~ "config :grisp_connect" - assert config_exs =~ ~s(device_linking_token: "link-token") - assert config_exs =~ "config :grisp_updater" - refute config_exs =~ "config :kernel" - refute config_exs =~ "logger: []" - assert {:ok, _quoted} = Code.string_to_quoted(config_exs) - - assert grisp_ini =~ "wlan=enable" - assert grisp_ini =~ "wpa=wpa_supplicant.conf" - assert grisp_ini =~ "-kernel logger_level notice" - refute grisp_ini =~ "__EPMD_ARGS__" - refute grisp_ini =~ "__GRISP_IO_ARGS__" - refute grisp_ini =~ "__WIFI_CONFIG__" - refute config_exs =~ "__GRISP_CONNECT_CONFIG__" - assert File.exists?(Path.join(files, "wpa_supplicant.conf")) - assert File.exists?(Path.join(files, "erl_inetrc")) - end -end