Current section

Files

Jump to
nerves_bootstrap lib mix tasks nerves.new.ex
Raw

lib/mix/tasks/nerves.new.ex

# SPDX-FileCopyrightText: 2016 Justin Schneck
# SPDX-FileCopyrightText: 2017 Frank Hunleth
# SPDX-FileCopyrightText: 2019 Milton Mazzarri
# SPDX-FileCopyrightText: 2019 Todd Resudek
# SPDX-FileCopyrightText: 2020 TORIFUKU Kaiou
# SPDX-FileCopyrightText: 2022 Jason Axelson
# SPDX-FileCopyrightText: 2022 Jon Carstens
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Mix.Tasks.Nerves.New do
@shortdoc "Creates a new Nerves application"
@moduledoc """
Creates a new Nerves project
mix nerves.new PATH [--module MODULE] [--app APP] [--target TARGET] [--cookie STRING] [--no-nerves-pack]
The project will be created at PATH. The application name and module name
will be inferred from PATH unless `--module` or `--app` is given.
An `--app` option can be given in order to name the OTP application for the
project.
A `--module` option can be given in order to name the modules in the
generated code skeleton.
A `--target` option can be given to limit support to one or more of the
[officially Nerves
systems](https://nerves.hexdocs.pm/supported-targets.html).
A `--cookie` options can be given to set the Erlang distribution
cookie in `vm.args`. This defaults to a randomly generated string.
Generate a project without `nerves_pack` support by passing
`--no-nerves-pack`.
## Examples
mix nerves.new blinky
Is equivalent to:
mix nerves.new blinky --module Blinky
Generate a project that only supports Raspberry Pi 5
mix nerves.new blinky --target rpi5
Generate a project that supports Raspberry Pi 5 and Raspberry Pi Zero
mix nerves.new blinky --target rpi5 --target rpi0
Generate a project without `nerves_pack`
mix nerves.new blinky --no-nerves-pack
"""
use Mix.Task
import Mix.Generator
alias NervesBootstrap.Versions
@default_targets [
:bbb,
:mangopi_mq_pro,
:qemu_aarch64,
:rpi,
:rpi0,
:rpi0_2,
:rpi2,
:rpi3,
:rpi4,
:rpi5,
:x86_64
]
# Mapping between easy-to-type target name and the actual Nerves system package
# It is ok for this list to contain more mappings than what's listed above.
@target_systems %{
bbb: :nerves_system_bbb,
grisp2: :nerves_system_grisp2,
mangopi_mq_pro: :nerves_system_mangopi_mq_pro,
npi_imx6ull: :nerves_system_npi_imx6ull,
osd32mp1: :nerves_system_osd32mp1,
qemu_aarch64: :nerves_system_qemu_aarch64,
rpi: :nerves_system_rpi,
rpi0: :nerves_system_rpi0,
rpi0_2: :nerves_system_rpi0_2,
rpi2: :nerves_system_rpi2,
rpi3: :nerves_system_rpi3,
rpi3a: :nerves_system_rpi3a,
rpi4: :nerves_system_rpi4,
rpi5: :nerves_system_rpi5,
trellis: :nerves_system_trellis,
x86_64: :nerves_system_x86_64
}
@new [
{:eex, "new/config/config.exs", "config/config.exs"},
{:eex, "new/config/host.exs", "config/host.exs"},
{:eex, "new/config/target.exs", "config/target.exs"},
{:eex, "new/lib/app_name.ex", "lib/app_name.ex"},
{:eex, "new/lib/app_name/application.ex", "lib/app_name/application.ex"},
{:eex, "new/test/test_helper.exs", "test/test_helper.exs"},
{:eex, "new/test/app_name_test.exs", "test/app_name_test.exs"},
{:eex, "new/rel/vm.args.eex", "rel/vm.args.eex"},
{:eex, "new/rootfs_overlay/etc/iex.exs", "rootfs_overlay/etc/iex.exs"},
{:text, "new/.gitignore", ".gitignore"},
{:text, "new/.formatter.exs", ".formatter.exs"},
{:eex, "new/mix.exs", "mix.exs"},
{:eex, "new/README.md", "README.md"},
{:keep, "new/rel", "rel"}
]
@reserved_names ~w[nerves]
# Embed all defined templates
root = Path.expand("../../../templates", __DIR__)
for {format, source, _} <- @new do
if format != :keep do
@external_resource Path.join(root, source)
defp render(unquote(source)), do: unquote(File.read!(Path.join(root, source)))
end
end
@switches [
app: :string,
module: :string,
target: :keep,
cookie: :string,
nerves_pack: :boolean,
source_date_epoch: :integer
]
@impl Mix.Task
def run([version]) when version in ~w(-v --version) do
Mix.shell().info("Nerves Bootstrap #{Versions.bootstrap_vsn()}")
end
def run(argv) do
# This version check is needed since installing Nerves Bootstrap on an
# old version of Elixir only results in a warning.
if not Versions.supported_elixir?() do
Mix.raise("""
Nerves Bootstrap #{Versions.bootstrap_vsn()} creates projects that require Elixir #{Versions.elixir_req()}.
You have Elixir #{System.version()}. Please update your Elixir version or downgrade
the version of Nerves Bootstrap that you're using.
See https://nerves.hexdocs.pm/installation.html for more information on
setting up your environment.
""")
end
{opts, argv} =
case OptionParser.parse(argv, strict: @switches) do
{opts, argv, []} ->
{opts, argv}
{_opts, _argv, [switch | _]} ->
Mix.raise("Invalid option: " <> switch_to_string(switch))
end
case argv do
[] ->
Mix.Task.run("help", ["nerves.new"])
[path | _] ->
app = opts[:app] || Path.basename(Path.expand(path))
check_application_name!(app, !!opts[:app])
mod = opts[:module] || Macro.camelize(app)
check_module_name_validity!(mod)
check_module_name_availability!(mod)
run(app, mod, path, opts)
end
end
defp run(app, _mod, _path, _opts) when app in @reserved_names,
do: Mix.raise("New projects cannot be named '#{app}'")
defp run(app, mod, path, opts) do
System.delete_env("MIX_TARGET")
in_umbrella? = in_umbrella?(path)
nerves_pack? = Keyword.get(opts, :nerves_pack, true)
targets = Keyword.get_values(opts, :target)
known_targets = Map.keys(@target_systems) |> Enum.sort()
targets =
Enum.map(targets, fn target ->
target = String.to_atom(target)
if target not in known_targets do
targets = Enum.join(known_targets, "\n ")
Mix.raise("""
Unknown target #{inspect(target)}
Known targets:
#{targets}
If you don't want any of these targets, one option is to pick one
and change the references to it in the resulting mix.exs.
""")
end
target
end)
targets = if targets == [], do: @default_targets, else: targets
cookie = opts[:cookie]
source_date_epoch = Keyword.get(opts, :source_date_epoch, generate_source_date_epoch())
elixir_version = System.version() |> Version.parse!()
binding = [
app_name: app,
app_module: mod,
elixir_req: "~> #{elixir_version.major}.#{elixir_version.minor}",
bootstrap_req: Versions.bootstrap_req(),
package_reqs: Versions.package_reqs(),
in_umbrella: in_umbrella?,
nerves_pack?: nerves_pack?,
targets: targets,
target_systems: @target_systems,
cookie: cookie,
source_date_epoch: source_date_epoch
]
copy_from(path, binding, @new)
print_mix_info(path)
end
defp recompile(regex) do
if Code.ensure_loaded?(Regex) and function_exported?(Regex, :recompile!, 1) do
apply(Regex, :recompile!, [regex])
else
regex
end
end
defp print_mix_info(path) do
Mix.shell().info("""
Your Nerves project was created successfully.
You should now pick a target. See https://nerves.hexdocs.pm/supported-targets.html
for supported target tags. If your target is on the list, set `MIX_TARGET`
to its tag name:
For example, run the following for the Raspberry Pi 5:
$ export MIX_TARGET=rpi5
If you will be using a custom system, update the `mix.exs`
dependencies to point to the desired system's package.
Now download the dependencies and build a firmware archive:
$ cd #{path}
$ mix deps.get
$ mix firmware
If your target boots up using a MicroSD card like a Raspberry Pi 5,
then insert the card into a reader on your computer and run:
$ mix burn
Then plug the MicroSD card into the target and power it up. See target
documentation above for more information and other targets.
""")
end
defp switch_to_string({name, nil}), do: name
defp switch_to_string({name, val}), do: name <> "=" <> val
defp check_application_name!(name, from_app_flag) do
if !(name =~ recompile(~r/^[a-z][\w_]*$/)) do
extra =
if from_app_flag do
""
else
". The application name is inferred from the path, if you'd like to " <>
"explicitly name the application then use the `--app APP` option."
end
Mix.raise(
"Application name must start with a letter and have only lowercase " <>
"letters, numbers and underscore, got: #{inspect(name)}" <> extra
)
end
end
defp check_module_name_validity!(name) do
if !(name =~ recompile(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/)) do
Mix.raise(
"Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}"
)
end
end
defp check_module_name_availability!(name) do
name = Module.concat(Elixir, name)
if Code.ensure_loaded?(name) do
Mix.raise("Module name #{inspect(name)} is already taken, please choose another name")
end
end
defp copy_from(target_dir, binding, mapping) when is_list(mapping) do
app_name = Keyword.fetch!(binding, :app_name)
Enum.each(mapping, fn {format, source, target_path} ->
target = Path.join(target_dir, String.replace(target_path, "app_name", app_name))
case format do
:keep ->
File.mkdir_p!(target)
:text ->
create_file(target, render(source))
:append ->
append_to(Path.dirname(target), Path.basename(target), render(source))
:eex ->
contents = EEx.eval_string(render(source), binding, file: source, trim: false)
create_file(target, contents)
end
end)
end
defp append_to(path, file, contents) do
file = Path.join(path, file)
File.write!(file, File.read!(file) <> contents)
end
defp in_umbrella?(app_path) do
umbrella = Path.expand(Path.join([app_path, "..", ".."]))
File.exists?(Path.join(umbrella, "mix.exs")) &&
Mix.Project.in_project(:umbrella_check, umbrella, fn _ ->
path = Mix.Project.config()[:apps_path]
path && Path.expand(path) == Path.join(umbrella, "apps")
end)
catch
_, _ -> false
end
defp generate_source_date_epoch() do
DateTime.utc_now() |> DateTime.to_unix()
end
end