@@ -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
0 commit comments