From f625c56505546d0c08ebcfcb7d875cf6fdb91078 Mon Sep 17 00:00:00 2001 From: AJ Foster Date: Mon, 13 Dec 2021 22:29:06 -0500 Subject: [PATCH 1/4] Add hex.registry add command --- lib/mix/tasks/hex.registry.ex | 225 ++++++++++ src/mix_hex_registry.erl | 14 + test/mix/tasks/hex.registry_test.exs | 600 ++++++++++++++++++--------- 3 files changed, 647 insertions(+), 192 deletions(-) diff --git a/lib/mix/tasks/hex.registry.ex b/lib/mix/tasks/hex.registry.ex index 159b367c..3c5e8528 100644 --- a/lib/mix/tasks/hex.registry.ex +++ b/lib/mix/tasks/hex.registry.ex @@ -58,6 +58,26 @@ defmodule Mix.Tasks.Hex.Registry do * `--name` - The name of the registry * `--private-key` - Path to the private key + + ## Add a package + + $ mix hex.registry add PUBLIC_DIR PACKAGE1 PACKAGE2 ... + + To add one or more packages to an existing registry, supply the public directory of the registry + and paths to the new packages. This action also requires the private key used to generate the + original registry: + + $ mix hex.registry add public --private-key=private_key.pem foo-1.0.0.tar + * reading public/name + * reading public/versions + * moving foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar + * reading public/packages/foo + * updating public/packages/foo + * updating public/names + * updating public/versions + + Supplying a `--name` is optional. If given, an error will be raised if the existing registry's + name is different than the supplied value. """ @impl true def run(args) do @@ -65,6 +85,9 @@ defmodule Mix.Tasks.Hex.Registry do {opts, args} = OptionParser.parse!(args, strict: @switches) case args do + ["add", public_dir, package | additional_packages] -> + add(public_dir, [package | additional_packages], opts) + ["build", public_dir] -> build(public_dir, opts) @@ -72,6 +95,7 @@ defmodule Mix.Tasks.Hex.Registry do Mix.raise(""" Invalid arguments, expected one of: + mix hex.registry add PUBLIC_DIR PACKAGE mix hex.registry build PUBLIC_DIR """) end @@ -80,10 +104,102 @@ defmodule Mix.Tasks.Hex.Registry do @impl true def tasks() do [ + {"add PUBLIC_DIR PACKAGE", "Add package to a local registry"}, {"build PUBLIC_DIR", "Build a local registry"} ] end + ## Add + + defp add(public_dir, packages, opts) do + repo_name_or_nil = opts[:name] + private_key_path = opts[:private_key] || raise "missing --private-key" + private_key = private_key_path |> File.read!() |> decode_private_key() + add(repo_name_or_nil, public_dir, private_key, packages) + end + + defp add(repo_name_or_nil, public_dir, private_key, packages) do + public_key = ensure_public_key(private_key, public_dir) + + existing_names = + read_names!(repo_name_or_nil, public_dir, public_key) + |> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end) + |> Enum.into(%{}) + + existing_versions = + read_versions!(repo_name_or_nil, public_dir, public_key) + |> Enum.map(fn %{name: name, versions: versions} -> + {name, %{updated_at: existing_names[name], versions: versions}} + end) + |> Enum.into(%{}) + + tarball_dir = Path.join(public_dir, "tarballs") + create_directory(tarball_dir) + repo_name = repo_name_or_nil || read_repository_name!(public_dir, private_key) + + paths_per_name = + packages + |> Enum.map(fn path -> move_file!(path, tarball_dir) end) + |> Enum.group_by(fn path -> + [name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true) + name + end) + + versions = + Enum.map(paths_per_name, fn {name, paths} -> + existing_releases = read_package(repo_name, public_dir, public_key, name) + + releases = + paths + |> Enum.map(&build_release(repo_name, &1)) + |> Enum.concat(existing_releases) + |> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt)) + |> Enum.uniq_by(& &1.version) + + updated_at = + paths + |> Enum.map(&File.stat!(&1).mtime) + |> Enum.sort() + |> Enum.at(-1) + + previous_updated_at = get_in(existing_names, [name, :updated_at, :seconds]) + updated_at = %{seconds: max_updated_at(previous_updated_at, updated_at), nanos: 0} + + package = + :mix_hex_registry.build_package( + %{repository: repo_name, name: name, releases: releases}, + private_key + ) + + write_file("#{public_dir}/packages/#{name}", package) + versions = Enum.map(releases, & &1.version) + {name, %{updated_at: updated_at, versions: versions}} + end) + |> Enum.into(%{}) + + versions = Map.merge(existing_versions, versions) + + names = + for {name, %{updated_at: updated_at}} <- versions do + %{name: name, updated_at: updated_at} + end + + payload = %{repository: repo_name, packages: names} + names = :mix_hex_registry.build_names(payload, private_key) + write_file("#{public_dir}/names", names) + + versions = + for {name, %{versions: versions}} <- versions do + %{name: name, versions: versions} + end + + payload = %{repository: repo_name, packages: versions} + versions = :mix_hex_registry.build_versions(payload, private_key) + write_file("#{public_dir}/versions", versions) + end + + ## Build + defp build(public_dir, opts) do repo_name = opts[:name] || raise "missing --name" private_key_path = opts[:private_key] || raise "missing --private-key" @@ -151,6 +267,8 @@ defmodule Mix.Tasks.Hex.Registry do write_file("#{public_dir}/versions", versions) end + ## Registry utilities + @unix_epoch :calendar.datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) @doc false @@ -158,6 +276,13 @@ defmodule Mix.Tasks.Hex.Registry do :calendar.datetime_to_gregorian_seconds(erl_datetime) - @unix_epoch end + defp max_updated_at(previous_as_unix_or_nil, nil), do: previous_as_unix_or_nil + defp max_updated_at(nil, current_as_datetime), do: to_unix(current_as_datetime) + + defp max_updated_at(previous_as_unix, current_as_datetime) do + max(previous_as_unix, to_unix(current_as_datetime)) + end + defp build_release(repo_name, tarball_path) do tarball = File.read!(tarball_path) {:ok, result} = :mix_hex_tarball.unpack(tarball, :memory) @@ -206,8 +331,80 @@ defmodule Mix.Tasks.Hex.Registry do {:error, :enoent} -> write_file(path, encoded_public_key) end + + encoded_public_key end + defp read_names!(repo_name, public_dir, public_key) do + path = Path.join(public_dir, "names") + payload = read_file!(path) + repo_name_or_no_verify = repo_name || :no_verify + + case :mix_hex_registry.unpack_names(payload, repo_name_or_no_verify, public_key) do + {:ok, names} -> + names + + _ -> + Mix.raise(""" + Invalid package name manifest at #{path} + + Is the repository name #{repo_name} correct? + """) + end + end + + defp read_versions!(repo_name, public_dir, public_key) do + path = Path.join(public_dir, "versions") + payload = read_file!(path) + repo_name_or_no_verify = repo_name || :no_verify + + case :mix_hex_registry.unpack_versions(payload, repo_name_or_no_verify, public_key) do + {:ok, versions} -> + versions + + _ -> + Mix.raise(""" + Invalid package version manifest at #{path} + + Is the repository name #{repo_name} correct? + """) + end + end + + defp read_repository_name!(public_dir, public_key) do + path = Path.join(public_dir, "names") + payload = read_file!(path) + + case :mix_hex_registry.get_repository_name(payload, public_key) do + {:ok, repo_name} -> + repo_name + + _ -> + Mix.raise(""" + Invalid package name manifest at #{path} + + Is the public key correct? + """) + end + end + + defp read_package(repo_name, public_dir, public_key, package_name) do + path = Path.join([public_dir, "packages", package_name]) + + case read_file(path) do + {:ok, payload} -> + case :mix_hex_registry.unpack_package(payload, repo_name, package_name, public_key) do + {:ok, package} -> package + _ -> [] + end + + _ -> + [] + end + end + + ## File utilities + defp create_directory(path) do unless File.dir?(path) do Hex.Shell.info(["* creating ", path]) @@ -215,6 +412,26 @@ defmodule Mix.Tasks.Hex.Registry do end end + defp read_file!(path) do + if File.exists?(path) do + Hex.Shell.info(["* reading ", path]) + else + Mix.raise("Error reading file #{path}") + end + + File.read!(path) + end + + defp read_file(path) do + if File.exists?(path) do + Hex.Shell.info(["* reading ", path]) + else + Hex.Shell.info(["* skipping ", path]) + end + + File.read(path) + end + defp write_file(path, data) do if File.exists?(path) do Hex.Shell.info(["* updating ", path]) @@ -226,6 +443,14 @@ defmodule Mix.Tasks.Hex.Registry do File.write!(path, data) end + defp move_file!(path, destination_dir) do + file = Path.basename(path) + destination_file = Path.join(destination_dir, file) + Hex.Shell.info(["* moving ", path, " -> ", destination_file]) + File.rename!(path, destination_file) + destination_file + end + defp remove_file(path) do Hex.Shell.info(["* removing ", path]) File.rm!(path) diff --git a/src/mix_hex_registry.erl b/src/mix_hex_registry.erl index 46f2aadc..c7ae8be2 100644 --- a/src/mix_hex_registry.erl +++ b/src/mix_hex_registry.erl @@ -8,6 +8,7 @@ decode_names/2, build_names/2, unpack_names/3, + get_repository_name/2, encode_versions/1, decode_versions/2, build_versions/2, @@ -62,6 +63,19 @@ decode_names(Payload, Repository) -> {error, unverified} end. +%% @doc +%% Get the encoded repository name. +get_repository_name(Payload, PublicKey) -> + case decode_and_verify_signed(zlib:gunzip(Payload), PublicKey) of + {ok, Names} -> decode_repository_name(Names); + Other -> Other + end. + +%% @private +decode_repository_name(Payload) -> + #{repository := Repository} = mix_hex_pb_names:decode_msg(Payload, 'Names'), + {ok, Repository}. + %% @doc %% Builds versions resource. build_versions(Versions, PrivateKey) -> diff --git a/test/mix/tasks/hex.registry_test.exs b/test/mix/tasks/hex.registry_test.exs index c2e1d61c..60eb61e0 100644 --- a/test/mix/tasks/hex.registry_test.exs +++ b/test/mix/tasks/hex.registry_test.exs @@ -1,201 +1,417 @@ defmodule Mix.Tasks.Hex.RegistryTest do use HexTest.Case - test "build" do - in_tmp(fn -> - bypass = setup_bypass() - - 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") - flush() + describe "add" do + test "adds a single package to an empty registry" do + in_tmp(fn -> + bypass = setup_bypass() - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/public_key"]} - assert_received {:mix_shell, :info, ["* creating public/tarballs"]} - assert_received {:mix_shell, :info, ["* creating public/names"]} - assert_received {:mix_shell, :info, ["* creating public/versions"]} - refute_received _ - - config = %{ - :mix_hex_core.default_config() - | repo_url: "http://localhost:#{bypass.port}", - repo_verify: false, - repo_verify_origin: false - } - - assert {:ok, {200, _, []}} = :mix_hex_repo.get_names(config) - assert {:ok, {200, _, []}} = :mix_hex_repo.get_versions(config) - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) - File.write!("public/tarballs/foo-0.10.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names - - assert updated_at == - "public/tarballs/foo-0.10.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) - File.write!("public/tarballs/foo-0.9.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names - - assert updated_at == - "public/tarballs/foo-0.9.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] - - # Versions with hyphen - {:ok, %{tarball: tarball}} = - :mix_hex_tarball.create(%{name: "foo", version: "1.0.0-rc"}, []) - - File.write!("public/tarballs/foo-1.0.0-rc.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "foo", updated_at: _}] = names - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0", "1.0.0-rc"]}] - - # Re-generating private key - 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") - flush() - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* public key at public/public_key does not" <> _]} - assert_received {:mix_shell, :info, ["* updating public/public_key"]} - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - # Package with deps - metadata = %{ - name: "bar", - version: "0.1.0", - requirements: %{ - "foo" => %{ - "app" => "foo", - "optional" => false, - "repository" => "acme", - "requirement" => "~> 0.1.0" - }, - "baz" => %{ - "app" => "baz", - "optional" => false, - "repository" => "external", - "requirement" => "~> 0.1.0" + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir!("subdir") + File.write!("subdir/foo-0.10.0.tar", tarball) + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.10.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]} + + assert_received {:mix_shell, :info, ["* skipping public/packages/foo"]} + assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.10.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] + end) + end + + test "adds a single package to a populated registry" do + in_tmp(fn -> + bypass = setup_bypass() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir_p!("public/tarballs") + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + File.mkdir!("subdir") + File.write!("subdir/foo-0.9.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} + + assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] + end) + end + + test "adds multiple packages" do + in_tmp(fn -> + bypass = setup_bypass() + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.mkdir_p!("public/tarballs") + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + flush() + + {:ok, %{tarball: bar_tarball}} = + :mix_hex_tarball.create(%{name: "bar", version: "0.1.0"}, []) + + {:ok, %{tarball: foo_tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + + File.mkdir!("subdir") + File.write!("subdir/bar-0.1.0.tar", bar_tarball) + File.write!("subdir/foo-0.9.0.tar", foo_tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar subdir/bar-0.1.0.tar) + ) + + assert_received {:mix_shell, :info, ["* reading public/names"]} + assert_received {:mix_shell, :info, ["* reading public/versions"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} + + assert_received {:mix_shell, :info, + ["* moving subdir/bar-0.1.0.tar -> public/tarballs/bar-0.1.0.tar"]} + + assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* skipping public/packages/bar"]} + assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + + assert [ + %{name: "bar", updated_at: %{seconds: bar_updated_at}}, + %{name: "foo", updated_at: %{seconds: foo_updated_at}} + ] = names + + assert bar_updated_at == + "public/tarballs/bar-0.1.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert foo_updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + + assert versions == [ + %{name: "bar", retired: [], versions: ["0.1.0"]}, + %{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]} + ] + end) + end + end + + describe "build" do + test "builds a registry" do + in_tmp(fn -> + bypass = setup_bypass() + + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + flush() + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/public_key"]} + assert_received {:mix_shell, :info, ["* creating public/tarballs"]} + assert_received {:mix_shell, :info, ["* creating public/names"]} + assert_received {:mix_shell, :info, ["* creating public/versions"]} + refute_received _ + + config = %{ + :mix_hex_core.default_config() + | repo_url: "http://localhost:#{bypass.port}", + repo_verify: false, + repo_verify_origin: false + } + + assert {:ok, {200, _, []}} = :mix_hex_repo.get_names(config) + assert {:ok, {200, _, []}} = :mix_hex_repo.get_versions(config) + + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) + + File.write!("public/tarballs/foo-0.10.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.10.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.10.0"]}] + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) + File.write!("public/tarballs/foo-0.9.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: %{seconds: updated_at}}] = names + + assert updated_at == + "public/tarballs/foo-0.9.0.tar" + |> File.stat!() + |> Map.fetch!(:mtime) + |> Mix.Tasks.Hex.Registry.to_unix() + + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] + + # Versions with hyphen + {:ok, %{tarball: tarball}} = + :mix_hex_tarball.create(%{name: "foo", version: "1.0.0-rc"}, []) + + File.write!("public/tarballs/foo-1.0.0-rc.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "foo", updated_at: _}] = names + assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) + + assert versions == [ + %{name: "foo", retired: [], versions: ["0.9.0", "0.10.0", "1.0.0-rc"]} + ] + + # Re-generating private key + 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") + flush() + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* public key at public/public_key does not" <> _]} + assert_received {:mix_shell, :info, ["* updating public/public_key"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + # Package with deps + metadata = %{ + name: "bar", + version: "0.1.0", + requirements: %{ + "foo" => %{ + "app" => "foo", + "optional" => false, + "repository" => "acme", + "requirement" => "~> 0.1.0" + }, + "baz" => %{ + "app" => "baz", + "optional" => false, + "repository" => "external", + "requirement" => "~> 0.1.0" + } } } - } - - {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(metadata, []) - File.write!("public/tarballs/bar-0.1.0.tar", tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - assert [%{name: "bar", updated_at: _}, %{name: "foo", updated_at: _}] = names - assert {:ok, {200, _, [package]}} = :mix_hex_repo.get_package(config, "bar") - - assert package.dependencies == [ - %{ - app: "baz", - optional: false, - package: "baz", - requirement: "~> 0.1.0", - repository: "external" - }, - %{ - app: "foo", - optional: false, - package: "foo", - requirement: "~> 0.1.0" - } - ] - - # Removing all package releases - File.rm!("public/tarballs/foo-0.9.0.tar") - File.rm!("public/tarballs/foo-0.10.0.tar") - File.rm!("public/tarballs/foo-1.0.0-rc.tar") - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - assert_received {:mix_shell, :info, ["* updating public/packages/bar"]} - assert_received {:mix_shell, :info, ["* removing public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - end) + + {:ok, %{tarball: tarball}} = :mix_hex_tarball.create(metadata, []) + File.write!("public/tarballs/bar-0.1.0.tar", tarball) + + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + + assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) + assert [%{name: "bar", updated_at: _}, %{name: "foo", updated_at: _}] = names + assert {:ok, {200, _, [package]}} = :mix_hex_repo.get_package(config, "bar") + + assert package.dependencies == [ + %{ + app: "baz", + optional: false, + package: "baz", + requirement: "~> 0.1.0", + repository: "external" + }, + %{ + app: "foo", + optional: false, + package: "foo", + requirement: "~> 0.1.0" + } + ] + + # Removing all package releases + File.rm!("public/tarballs/foo-0.9.0.tar") + File.rm!("public/tarballs/foo-0.10.0.tar") + File.rm!("public/tarballs/foo-1.0.0-rc.tar") + Mix.Task.reenable("hex.registry") + + Mix.Task.run( + "hex.registry", + ~w(build public --name acme --private-key private_key.pem) + ) + + assert_received {:mix_shell, :info, ["* updating public/packages/bar"]} + assert_received {:mix_shell, :info, ["* removing public/packages/foo"]} + assert_received {:mix_shell, :info, ["* updating public/names"]} + assert_received {:mix_shell, :info, ["* updating public/versions"]} + refute_received _ + end) + end end defp setup_bypass() do From 1edd745231ea64730c81592fa5c64dfbeef4a60f Mon Sep 17 00:00:00 2001 From: AJ Foster <2789166+aj-foster@users.noreply.github.com> Date: Thu, 10 Feb 2022 21:28:32 -0500 Subject: [PATCH 2/4] Require --name for hex.registry add --- lib/mix/tasks/hex.registry.ex | 30 ++++++------------------------ src/mix_hex_registry.erl | 16 +--------------- 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/lib/mix/tasks/hex.registry.ex b/lib/mix/tasks/hex.registry.ex index 3c5e8528..802a77ee 100644 --- a/lib/mix/tasks/hex.registry.ex +++ b/lib/mix/tasks/hex.registry.ex @@ -112,22 +112,22 @@ defmodule Mix.Tasks.Hex.Registry do ## Add defp add(public_dir, packages, opts) do - repo_name_or_nil = opts[:name] - private_key_path = opts[:private_key] || raise "missing --private-key" + repo_name = opts[:name] || Mix.raise("missing --name") + private_key_path = opts[:private_key] || Mix.raise("missing --private-key") private_key = private_key_path |> File.read!() |> decode_private_key() - add(repo_name_or_nil, public_dir, private_key, packages) + add(repo_name, public_dir, private_key, packages) end - defp add(repo_name_or_nil, public_dir, private_key, packages) do + defp add(repo_name, public_dir, private_key, packages) do public_key = ensure_public_key(private_key, public_dir) existing_names = - read_names!(repo_name_or_nil, public_dir, public_key) + read_names!(repo_name, public_dir, public_key) |> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end) |> Enum.into(%{}) existing_versions = - read_versions!(repo_name_or_nil, public_dir, public_key) + read_versions!(repo_name, public_dir, public_key) |> Enum.map(fn %{name: name, versions: versions} -> {name, %{updated_at: existing_names[name], versions: versions}} end) @@ -135,7 +135,6 @@ defmodule Mix.Tasks.Hex.Registry do tarball_dir = Path.join(public_dir, "tarballs") create_directory(tarball_dir) - repo_name = repo_name_or_nil || read_repository_name!(public_dir, private_key) paths_per_name = packages @@ -371,23 +370,6 @@ defmodule Mix.Tasks.Hex.Registry do end end - defp read_repository_name!(public_dir, public_key) do - path = Path.join(public_dir, "names") - payload = read_file!(path) - - case :mix_hex_registry.get_repository_name(payload, public_key) do - {:ok, repo_name} -> - repo_name - - _ -> - Mix.raise(""" - Invalid package name manifest at #{path} - - Is the public key correct? - """) - end - end - defp read_package(repo_name, public_dir, public_key, package_name) do path = Path.join([public_dir, "packages", package_name]) diff --git a/src/mix_hex_registry.erl b/src/mix_hex_registry.erl index c7ae8be2..4375f28c 100644 --- a/src/mix_hex_registry.erl +++ b/src/mix_hex_registry.erl @@ -1,4 +1,4 @@ -%% Vendored from hex_core v0.8.2, do not edit manually +%% Vendored from hex_core v0.8.0, do not edit manually %% @doc %% Functions for encoding and decoding Hex registries. @@ -8,7 +8,6 @@ decode_names/2, build_names/2, unpack_names/3, - get_repository_name/2, encode_versions/1, decode_versions/2, build_versions/2, @@ -63,19 +62,6 @@ decode_names(Payload, Repository) -> {error, unverified} end. -%% @doc -%% Get the encoded repository name. -get_repository_name(Payload, PublicKey) -> - case decode_and_verify_signed(zlib:gunzip(Payload), PublicKey) of - {ok, Names} -> decode_repository_name(Names); - Other -> Other - end. - -%% @private -decode_repository_name(Payload) -> - #{repository := Repository} = mix_hex_pb_names:decode_msg(Payload, 'Names'), - {ok, Repository}. - %% @doc %% Builds versions resource. build_versions(Versions, PrivateKey) -> From fe833def0f5f12af237cacc719e1f9f67f0faca9 Mon Sep 17 00:00:00 2001 From: AJ Foster <2789166+aj-foster@users.noreply.github.com> Date: Thu, 10 Feb 2022 23:46:12 -0500 Subject: [PATCH 3/4] Update documentation related to `--name` flag --- lib/mix/tasks/hex.registry.ex | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/mix/tasks/hex.registry.ex b/lib/mix/tasks/hex.registry.ex index 802a77ee..0ab0966b 100644 --- a/lib/mix/tasks/hex.registry.ex +++ b/lib/mix/tasks/hex.registry.ex @@ -61,13 +61,13 @@ defmodule Mix.Tasks.Hex.Registry do ## Add a package - $ mix hex.registry add PUBLIC_DIR PACKAGE1 PACKAGE2 ... + $ mix hex.registry add PUBLIC_DIR PACKAGE - To add one or more packages to an existing registry, supply the public directory of the registry - and paths to the new packages. This action also requires the private key used to generate the - original registry: + To add a package to an existing registry, supply the public directory of the registry and path to + the new packages. This action also requires the name of the registry and the private key + originally used to generate it: - $ mix hex.registry add public --private-key=private_key.pem foo-1.0.0.tar + $ mix hex.registry add public --name=acme --private-key=private_key.pem foo-1.0.0.tar * reading public/name * reading public/versions * moving foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar @@ -76,8 +76,6 @@ defmodule Mix.Tasks.Hex.Registry do * updating public/names * updating public/versions - Supplying a `--name` is optional. If given, an error will be raised if the existing registry's - name is different than the supplied value. """ @impl true def run(args) do From cd00ed87448e93a50aa069e53f7b750e63992eff Mon Sep 17 00:00:00 2001 From: AJ Foster <2789166+aj-foster@users.noreply.github.com> Date: Fri, 11 Nov 2022 23:55:07 -0500 Subject: [PATCH 4/4] Restrict hex.registry add to a single package --- lib/mix/tasks/hex.registry.ex | 175 ++++++++++----------------- test/mix/tasks/hex.registry_test.exs | 99 +-------------- 2 files changed, 68 insertions(+), 206 deletions(-) diff --git a/lib/mix/tasks/hex.registry.ex b/lib/mix/tasks/hex.registry.ex index 0ab0966b..fc5da35c 100644 --- a/lib/mix/tasks/hex.registry.ex +++ b/lib/mix/tasks/hex.registry.ex @@ -64,14 +64,11 @@ defmodule Mix.Tasks.Hex.Registry do $ mix hex.registry add PUBLIC_DIR PACKAGE To add a package to an existing registry, supply the public directory of the registry and path to - the new packages. This action also requires the name of the registry and the private key - originally used to generate it: + the new package. This action also requires the name of the registry and the private key originally + used to generate it: $ mix hex.registry add public --name=acme --private-key=private_key.pem foo-1.0.0.tar - * reading public/name - * reading public/versions - * moving foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar - * reading public/packages/foo + * copying foo-1.0.0.tar -> public/tarballs/foo-1.0.0.tar * updating public/packages/foo * updating public/names * updating public/versions @@ -83,8 +80,8 @@ defmodule Mix.Tasks.Hex.Registry do {opts, args} = OptionParser.parse!(args, strict: @switches) case args do - ["add", public_dir, package | additional_packages] -> - add(public_dir, [package | additional_packages], opts) + ["add", public_dir, package] -> + add(public_dir, package, opts) ["build", public_dir] -> build(public_dir, opts) @@ -109,90 +106,72 @@ defmodule Mix.Tasks.Hex.Registry do ## Add - defp add(public_dir, packages, opts) do + defp add(public_dir, package, opts) do repo_name = opts[:name] || Mix.raise("missing --name") private_key_path = opts[:private_key] || Mix.raise("missing --private-key") private_key = private_key_path |> File.read!() |> decode_private_key() - add(repo_name, public_dir, private_key, packages) + add(repo_name, public_dir, private_key, package) end - defp add(repo_name, public_dir, private_key, packages) do + defp add(repo_name, public_dir, private_key, package) do public_key = ensure_public_key(private_key, public_dir) - - existing_names = - read_names!(repo_name, public_dir, public_key) - |> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end) - |> Enum.into(%{}) - - existing_versions = - read_versions!(repo_name, public_dir, public_key) - |> Enum.map(fn %{name: name, versions: versions} -> - {name, %{updated_at: existing_names[name], versions: versions}} - end) - |> Enum.into(%{}) - tarball_dir = Path.join(public_dir, "tarballs") create_directory(tarball_dir) - paths_per_name = - packages - |> Enum.map(fn path -> move_file!(path, tarball_dir) end) - |> Enum.group_by(fn path -> - [name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true) - name - end) + path = copy_file(package, tarball_dir) + [package_name | _rest] = String.split(Path.basename(path), ["-", ".tar"], trim: true) - versions = - Enum.map(paths_per_name, fn {name, paths} -> - existing_releases = read_package(repo_name, public_dir, public_key, name) + existing_package_releases = read_package(repo_name, public_dir, public_key, package_name) + new_package_release = build_release(repo_name, package) - releases = - paths - |> Enum.map(&build_release(repo_name, &1)) - |> Enum.concat(existing_releases) - |> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt)) - |> Enum.uniq_by(& &1.version) + package_releases = + [new_package_release | existing_package_releases] + |> Enum.sort(&(Hex.Version.compare(&1.version, &2.version) == :lt)) + |> Enum.uniq_by(& &1.version) - updated_at = - paths - |> Enum.map(&File.stat!(&1).mtime) - |> Enum.sort() - |> Enum.at(-1) + package = + :mix_hex_registry.build_package( + %{repository: repo_name, name: package_name, releases: package_releases}, + private_key + ) - previous_updated_at = get_in(existing_names, [name, :updated_at, :seconds]) - updated_at = %{seconds: max_updated_at(previous_updated_at, updated_at), nanos: 0} + write_file("#{public_dir}/packages/#{package_name}", package) - package = - :mix_hex_registry.build_package( - %{repository: repo_name, name: name, releases: releases}, - private_key - ) + existing_names = + read_names(repo_name, public_dir, public_key) + |> Enum.map(fn %{name: name, updated_at: updated_at} -> {name, updated_at} end) + |> Enum.into(%{}) - write_file("#{public_dir}/packages/#{name}", package) - versions = Enum.map(releases, & &1.version) - {name, %{updated_at: updated_at, versions: versions}} + previous_updated_at = get_in(existing_names, [package_name, :updated_at, :seconds]) + new_version_updated_at = File.stat!(path).mtime + updated_at = %{seconds: max_updated_at(previous_updated_at, new_version_updated_at), nanos: 0} + package_versions = Enum.map(package_releases, & &1.version) + + repo_versions = + read_versions(repo_name, public_dir, public_key) + |> Enum.map(fn %{name: name, versions: versions} -> + {name, %{updated_at: existing_names[name], versions: versions}} end) |> Enum.into(%{}) + |> Map.put(package_name, %{updated_at: updated_at, versions: package_versions}) - versions = Map.merge(existing_versions, versions) - - names = - for {name, %{updated_at: updated_at}} <- versions do + repo_names = + Enum.map(repo_versions, fn {name, %{updated_at: updated_at}} -> %{name: name, updated_at: updated_at} - end + end) - payload = %{repository: repo_name, packages: names} - names = :mix_hex_registry.build_names(payload, private_key) - write_file("#{public_dir}/names", names) + payload = %{repository: repo_name, packages: repo_names} + repo_names = :mix_hex_registry.build_names(payload, private_key) + write_file("#{public_dir}/names", repo_names) - versions = - for {name, %{versions: versions}} <- versions do + repo_versions = + Enum.map(repo_versions, fn {name, %{versions: versions}} -> %{name: name, versions: versions} - end + end) - payload = %{repository: repo_name, packages: versions} - versions = :mix_hex_registry.build_versions(payload, private_key) - write_file("#{public_dir}/versions", versions) + payload = %{repository: repo_name, packages: repo_versions} + repo_versions = :mix_hex_registry.build_versions(payload, private_key) + write_file("#{public_dir}/versions", repo_versions) end ## Build @@ -332,46 +311,44 @@ defmodule Mix.Tasks.Hex.Registry do encoded_public_key end - defp read_names!(repo_name, public_dir, public_key) do + defp read_names(repo_name, public_dir, public_key) do path = Path.join(public_dir, "names") - payload = read_file!(path) + payload = File.read!(path) repo_name_or_no_verify = repo_name || :no_verify case :mix_hex_registry.unpack_names(payload, repo_name_or_no_verify, public_key) do {:ok, names} -> names - _ -> - Mix.raise(""" - Invalid package name manifest at #{path} - - Is the repository name #{repo_name} correct? - """) + {:error, reason} -> + Mix.raise( + "Invalid package name manifest at #{path}: #{inspect(reason)}." <> + "\n\nIs the repository name #{repo_name} correct?" + ) end end - defp read_versions!(repo_name, public_dir, public_key) do + defp read_versions(repo_name, public_dir, public_key) do path = Path.join(public_dir, "versions") - payload = read_file!(path) + payload = File.read!(path) repo_name_or_no_verify = repo_name || :no_verify case :mix_hex_registry.unpack_versions(payload, repo_name_or_no_verify, public_key) do {:ok, versions} -> versions - _ -> - Mix.raise(""" - Invalid package version manifest at #{path} - - Is the repository name #{repo_name} correct? - """) + {:error, reason} -> + Mix.raise( + "Invalid package version manifest at #{path}: #{inspect(reason)}." <> + "\n\nIs the repository name #{repo_name} correct?" + ) end end defp read_package(repo_name, public_dir, public_key, package_name) do path = Path.join([public_dir, "packages", package_name]) - case read_file(path) do + case File.read(path) do {:ok, payload} -> case :mix_hex_registry.unpack_package(payload, repo_name, package_name, public_key) do {:ok, package} -> package @@ -392,26 +369,6 @@ defmodule Mix.Tasks.Hex.Registry do end end - defp read_file!(path) do - if File.exists?(path) do - Hex.Shell.info(["* reading ", path]) - else - Mix.raise("Error reading file #{path}") - end - - File.read!(path) - end - - defp read_file(path) do - if File.exists?(path) do - Hex.Shell.info(["* reading ", path]) - else - Hex.Shell.info(["* skipping ", path]) - end - - File.read(path) - end - defp write_file(path, data) do if File.exists?(path) do Hex.Shell.info(["* updating ", path]) @@ -423,11 +380,11 @@ defmodule Mix.Tasks.Hex.Registry do File.write!(path, data) end - defp move_file!(path, destination_dir) do + defp copy_file(path, destination_dir) do file = Path.basename(path) destination_file = Path.join(destination_dir, file) - Hex.Shell.info(["* moving ", path, " -> ", destination_file]) - File.rename!(path, destination_file) + Hex.Shell.info(["* copying ", path, " -> ", destination_file]) + File.cp!(path, destination_file) destination_file end diff --git a/test/mix/tasks/hex.registry_test.exs b/test/mix/tasks/hex.registry_test.exs index 60eb61e0..c127a543 100644 --- a/test/mix/tasks/hex.registry_test.exs +++ b/test/mix/tasks/hex.registry_test.exs @@ -27,13 +27,9 @@ defmodule Mix.Tasks.Hex.RegistryTest do ~w(add public --name acme --private-key private_key.pem subdir/foo-0.10.0.tar) ) - assert_received {:mix_shell, :info, ["* reading public/names"]} - assert_received {:mix_shell, :info, ["* reading public/versions"]} - assert_received {:mix_shell, :info, - ["* moving subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]} + ["* copying subdir/foo-0.10.0.tar -> public/tarballs/foo-0.10.0.tar"]} - assert_received {:mix_shell, :info, ["* skipping public/packages/foo"]} assert_received {:mix_shell, :info, ["* creating public/packages/foo"]} assert_received {:mix_shell, :info, ["* updating public/names"]} assert_received {:mix_shell, :info, ["* updating public/versions"]} @@ -89,13 +85,9 @@ defmodule Mix.Tasks.Hex.RegistryTest do ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar) ) - assert_received {:mix_shell, :info, ["* reading public/names"]} - assert_received {:mix_shell, :info, ["* reading public/versions"]} - assert_received {:mix_shell, :info, - ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} + ["* copying subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} - assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} assert_received {:mix_shell, :info, ["* updating public/names"]} assert_received {:mix_shell, :info, ["* updating public/versions"]} @@ -121,93 +113,6 @@ defmodule Mix.Tasks.Hex.RegistryTest do assert versions == [%{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]}] end) end - - test "adds multiple packages" do - in_tmp(fn -> - bypass = setup_bypass() - - {:ok, %{tarball: tarball}} = - :mix_hex_tarball.create(%{name: "foo", version: "0.10.0"}, []) - - File.mkdir_p!("public/tarballs") - File.write!("public/tarballs/foo-0.10.0.tar", tarball) - 0 = Mix.shell().cmd("openssl genrsa -out private_key.pem") - - Mix.Task.run( - "hex.registry", - ~w(build public --name acme --private-key private_key.pem) - ) - - flush() - - {:ok, %{tarball: bar_tarball}} = - :mix_hex_tarball.create(%{name: "bar", version: "0.1.0"}, []) - - {:ok, %{tarball: foo_tarball}} = - :mix_hex_tarball.create(%{name: "foo", version: "0.9.0"}, []) - - File.mkdir!("subdir") - File.write!("subdir/bar-0.1.0.tar", bar_tarball) - File.write!("subdir/foo-0.9.0.tar", foo_tarball) - - Mix.Task.reenable("hex.registry") - - Mix.Task.run( - "hex.registry", - ~w(add public --name acme --private-key private_key.pem subdir/foo-0.9.0.tar subdir/bar-0.1.0.tar) - ) - - assert_received {:mix_shell, :info, ["* reading public/names"]} - assert_received {:mix_shell, :info, ["* reading public/versions"]} - - assert_received {:mix_shell, :info, - ["* moving subdir/foo-0.9.0.tar -> public/tarballs/foo-0.9.0.tar"]} - - assert_received {:mix_shell, :info, - ["* moving subdir/bar-0.1.0.tar -> public/tarballs/bar-0.1.0.tar"]} - - assert_received {:mix_shell, :info, ["* reading public/packages/foo"]} - assert_received {:mix_shell, :info, ["* updating public/packages/foo"]} - assert_received {:mix_shell, :info, ["* skipping public/packages/bar"]} - assert_received {:mix_shell, :info, ["* creating public/packages/bar"]} - assert_received {:mix_shell, :info, ["* updating public/names"]} - assert_received {:mix_shell, :info, ["* updating public/versions"]} - refute_received _ - - config = %{ - :mix_hex_core.default_config() - | repo_url: "http://localhost:#{bypass.port}", - repo_verify: false, - repo_verify_origin: false - } - - assert {:ok, {200, _, names}} = :mix_hex_repo.get_names(config) - - assert [ - %{name: "bar", updated_at: %{seconds: bar_updated_at}}, - %{name: "foo", updated_at: %{seconds: foo_updated_at}} - ] = names - - assert bar_updated_at == - "public/tarballs/bar-0.1.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert foo_updated_at == - "public/tarballs/foo-0.9.0.tar" - |> File.stat!() - |> Map.fetch!(:mtime) - |> Mix.Tasks.Hex.Registry.to_unix() - - assert {:ok, {200, _, versions}} = :mix_hex_repo.get_versions(config) - - assert versions == [ - %{name: "bar", retired: [], versions: ["0.1.0"]}, - %{name: "foo", retired: [], versions: ["0.9.0", "0.10.0"]} - ] - end) - end end describe "build" do