Current section
Files
Jump to
Current section
Files
lib/mix/tasks/upload.ex
# SPDX-FileCopyrightText: 2020 Frank Hunleth
# SPDX-FileCopyrightText: 2022 Jon Carstens
# SPDX-FileCopyrightText: 2024 Benjamin Milde
# SPDX-FileCopyrightText: 2024 Jon Ringle
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Mix.Tasks.Upload do
use Mix.Task
@shortdoc "Uploads firmware to a Nerves device over SSH"
@moduledoc """
Upgrade the firmware on a Nerves device using SSH.
By default, `mix upload` reads the firmware built by the current `MIX_ENV`
and `MIX_TARGET` settings, and sends it to `nerves.local`. Pass in a another
hostname to send the firmware elsewhere.
NOTE: This implementation cannot ask for passphrases, and therefore, cannot
connect to devices protected by username/passwords or decrypt
password-protected private keys. One workaround is to use the `ssh-agent` to
pass credentials.
## Command line options
* `--firmware` - The path to a fw file
* `--port` - An alternative TCP port to use for the upload (defaults to 22)
## Examples
Upgrade a Raspberry Pi Zero at `nerves.local`:
MIX_TARGET=rpi0 mix upload nerves.local
Upgrade `192.168.1.120` and explicitly pass the `.fw` file:
mix upload 192.168.1.120 --firmware _build/rpi0_prod/nerves/images/app.fw
"""
@switches [
firmware: :string,
port: :integer
]
@doc false
@spec run([String.t()]) :: :ok
def run(argv) do
{opts, args, unknown} = OptionParser.parse(argv, strict: @switches)
if unknown != [] do
[{param, _} | _] = unknown
Mix.raise("unknown parameter passed to mix upload: #{param}")
end
ip =
case args do
[address] -> address
[] -> "nerves.local"
_other -> Mix.raise(target_ip_address_or_name_msg())
end
check_requirements!()
port = opts[:port] || 22
validate_port!(port)
firmware_path = firmware(opts)
Mix.shell().info("""
Path: #{firmware_path}
#{maybe_print_firmware_uuid(firmware_path)}
Uploading to #{ip}:#{port}...
""")
# LD_LIBRARY_PATH is unset to avoid errors with host ssl (see commit 9b1df471)
{_, status} =
InteractiveCmd.shell(
"cat #{shell_quote(firmware_path)} | ssh -p #{port} -s -- #{shell_quote(ip)} fwup",
env: [{"LD_LIBRARY_PATH", false}]
)
if status != 0 do
Mix.raise("""
Failed to upgrade the device.
If this persists and it's not a networking or device issue, please
try using the `upload.sh` script generated by `mix firmware.gen.script`.
""")
end
:ok
end
defp firmware(opts) do
path = opts[:firmware] || default_firmware()
absolute_path = Path.expand(path)
if not File.exists?(absolute_path) do
Mix.raise("""
The firmware file does not exist.
Path:
#{absolute_path}
Run `mix firmware` to build it or check the path.
""")
end
absolute_path
end
defp default_firmware() do
if Mix.target() == :host do
Mix.raise("""
You must call mix with a target set or pass the firmware's path.
Examples:
$ MIX_TARGET=rpi0 mix upload nerves.local
or
$ mix upload nerves.local --firmware _build/rpi0_prod/nerves/images/app.fw
""")
end
build_path = Mix.Project.build_path()
app = Mix.Project.config()[:app]
Path.join([build_path, "nerves", "images", "#{app}.fw"])
end
defp check_requirements!() do
check_ssh!()
with {:error, reason} <- InteractiveCmd.check_requirements() do
Mix.raise(reason)
end
:ok
end
defp check_ssh!() do
if System.find_executable("ssh") == nil do
Mix.raise("""
Cannot find 'ssh'. Check that it exists in your path
""")
end
end
defp validate_port!(port) when is_integer(port) and port > 0 and port <= 65535, do: :ok
defp validate_port!(port) do
Mix.raise("Invalid port: #{inspect(port)}. Port must be an integer between 1 and 65535.")
end
defp target_ip_address_or_name_msg() do
~S"""
mix upload expects a target IP address or hostname
Example:
If the device is reachable using `nerves-1234.local`, try:
`mix upload nerves-1234.local`
"""
end
defp maybe_print_firmware_uuid(fw_path) do
fwup = System.find_executable("fwup")
{uuid, 0} = System.cmd(fwup, ["-m", "--metadata-key", "meta-uuid", "-i", fw_path])
"UUID: #{uuid}\n"
catch
# fwup may not be on the host or something else failed, but continue
# on as normal by returning an empty line
_, _ -> ""
end
defp shell_quote(str), do: "'" <> String.replace(str, "'", "'\"'\"'") <> "'"
end