1 # This file is part of NIT ( http://www.nitlanguage.org ).
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # Nit package manager command line interface
25 # Command line action, passed after `nitpm`
26 abstract class Command
28 # Short name of the command, specified in the command line
29 fun name
: String is abstract
31 # Short usage description
32 fun usage
: String is abstract
35 fun description
: String is abstract
37 # Apply this command consiering the `args` that follow
38 fun apply
(args
: Array[String]) do end
40 private var all_commands
: Map[String, Command]
42 init do all_commands
[name
] = self
44 # Print the help message for this command
47 print
"usage: {usage}"
49 print
" {description}"
53 # Install a new package
57 redef fun name
do return "install"
58 redef fun usage
do return "nitpm install [package0[=version] [package1 ...]]"
59 redef fun description
do return "Install packages by name, Git repository address or from the local package.ini"
61 # Packages installed in this run (identified by the full path)
62 private var installed
= new Array[String]
66 if args
.not_empty
then
67 # Install each package
69 # Parse each arg as an import string, with versions and commas
73 # Install packages from local package.ini
74 var ini_path
= "package.ini"
75 if not ini_path
.file_exists
then
76 print_error
"Local `package.ini` not found."
81 var ini
= new IniFile.from_file
(ini_path
)
82 var import_line
= ini
["package.import"]
83 if import_line
== null then
84 print_error
"The local `package.ini` declares no external dependencies."
89 install_packages import_line
93 # Install packages defined by the `import_line`
94 private fun install_packages
(import_line
: String)
96 var imports
= import_line
.parse_import
97 for name
, ext_package
in imports
do
98 install_package
(ext_package
.id
, ext_package
.version
)
102 # Install the `package_id` at `version`
103 private fun install_package
(package_id
: String, version
: nullable String)
105 if package_id
.is_package_name
then
106 # Ask a centralized server
107 # TODO customizable server list
108 # TODO parse ini file in memory
110 var url
= "https://nitlanguage.org/catalog/p/{package_id}.ini"
111 var ini_path
= "/tmp/{package_id}.ini"
113 if verbose
then print
"Looking for a package description at '{url}'"
115 var request
= new CurlHTTPRequest(url
)
116 request
.verbose
= verbose
117 var response
= request
.download_to_file
(ini_path
)
119 if response
isa CurlResponseFailed then
120 print_error
"Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
124 assert response
isa CurlFileResponseSuccess
125 if response
.status_code
== 404 then
126 print_error
"Package '{package_id}' not found on the server"
128 else if response
.status_code
!= 200 then
129 print_error
"Server side error: {response.status_code}"
134 print
"Found a package description:"
135 print ini_path
.to_path
.read_all
138 var ini
= new IniFile.from_file
(ini_path
)
139 var git_repo
= ini
["upstream.git"]
140 if git_repo
== null then
141 print_error
"Package description invalid, or it does not declare a Git repository"
146 install_from_git
(git_repo
, package_id
, version
)
148 var name
= package_id
.git_name
149 if name
!= null and name
!= "." and not name
.is_empty
then
151 install_from_git
(package_id
, name
, version
)
153 print_error
"Failed to infer the package name"
159 private fun install_from_git
(git_repo
, name
: String, version
: nullable String)
163 var target_dir
= nitpm_lib_dir
/ name
164 if version
!= null then target_dir
+= "=" + version
165 if installed
.has
(target_dir
) then
166 # Ignore packages installed in this run
169 installed
.add target_dir
171 if target_dir
.file_exists
then
172 # Warn about packages previously installed,
173 # install dependencies anyway in case of a previous error.
174 print_error
"Package '{name}' is already installed"
176 # Actually install it
178 if version
!= null then cmd_branch
= "--branch '{version}'"
180 var cmd
= "git clone --depth 1 {cmd_branch} {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
181 if verbose
then print
"+ {cmd}"
183 if "NIT_TESTING".environ
== "true" then
184 # Silence git output when testing
185 cmd
+= " 2> /dev/null"
188 var proc
= new Process("sh", "-c", cmd
)
191 if proc
.status
!= 0 then
192 print_error
"Install of '{name}' failed"
198 var ini
= new IniFile.from_file
(target_dir
/"package.ini")
199 var import_line
= ini
["package.import"]
200 if import_line
!= null then
201 install_packages import_line
210 redef fun name
do return "upgrade"
211 redef fun usage
do return "nitpm upgrade <package>"
212 redef fun description
do return "Upgrade a package"
214 redef fun apply
(args
)
216 if args
.length
!= 1 then
221 var name
= args
.first
222 var target_dir
= nitpm_lib_dir
/ name
224 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
225 print_error
"Package not found"
231 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
232 if verbose
then print
"+ {cmd}"
234 var proc
= new Process("sh", "-c", cmd
)
237 if proc
.status
!= 0 then
238 print_error
"Upgrade failed"
244 # Uninstall a package
245 class CommandUninstall
248 redef fun name
do return "uninstall"
249 redef fun usage
do return "nitpm uninstall [-f] <package0>[=version] [package1 ...]"
250 redef fun description
do return "Uninstall packages"
252 redef fun apply
(args
)
255 var force
= args
.has
(opt_force
)
256 if force
then args
.remove
(opt_force
)
258 if args
.is_empty
then
265 var clean_nitpm_lib_dir
= nitpm_lib_dir
.simplify_path
266 var target_dir
= clean_nitpm_lib_dir
/ name
268 # Check validity of the package to delete
269 target_dir
= target_dir
.simplify_path
270 var within_dir
= target_dir
.has_prefix
(clean_nitpm_lib_dir
+ "/") and
271 target_dir
.length
> clean_nitpm_lib_dir
.length
+ 1
272 var valid_name
= name
.length
> 0 and name
.chars
.first
.is_lower
273 if not valid_name
or not within_dir
then
274 print_error
"Package name '{name}' is invalid"
278 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
279 print_error
"Package not found"
285 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
286 var accept
= response
!= null and
287 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
288 if not accept
then return
291 var cmd
= "rm -rf {target_dir.escape_to_sh}"
292 if verbose
then print
"+ {cmd}"
294 var proc
= new Process("sh", "-c", cmd
)
297 if proc
.status
!= 0 then
298 print_error
"Uninstall failed"
305 # List all installed packages
309 redef fun name
do return "list"
310 redef fun usage
do return "nitpm list"
311 redef fun description
do return "List installed packages"
313 redef fun apply
(args
)
315 var files
= nitpm_lib_dir
.files
316 var name_to_desc
= new Map[String, nullable String]
319 # Collect package info
321 var ini_path
= nitpm_lib_dir
/ file
/ "package.ini"
322 if verbose
then print
"- Reading ini file at {ini_path}"
323 var ini
= new IniFile.from_file
(ini_path
)
324 var tags
= ini
["package.tags"]
326 name_to_desc
[file
] = tags
327 max_name_len
= max_name_len
.max
(file
.length
)
330 # Sort in alphabetical order
331 var sorted_names
= name_to_desc
.keys
.to_a
332 alpha_comparator
.sort sorted_names
334 # Print with clear columns
335 for name
in sorted_names
do
336 var col0
= name
.justify
(max_name_len
+1, 0.0)
337 var col1
= name_to_desc
[name
] or else ""
338 var line
= col0
+ col1
344 # Show general help or help specific to a command
348 redef fun name
do return "help"
349 redef fun usage
do return "nitpm help [command]"
350 redef fun description
do return "Show general help message or the help for a command"
352 redef fun apply
(args
)
354 # Try first to help about a valid action
355 if args
.length
== 1 then
356 var command
= commands
.get_or_null
(args
.first
)
357 if command
!= null then
358 command
.print_local_help
369 # General command line options
370 var opts
= new OptionContext
373 var opt_help
= new OptionBool("Show help message", "-h", "--help")
375 # Verbose mode option
376 var opt_verbose
= new OptionBool("Print more information", "-v", "--verbose")
377 private fun verbose
: Bool do return opt_verbose
.value
379 # All command line actions, mapped to their short `name`
380 var commands
= new Map[String, Command]
382 private var command_install
= new CommandInstall(commands
)
383 private var command_list
= new CommandList(commands
)
384 private var command_update
= new CommandUpgrade(commands
)
385 private var command_uninstall
= new CommandUninstall(commands
)
386 private var command_help
= new CommandHelp(commands
)
389 redef fun nitpm_lib_dir
391 if "NIT_TESTING".environ
== "true" then
392 return "/tmp/nitpm-test-" + "NIT_TESTING_ID".environ
396 # Print the general help message
397 private fun print_help
399 print
"usage: nitpm <command> [options]"
403 for command
in commands
.values
do
404 print
" {command.name.justify(11, 0.0)} {command.description}"
412 # Check if `git` is available, exit if not
413 private fun check_git
415 var proc
= new ProcessReader("git", "--version")
419 if proc
.status
!= 0 then
420 print_error
"Please install `git`"
426 opts
.add_option
(opt_help
, opt_verbose
)
430 if opt_help
.value
then
435 if opts
.errors
.not_empty
then
436 for error
in opts
.errors
do print error
442 if rest
.is_empty
then
447 # Find and apply action
448 var action_name
= rest
.shift
449 var action
= commands
.get_or_null
(action_name
)
450 if action
!= null then