Skip to content

Commit 6aa5901

Browse files
committed
Unify the common code involved in installing archives and escripts
1 parent f20f7b2 commit 6aa5901

File tree

11 files changed

+235
-234
lines changed

11 files changed

+235
-234
lines changed

lib/mix/lib/mix/escript.ex

Lines changed: 0 additions & 16 deletions
This file was deleted.

lib/mix/lib/mix/local.ex

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,42 @@ defmodule Mix.Local do
33

44
@public_keys_html "https://s3.amazonaws.com/s3.hex.pm/installs/public_keys.html"
55

6+
@type item :: :archive | :escript
7+
68
@doc """
7-
The path for local archives.
9+
Returns the name for an archive or an escript, based on on the project config.
10+
11+
## Examples
12+
13+
iex> Mix.Local.name_for(:archive, [app: "foo", version: "0.1.0"])
14+
"foo-0.1.0.ez"
15+
16+
iex> Mix.Local.name_for(:escript, [escript: [name: "foo"]])
17+
"foo"
818
9-
Check `mix archive` for info.
1019
"""
11-
def archives_path do
12-
System.get_env("MIX_ARCHIVES") ||
13-
Path.join(Mix.Utils.mix_home, "archives")
20+
@spec name_for(item, Keyword.t) :: String.t
21+
def name_for(:archive, project) do
22+
version = if version = project[:version], do: "-#{version}"
23+
"#{project[:app]}#{version}.ez"
1424
end
1525

16-
@doc """
17-
The path for local escripts.
26+
def name_for(:escript, project) do
27+
case get_in(project, [:escript, :name]) do
28+
nil -> project[:app]
29+
name -> name
30+
end |> to_string()
31+
end
1832

19-
Check `mix escript` for info.
33+
@doc """
34+
The path for local archives or escripts.
2035
"""
21-
def escripts_path do
36+
@spec path_for(item) :: String.t
37+
def path_for(:archive) do
38+
System.get_env("MIX_ARCHIVES") || Path.join(Mix.Utils.mix_home, "archives")
39+
end
40+
41+
def path_for(:escript) do
2242
Path.join(Mix.Utils.mix_home, "escripts")
2343
end
2444

@@ -61,13 +81,13 @@ defmodule Mix.Local do
6181
end
6282

6383
defp archives(name, suffix) do
64-
archives_path()
84+
path_for(:archive)
6585
|> Path.join(name <> suffix)
6686
|> Path.wildcard
6787
end
6888

6989
defp archives_ebin do
70-
Path.join(archives_path(), "*.ez") |> Path.wildcard |> Enum.map(&Mix.Archive.ebin/1)
90+
Path.join(path_for(:archive), "*.ez") |> Path.wildcard |> Enum.map(&Mix.Archive.ebin/1)
7191
end
7292

7393
defp check_elixir_version_in_ebin(ebin) do

lib/mix/lib/mix/local/installer.ex

Lines changed: 146 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,65 +5,180 @@ defmodule Mix.Local.Installer do
55
"""
66

77
@doc """
8-
Print a list of items in a uniform way. Used for printing the list of installed archives and
9-
escripts.
8+
Checks that the argument given to install is supported by the respective module.
9+
"""
10+
@callback check_path_or_url(String.t) :: :ok | {:error, String.t}
1011

11-
## Options
12+
@doc """
13+
Returns a list of already installed version of the same archive or escript.
14+
"""
15+
@callback find_previous_versions(String.t, Path.t) :: [Path.t]
1216

13-
* `:empty_message` - the message to print when there are no items
14-
* `:footnote` - the message to print after the list
17+
@doc """
18+
Custom actions to be performed before the actual installation.
19+
"""
20+
@callback before_install(String.t, Path.t) :: :ok | {:error, String.t}
1521

22+
@doc """
23+
Custom actions to be performed after the installation has succeeded.
1624
"""
17-
def print_list([], options) do
18-
Mix.shell.info Keyword.get(options, :empty_message, "No items found.")
25+
@callback after_install(Path.t, [Path.t]) :: term
26+
27+
@doc """
28+
Common implementation of installation for archives and escripts.
29+
30+
Relies on a few callbacks provided by respective callback modules
31+
for customizing certain steps in the installation process.
32+
"""
33+
@spec install({module, atom}, OptionParser.argv, Keyword.t) :: boolean
34+
def install({module, name}, argv, switches) do
35+
{opts, args, _} = OptionParser.parse(argv, switches: switches)
36+
37+
case args do
38+
[src] ->
39+
with :ok <- check_argument(src), :ok <- module.check_path_or_url(src) do
40+
do_install({module, name}, src, opts)
41+
else
42+
{:error, message} -> Mix.raise message <> "\n" <> usage(name)
43+
end
44+
45+
[] ->
46+
src = Mix.Local.name_for(name, Mix.Project.config)
47+
if File.exists?(src) do
48+
do_install({module, name}, src, opts)
49+
else
50+
Mix.raise "Expected an #{name} to exist in the current directory " <>
51+
"or an argument to be given.\n#{usage(name)}"
52+
end
53+
54+
_ ->
55+
Mix.raise "Unexpected arguments.\n#{usage(name)}"
56+
end
1957
end
2058

21-
def print_list(items, options) do
22-
Enum.each items, fn item -> Mix.shell.info ["* ", item] end
23-
Mix.shell.info Keyword.get(options, :footnote, "")
59+
defp check_argument(arg) do
60+
if local_path?(arg) or file_url?(arg) do
61+
:ok
62+
else
63+
{:error, "Expected a local file path or a file URL."}
64+
end
2465
end
2566

26-
@doc """
27-
A common implementation for uninstalling archives, scripts, etc.
67+
defp local_path?(url_or_path) do
68+
File.regular?(url_or_path)
69+
end
70+
71+
defp file_url?(url_or_path) do
72+
URI.parse(url_or_path).scheme in ["http", "https"]
73+
end
74+
75+
defp usage(name), do: "Usage: mix #{name}.install <path or url>"
76+
77+
defp do_install({module, name}, src, opts) do
78+
src_basename = URI.parse(src).path
79+
dst_dir_path = Mix.Local.path_for(name)
80+
dst_file_path = Path.join(dst_dir_path, src_basename)
81+
previous_files = module.find_previous_versions(src, dst_file_path)
82+
83+
if opts[:force] || should_install?(name, src, previous_files) do
84+
case module.before_install(src, dst_file_path) do
85+
:ok -> :ok
86+
{:error, message} -> Mix.raise message
87+
end
88+
89+
case Mix.Utils.read_path(src, opts) do
90+
{:ok, binary} ->
91+
File.mkdir_p!(dst_dir_path)
92+
File.write!(dst_file_path, binary)
93+
94+
:badpath ->
95+
Mix.raise "Expected #{inspect src} to be a URL or a local file path"
2896

29-
## Options
97+
{:local, message} ->
98+
Mix.raise message
3099

31-
* `:item_name` - the name of the item being uninstalled. Also the name of the task which is used
32-
to print a list of all such items
33-
* `:item_plural` - plural of item name
100+
{kind, message} when kind in [:remote, :checksum] ->
101+
Mix.raise """
102+
#{message}
34103
104+
Could not fetch #{name} at:
105+
106+
#{src}
107+
108+
Please download the #{name} above manually to your current directory and run:
109+
110+
mix #{name}.install ./#{src_basename}
111+
"""
112+
end
113+
114+
Mix.shell.info [:green, "* creating ", :reset, Path.relative_to_cwd(dst_file_path)]
115+
_ = module.after_install(dst_file_path, previous_files)
116+
true
117+
else
118+
false
119+
end
120+
end
121+
122+
defp should_install?(name, src, previous_files) do
123+
message = case previous_files do
124+
[] ->
125+
"Are you sure you want to install #{name} #{inspect src}?"
126+
[file] ->
127+
"Found existing #{name}: #{file}.\n" <>
128+
"Are you sure you want to replace it with #{inspect src}?"
129+
files ->
130+
"Found existing #{name}s: #{Enum.map_join(files, ", ", &Path.basename/1)}.\n" <>
131+
"Are you sure you want to replace them with #{inspect src}?"
132+
end
133+
Mix.shell.yes?(message)
134+
end
135+
136+
@doc """
137+
Print a list of items in a uniform way. Used for printing the list of installed archives and
138+
escripts.
139+
"""
140+
@spec print_list(atom, [String.t]) :: :ok
141+
def print_list(type, []) do
142+
Mix.shell.info "No #{type}s currently installed."
143+
end
144+
145+
def print_list(type, items) do
146+
Enum.each items, fn item -> Mix.shell.info ["* ", item] end
147+
item_name = String.capitalize("#{type}")
148+
Mix.shell.info "#{item_name}s installed at: #{Mix.Local.path_for(type)}"
149+
end
150+
151+
@doc """
152+
A common implementation for uninstalling archives and scripts.
35153
"""
36-
def uninstall(argv, root, options) do
154+
@spec uninstall(atom, OptionParser.argv) :: boolean
155+
def uninstall(type, argv) do
37156
{_, argv, _} = OptionParser.parse(argv)
38157

39-
item_name = Keyword.fetch!(options, :item_name)
40-
item_plural = Keyword.fetch!(options, :item_plural)
158+
item_name = "#{type}"
159+
item_plural = "#{type}s"
160+
root = Mix.Local.path_for(type)
41161

42162
if name = List.first(argv) do
43163
path = Path.join(root, name)
44164
if File.regular?(path) do
45-
if should_uninstall?(path, item_name), do: File.rm!(path)
165+
if should_uninstall?(path, item_name) do
166+
File.rm!(path)
167+
true
168+
else
169+
false
170+
end
46171
else
47172
Mix.shell.error "Could not find a local #{item_name} named #{inspect name}. "<>
48173
"Existing #{item_plural} are:"
49174
Mix.Task.run item_name
175+
false
50176
end
51177
else
52178
Mix.raise "No #{item_name} was given to #{item_name}.uninstall"
53179
end
54180
end
55181

56-
@doc """
57-
Parse `path_or_url` as a URI and return its base name.
58-
"""
59-
def basename(path_or_url) do
60-
if path = URI.parse(path_or_url).path do
61-
Path.basename(path)
62-
else
63-
Mix.raise "Expected #{inspect path_or_url} to be a url or a local file path"
64-
end
65-
end
66-
67182
defp should_uninstall?(path, item_name) do
68183
Mix.shell.yes?("Are you sure you want to uninstall #{item_name} #{path}?")
69184
end

lib/mix/lib/mix/tasks/archive.build.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ defmodule Mix.Tasks.Archive.Build do
5757
"please pass -i as an option"
5858
end
5959

60+
project_config = Mix.Project.config
6061
target = cond do
6162
output = opts[:output] ->
6263
output
63-
app = Mix.Project.config[:app] ->
64-
Mix.Archive.name(app, Mix.Project.config[:version])
64+
project_config[:app] ->
65+
Mix.Local.name_for(:archive, project_config)
6566
true ->
6667
Mix.raise "Cannot create archive without output file, " <>
6768
"please pass -o as an option"

lib/mix/lib/mix/tasks/archive.ex

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,10 @@ defmodule Mix.Tasks.Archive do
1818
@spec run(OptionParser.argv) :: :ok
1919
def run(_) do
2020
archives =
21-
Mix.Local.archives_path
21+
Mix.Local.path_for(:archive)
2222
|> Path.join("*.ez")
2323
|> Path.wildcard()
2424
|> Enum.map(&Path.basename/1)
25-
26-
Mix.Local.Installer.print_list(archives,
27-
empty_message: "No archives currently installed.",
28-
footnote: "Archives installed at: #{Mix.Local.archives_path}")
25+
Mix.Local.Installer.print_list(:archive, archives)
2926
end
3027
end

0 commit comments

Comments
 (0)