Packages
nerves
1.13.2
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.0
1.11.3
1.11.2
1.11.1
1.11.0
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.3
1.9.2
retired
1.9.1
1.9.0
1.8.0
1.7.17
1.7.16
1.7.15
1.7.14
retired
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
retired
1.7.1
retired
1.7.0
retired
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.1
1.1.0
1.0.1
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.11.0
0.10.1
0.10.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.3
0.8.2
0.8.1
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-rc.0
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
Craft and deploy bulletproof embedded software
Current section
Files
Jump to
Current section
Files
lib/nerves/artifact.ex
# SPDX-FileCopyrightText: 2016 Justin Schneck
# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2019 Jon Carstens
# SPDX-FileCopyrightText: 2021 Jaremy J. Creechley
# SPDX-FileCopyrightText: 2023 James Harton
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Nerves.Artifact do
@moduledoc """
Package artifacts are the product of compiling a package with a
specific toolchain.
"""
alias Nerves.Artifact.BuildRunners
alias Nerves.Artifact.Cache
alias Nerves.Artifact.Resolvers
@checksum_short 7
# credo:disable-for-next-line Credo.Check.Readability.Specs
def __checksum_short_length__(), do: @checksum_short
@doc """
Builds the package and produces an See Nerves.Artifact
for more information.
"""
@spec build(Nerves.Package.t(), Nerves.Package.t()) :: :ok | {:error, File.posix()}
def build(pkg, toolchain) do
with {build_runner, opts} <- pkg.build_runner,
{:ok, path} <- build_runner.build(pkg, toolchain, opts) do
Cache.put(pkg, path)
else
:noop ->
:ok
{:error, error} ->
Mix.raise("""
Nerves encountered an error while constructing the artifact
#{if String.valid?(error), do: error, else: inspect(error)}
""")
end
end
@doc """
Produces an archive of the package artifact which can be fetched when
calling `nerves.artifact.get`.
"""
@spec archive(Nerves.Package.t(), Nerves.Package.t(), keyword()) :: {:ok, String.t()}
def archive(%{app: app, build_runner: nil}, _toolchain, _opts) do
Mix.raise("""
#{inspect(app)} does not declare a build_runner and therefore cannot
be used to produce an artifact archive.
""")
end
def archive(pkg, toolchain, opts) do
Mix.shell().info("Creating Artifact Archive")
opts = default_archive_opts(pkg, opts)
{build_runner, _opts} = pkg.build_runner
_ = Code.ensure_compiled(pkg.platform)
{:ok, archive_path} = build_runner.archive(pkg, toolchain, opts)
archive_path = Path.expand(archive_path)
path =
opts[:path]
|> Path.expand()
|> Path.join(download_name(pkg) <> ext(pkg))
if path != archive_path do
File.cp!(archive_path, path)
end
{:ok, archive_path}
end
@doc """
Cleans the artifacts for the package build_runners of all packages.
"""
@spec clean(Nerves.Package.t()) :: :ok | {:error, term}
def clean(pkg) do
Mix.shell().info("Cleaning Nerves Package #{pkg.app}")
case pkg.build_runner do
{build_runner, _opts} ->
build_runner.clean(pkg)
_ ->
Mix.shell().info("No build_runner specified for #{pkg.app}")
{:error, :no_build_runner}
end
end
@doc """
Determines if the artifact for a package is stale and needs to be rebuilt.
"""
@spec stale?(Nerves.Package.t()) :: boolean
def stale?(pkg) do
if env_var?(pkg) do
false
else
!Cache.valid?(pkg)
end
end
@doc """
Get the artifact name
"""
@spec name(Nerves.Package.t()) :: String.t()
def name(pkg) do
"#{pkg.app}-#{host_tuple(pkg)}-#{pkg.version}"
end
@doc """
Get the artifact download name
"""
@spec download_name(Nerves.Package.t(), checksum_short: non_neg_integer()) :: String.t()
def download_name(pkg, opts \\ []) do
checksum_short = opts[:checksum_short] || @checksum_short
"#{pkg.app}-#{host_tuple(pkg)}-#{pkg.version}-#{checksum(pkg, short: checksum_short)}"
end
@doc """
Get the base dir for where an artifact for a package should be stored.
The directory for artifacts will be found in the directory returned
by `Nerves.Env.data_dir/0` (i.e. `"#{Nerves.Env.data_dir()}/artifacts/"`).
This location can be overridden by the environment variable `NERVES_ARTIFACTS_DIR`.
"""
@spec base_dir() :: String.t()
def base_dir() do
case System.get_env("NERVES_ARTIFACTS_DIR") do
nil -> Path.join(Nerves.Env.data_dir(), "artifacts")
dir -> dir
end
end
@doc """
Get the path to where the artifact is built
"""
@spec build_path(Nerves.Package.t()) :: binary
def build_path(pkg) do
Path.join([pkg.path, ".nerves", "artifacts", name(pkg)])
end
@doc """
Get the path where the global artifact will be linked to.
This path is typically a location within build_path, but can be
vary on different build platforms.
"""
@spec build_path_link(Nerves.Package.t()) :: Path.t()
def build_path_link(pkg) do
case pkg.platform do
platform when is_atom(platform) ->
if :erlang.function_exported(platform, :build_path_link, 1) do
apply(platform, :build_path_link, [pkg])
else
build_path(pkg)
end
_ ->
build_path(pkg)
end
end
@doc """
Produce a base16 encoded checksum for the package from the list of files
and expanded folders listed in the checksum config key.
"""
@spec checksum(Nerves.Package.t(), short: non_neg_integer()) :: String.t()
def checksum(pkg, opts \\ []) do
blob =
(pkg.config[:checksum] || [])
|> expand_paths(pkg.path)
|> Enum.map(&File.read!/1)
|> Enum.map(&:crypto.hash(:sha256, &1))
checksum =
:crypto.hash(:sha256, blob)
|> Base.encode16()
case Keyword.get(opts, :short) do
nil ->
checksum
short_len ->
{checksum_short, _} = String.split_at(checksum, short_len)
checksum_short
end
end
@doc """
The full path to the artifact.
"""
@spec dir(Nerves.Package.t()) :: String.t()
def dir(pkg) do
if env_var?(pkg) do
System.get_env(env_var(pkg)) |> Path.expand()
else
base_dir()
|> Path.join(name(pkg))
end
end
@doc """
Check to see if the artifact path is being set from the system env.
"""
@spec env_var?(Nerves.Package.t()) :: boolean
def env_var?(pkg) do
name = env_var(pkg)
dir = System.get_env(name)
dir != nil and File.dir?(dir)
end
@doc """
Determine the environment variable which would be set to override the path.
"""
@spec env_var(Nerves.Package.t()) :: String.t()
def env_var(pkg) do
case pkg.type do
:toolchain ->
"NERVES_TOOLCHAIN"
:system ->
"NERVES_SYSTEM"
_ ->
pkg.app
|> Atom.to_string()
|> String.upcase()
end
end
@doc """
Expands the sites helpers from `artifact_sites` in the nerves_package config.
Artifact sites can pass options as a third parameter for adding headers
or query string parameters. For example, if you are trying to resolve
artifacts hosted in a private Github repo, use `:github_api` and
pass a user, tag, and personal access token into the sites helper:
```elixir
{:github_api, "owner/repo", username: "skroob", token: "1234567", tag: "v0.1.0"}
```
Or pass query parameters for the URL:
```elixir
{:prefix, "https://my-organization.com", query_params: %{"id" => "1234567", "token" => "abcd"}}
```
You can also use this to add an authorization header for files behind basic auth.
```elixir
{:prefix, "http://my-organization.com/", headers: [{"Authorization", "Basic " <> System.get_env("BASIC_AUTH")}}]}
```
"""
@spec expand_sites(Nerves.Package.t()) :: [
{Resolvers.URI | Resolvers.GithubAPI, {Path.t(), Keyword.t()}}
]
def expand_sites(pkg) do
case pkg.config[:artifact_url] do
nil ->
Keyword.get(pkg.config, :artifact_sites, [])
|> Enum.map(&expand_site(&1, pkg))
urls when is_list(urls) ->
# artifact_url is deprecated and this code can be removed sometime following
# nerves 1.0
if Enum.any?(urls, &(!is_binary(&1))) do
Mix.raise("""
artifact_urls can only be strings.
Please use artifact_sites instead.
""")
end
urls
_invalid ->
Mix.raise("Invalid artifact_url. Please use artifact_sites instead")
end
end
@doc """
Get the path to where the artifact archive is downloaded to.
"""
@spec download_path(Nerves.Package.t()) :: String.t()
def download_path(pkg) do
name = download_name(pkg) <> ext(pkg)
Nerves.Env.download_dir()
|> Path.join(name)
|> Path.expand()
end
@doc """
Get the host_tuple for the package. Toolchains are specifically build to run
on a host for a target. Other packages are host agnostic for now. They are
marked as `portable`.
"""
@spec host_tuple(Nerves.Package.t()) :: String.t()
def host_tuple(%{type: :system}) do
"portable"
end
def host_tuple(_pkg) do
(Nerves.Env.host_os() <> "_" <> Nerves.Env.host_arch())
|> normalize_osx()
end
# Workaround for OTP 24 returning 'aarch64-apple-darwin20.4.0'
# and OTP 23 and earlier returning 'arm-apple-darwin20.4.0'.
#
# The current Nerves tooling naming uses "arm".
defp normalize_osx("darwin_aarch64"), do: "darwin_arm"
defp normalize_osx(other), do: other
@doc """
Determines the extension for an artifact based off its type.
Toolchains use xz compression.
"""
@spec ext(Nerves.Package.t()) :: String.t()
def ext(%{type: :toolchain}), do: ".tar.xz"
def ext(_), do: ".tar.gz"
@spec build_runner(keyword()) :: {module(), keyword()}
def build_runner(config) do
opts = config[:nerves_package][:build_runner_opts] || []
mod =
config[:nerves_package][:build_runner] || build_runner_type(config[:nerves_package][:type])
{mod, opts}
end
defp build_runner_type(:system_platform), do: nil
defp build_runner_type(:toolchain_platform), do: nil
defp build_runner_type(:toolchain), do: BuildRunners.Local
defp build_runner_type(:system) do
case :os.type() do
{_, :linux} -> BuildRunners.Local
_ -> BuildRunners.Docker
end
end
defp build_runner_type(_), do: BuildRunners.Local
defp expand_paths(paths, dir) do
paths
|> Enum.map(&Path.join(dir, &1))
|> Enum.flat_map(&Path.wildcard/1)
|> Enum.flat_map(&dir_files/1)
|> Enum.map(&Path.expand/1)
|> Enum.filter(&File.regular?/1)
|> Enum.uniq()
end
defp dir_files(path) do
if File.dir?(path) do
Path.wildcard(Path.join(path, "**"))
else
[path]
end
end
defp default_archive_opts(pkg, opts) do
name = download_name(pkg) <> ext(pkg)
opts
|> Keyword.put_new(:name, name)
|> Keyword.put_new(:path, File.cwd!())
end
defp expand_site(_, _, _ \\ [])
defp expand_site({:github_releases, org_proj}, pkg, opts) do
opts =
opts
|> Keyword.put(:artifact_name, download_name(pkg, opts) <> ext(pkg))
|> Keyword.put(:public?, true)
|> update_in([:tag], &(&1 || "v#{pkg.version}"))
{Resolvers.GithubAPI, {org_proj, opts}}
end
defp expand_site({:gitea_releases, repo_uri}, pkg, opts) when is_binary(repo_uri),
do: expand_site({:gitea_releases, URI.parse(repo_uri)}, pkg, opts)
defp expand_site({:gitea_releases, repo_uri}, pkg, opts)
when is_nil(repo_uri.scheme) and is_nil(repo_uri.host),
do: expand_site({:gitea_releases, URI.parse("https://#{repo_uri.path}")}, pkg, opts)
defp expand_site({:gitea_releases, repo_uri}, pkg, opts) do
repo_uri = URI.parse(repo_uri)
base_url = %{repo_uri | path: "/"} |> to_string()
org_proj = repo_uri.path |> String.trim_leading("/")
opts =
opts
|> Keyword.put(:base_url, base_url)
|> Keyword.put(:artifact_name, download_name(pkg, opts) <> ext(pkg))
|> Keyword.put(:public?, true)
|> update_in([:tag], &(&1 || "v#{pkg.version}"))
{Resolvers.GiteaAPI, {org_proj, opts}}
end
defp expand_site({:prefix, url}, pkg, opts) do
expand_site({:prefix, url, []}, pkg, opts)
end
defp expand_site({:prefix, path, resolver_opts}, pkg, opts) do
path = Path.join(path, download_name(pkg, opts) <> ext(pkg))
{Resolvers.URI, {path, resolver_opts}}
end
defp expand_site({:github_api, org_proj, resolver_opts}, pkg, opts) do
resolver_opts =
Keyword.put(resolver_opts, :artifact_name, download_name(pkg, opts) <> ext(pkg))
{Resolvers.GithubAPI, {org_proj, resolver_opts}}
end
defp expand_site({:gitea_api, org_proj, resolver_opts}, pkg, opts) do
resolver_opts =
Keyword.put(resolver_opts, :artifact_name, download_name(pkg, opts) <> ext(pkg))
{Resolvers.GiteaAPI, {org_proj, resolver_opts}}
end
defp expand_site(site, _pkg, _opts),
do:
Mix.raise("""
Unsupported artifact site
#{inspect(site)}
Supported artifact sites:
{:github_releases, "owner/repo"}
{:github_api, "owner/repo", username: "skroob", token: "1234567", tag: "v0.1.0"}
{:gitea_releases, "host/owner/repo"},
{:gitea_api, "owner/repo", base_url: "https://gitea.com", token: "123456", tag: "v0.1.0"}
{:prefix, "http://myserver.com/artifacts"}
{:prefix, "http://myserver.com/artifacts", headers: [{"Authorization", "Basic: 1234567=="}]}
{:prefix, "http://myserver.com/artifacts", query_params: %{"id" => "1234567"}}
{:prefix, "file:///my_artifacts/"}
{:prefix, "/users/my_user/artifacts/"}
""")
end