defmodule ReleaseKit.PackageBuilder do
@moduledoc """
Packages a Mix release directory into a ReleaseKit artifact.
"""
alias ReleaseKit.{
Artifact,
ArtifactInfo,
BuildInfo,
Checksum,
Lifecycle,
Manifest,
Package,
PackageCache,
Telemetry
}
@enforce_keys [:release_dir, :out_dir, :name, :release, :version, :opts, :target, :assets]
defstruct [:release_dir, :out_dir, :name, :release, :version, :opts, :target, :assets]
@type t :: %__MODULE__{
release_dir: Path.t(),
out_dir: Path.t(),
name: String.t(),
release: String.t(),
version: String.t(),
opts: keyword(),
target: ReleaseKit.Target.t(),
assets: [ReleaseKit.AssetInfo.t()]
}
@type result :: {Path.t(), Path.t(), Manifest.t()}
@doc """
Packages the release directory and returns tarball, manifest path, and manifest.
"""
@spec run(t()) :: result()
def run(%__MODULE__{} = request) do
Telemetry.span_with_metadata(:package, metadata(request), fn ->
compression = package_compression(request.opts)
tarball = Path.join(request.out_dir, "#{request.name}#{package_extension(compression)}")
manifest_path = manifest_path(request)
checksum_path = tarball <> ".sha256"
build = BuildInfo.detect()
Lifecycle.run(:before_package, request.opts)
cache_path = PackageCache.path(manifest_path)
paths = {request.release_dir, tarball, manifest_path, compression}
release_info = {request.release, request.version}
package_cache = package_cache(cache_path, request, paths, release_info, build, compression)
{result, cache_hit?} =
package(paths, release_info, request, build, package_cache, checksum_path)
Lifecycle.run(:after_package, request.opts)
{result, %{cache_hit?: cache_hit?}}
end)
end
defp package(paths, release_info, request, build, package_cache, checksum_path) do
{_release_dir, tarball, manifest_path, _compression} = paths
cache_path = PackageCache.path(manifest_path)
cond do
package_cache_hit?(
request.opts,
cache_path,
tarball,
checksum_path,
manifest_path,
package_cache
) ->
checksum = checksum_from_sidecar!(checksum_path)
PackageCache.write!(
cache_path,
PackageCache.put_package(package_cache, tarball, checksum)
)
manifest = cached_manifest(tarball, checksum, release_info, request, build)
{{tarball, manifest_path, manifest}, true}
reusable_package?(request.opts, cache_path, package_cache) ->
{reuse_package!(paths, release_info, request, build, package_cache), true}
true ->
{write_package!(paths, release_info, request, build, package_cache), false}
end
end
defp package_cache(
cache_path,
request,
{_release_dir, tarball, manifest_path, _compression},
{release, version},
build,
compression
) do
artifact_context = {
Mix.Project.config()[:app],
release,
version,
Mix.env(),
Path.expand(tarball),
Path.expand(manifest_path),
Keyword.get(request.opts, :port),
Keyword.get(request.opts, :health_path),
Keyword.get(request.opts, :env_clear, %{}),
Keyword.get(request.opts, :env_secret, []),
request.target,
build,
request.assets,
compression
}
PackageCache.fingerprint(cache_path, request.release_dir, artifact_context, compression)
end
defp cached_manifest(tarball, checksum, {release, version}, request, build) do
compression = package_compression(request.opts)
artifact = ArtifactInfo.new(tarball, checksum, tarball <> ".sha256", compression)
new_manifest(tarball, release, version, request, build, artifact)
end
defp reusable_package?(opts, cache_path, package_cache) do
opts
|> Keyword.get(:package, [])
|> Package.reuse() != false and
match?(
{:ok, _tarball, _checksum},
PackageCache.reuse_source(cache_path, package_cache.content_fingerprint)
)
end
defp reuse_package!(
{_release_dir, tarball, manifest_path, _compression},
release_info,
request,
build,
package_cache
) do
cache_path = PackageCache.path(manifest_path)
{:ok, source_tarball, checksum} =
PackageCache.reuse_source(cache_path, package_cache.content_fingerprint)
copy_reused_tarball!(source_tarball, tarball, package_reuse(request.opts))
checksum_path = Checksum.write_sidecar!(tarball, checksum)
compression = package_compression(request.opts)
artifact = ArtifactInfo.new(tarball, checksum, checksum_path, compression)
{release, version} = release_info
manifest = new_manifest(tarball, release, version, request, build, artifact)
Manifest.write!(manifest, manifest_path)
PackageCache.write!(cache_path, PackageCache.put_package(package_cache, tarball, checksum))
{tarball, manifest_path, manifest}
end
defp write_package!(
{release_dir, tarball, manifest_path, compression},
{release, version},
request,
build,
package_cache
) do
Artifact.create_tarball!(release_dir, tarball, compression)
checksum = Checksum.sha256_file!(tarball)
checksum_path = Checksum.write_sidecar!(tarball, checksum)
artifact = ArtifactInfo.new(tarball, checksum, checksum_path, compression)
manifest = new_manifest(tarball, release, version, request, build, artifact)
Manifest.write!(manifest, manifest_path)
PackageCache.write!(
PackageCache.path(manifest_path),
PackageCache.put_package(package_cache, tarball, checksum)
)
{tarball, manifest_path, manifest}
end
defp new_manifest(tarball, release, version, request, build, artifact) do
Manifest.new(
app: Mix.Project.config()[:app] |> to_string(),
release: release,
version: version,
mix_env: Mix.env(),
tarball: tarball,
port: Keyword.get(request.opts, :port),
health_path: Keyword.get(request.opts, :health_path),
env_clear: Keyword.get(request.opts, :env_clear, %{}),
env_secret: Keyword.get(request.opts, :env_secret, []),
artifact: artifact,
target: request.target,
build: build,
assets: request.assets
)
end
defp manifest_path(request) do
Path.join(
request.out_dir,
"#{manifest_name(request.release, request.target, Keyword.get(request.opts, :target_suffix?, false))}.etf"
)
end
defp package_compression(opts) do
opts |> Keyword.get(:package, []) |> Package.compression()
end
defp package_reuse(opts) do
opts |> Keyword.get(:package, []) |> Package.reuse()
end
defp checksum_from_sidecar!(checksum_path) do
checksum_path |> File.read!() |> String.split() |> List.first()
end
defp copy_reused_tarball!(source, source, _reuse), do: :ok
defp copy_reused_tarball!(source, target, :copy) do
target |> Path.dirname() |> File.mkdir_p!()
File.cp!(source, target)
end
defp copy_reused_tarball!(source, target, :hardlink) do
target |> Path.dirname() |> File.mkdir_p!()
File.rm(target)
case File.ln(source, target) do
:ok -> :ok
{:error, _reason} -> File.cp!(source, target)
end
end
defp package_extension(:gzip), do: ".tar.gz"
defp package_extension(:none), do: ".tar"
defp package_cache_hit?(opts, cache_path, tarball, checksum_path, manifest_path, fingerprint) do
opts
|> Keyword.get(:package, [])
|> Package.cache?() and
PackageCache.hit?(cache_path, tarball, checksum_path, manifest_path, fingerprint)
end
defp manifest_name(release, _target, false), do: release
defp manifest_name(release, target, true), do: "#{release}-#{ReleaseKit.Target.suffix(target)}"
defp metadata(request) do
%{release: request.release, version: request.version, mix_env: Mix.env()}
end
end