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 ConfigTree(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 choose a future safe URL
108 # TODO customizable server list
109 # TODO parse ini file in memory
111 var url
= "https://xymus.net/nitpm/{package_id}.ini"
112 var ini_path
= "/tmp/{package_id}.ini"
114 if verbose
then print
"Looking for a package description at '{url}'"
116 var request
= new CurlHTTPRequest(url
)
117 request
.verbose
= verbose
118 var response
= request
.download_to_file
(ini_path
)
120 if response
isa CurlResponseFailed then
121 print_error
"Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
125 assert response
isa CurlFileResponseSuccess
126 if response
.status_code
== 404 then
127 print_error
"Package '{package_id}' not found on the server"
129 else if response
.status_code
!= 200 then
130 print_error
"Server side error: {response.status_code}"
135 print
"Found a package description:"
136 print ini_path
.to_path
.read_all
139 var ini
= new ConfigTree(ini_path
)
140 var git_repo
= ini
["upstream.git"]
141 if git_repo
== null then
142 print_error
"Package description invalid, or it does not declare a Git repository"
147 install_from_git
(git_repo
, package_id
, version
)
149 var name
= package_id
.git_name
150 if name
!= null and name
!= "." and not name
.is_empty
then
152 install_from_git
(package_id
, name
, version
)
154 print_error
"Failed to infer the package name"
160 private fun install_from_git
(git_repo
, name
: String, version
: nullable String)
164 var target_dir
= nitpm_lib_dir
/ name
165 if version
!= null then target_dir
+= "=" + version
166 if installed
.has
(target_dir
) then
167 # Ignore packages installed in this run
170 installed
.add target_dir
172 if target_dir
.file_exists
then
173 # Warn about packages previously installed,
174 # install dependencies anyway in case of a previous error.
175 print_error
"Package '{name}' is already installed"
177 # Actually install it
179 if version
!= null then cmd_branch
= "--branch '{version}'"
181 var cmd
= "git clone --depth 1 {cmd_branch} {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
182 if verbose
then print
"+ {cmd}"
184 if "NIT_TESTING".environ
== "true" then
185 # Silence git output when testing
186 cmd
+= " 2> /dev/null"
189 var proc
= new Process("sh", "-c", cmd
)
192 if proc
.status
!= 0 then
193 print_error
"Install of '{name}' failed"
199 var ini
= new ConfigTree(target_dir
/"package.ini")
200 var import_line
= ini
["package.import"]
201 if import_line
!= null then
202 install_packages import_line
211 redef fun name
do return "upgrade"
212 redef fun usage
do return "nitpm upgrade <package>"
213 redef fun description
do return "Upgrade a package"
215 redef fun apply
(args
)
217 if args
.length
!= 1 then
222 var name
= args
.first
223 var target_dir
= nitpm_lib_dir
/ name
225 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
226 print_error
"Package not found"
232 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
233 if verbose
then print
"+ {cmd}"
235 var proc
= new Process("sh", "-c", cmd
)
238 if proc
.status
!= 0 then
239 print_error
"Upgrade failed"
245 # Uninstall a package
246 class CommandUninstall
249 redef fun name
do return "uninstall"
250 redef fun usage
do return "nitpm uninstall [-f] <package0>[=version] [package1 ...]"
251 redef fun description
do return "Uninstall packages"
253 redef fun apply
(args
)
256 var force
= args
.has
(opt_force
)
257 if force
then args
.remove
(opt_force
)
259 if args
.is_empty
then
266 var clean_nitpm_lib_dir
= nitpm_lib_dir
.simplify_path
267 var target_dir
= clean_nitpm_lib_dir
/ name
269 # Check validity of the package to delete
270 target_dir
= target_dir
.simplify_path
271 var within_dir
= target_dir
.has_prefix
(clean_nitpm_lib_dir
+ "/") and
272 target_dir
.length
> clean_nitpm_lib_dir
.length
+ 1
273 var valid_name
= name
.length
> 0 and name
.chars
.first
.is_lower
274 if not valid_name
or not within_dir
then
275 print_error
"Package name '{name}' is invalid"
279 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
280 print_error
"Package not found"
286 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
287 var accept
= response
!= null and
288 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
289 if not accept
then return
292 var cmd
= "rm -rf {target_dir.escape_to_sh}"
293 if verbose
then print
"+ {cmd}"
295 var proc
= new Process("sh", "-c", cmd
)
298 if proc
.status
!= 0 then
299 print_error
"Uninstall failed"
306 # List all installed packages
310 redef fun name
do return "list"
311 redef fun usage
do return "nitpm list"
312 redef fun description
do return "List installed packages"
314 redef fun apply
(args
)
316 var files
= nitpm_lib_dir
.files
317 var name_to_desc
= new Map[String, nullable String]
320 # Collect package info
322 var ini_path
= nitpm_lib_dir
/ file
/ "package.ini"
323 if verbose
then print
"- Reading ini file at {ini_path}"
324 var ini
= new ConfigTree(ini_path
)
325 var tags
= ini
["package.tags"]
327 name_to_desc
[file
] = tags
328 max_name_len
= max_name_len
.max
(file
.length
)
331 # Sort in alphabetical order
332 var sorted_names
= name_to_desc
.keys
.to_a
333 alpha_comparator
.sort sorted_names
335 # Print with clear columns
336 for name
in sorted_names
do
337 var col0
= name
.justify
(max_name_len
+1, 0.0)
338 var col1
= name_to_desc
[name
] or else ""
339 var line
= col0
+ col1
345 # Show general help or help specific to a command
349 redef fun name
do return "help"
350 redef fun usage
do return "nitpm help [command]"
351 redef fun description
do return "Show general help message or the help for a command"
353 redef fun apply
(args
)
355 # Try first to help about a valid action
356 if args
.length
== 1 then
357 var command
= commands
.get_or_null
(args
.first
)
358 if command
!= null then
359 command
.print_local_help
370 # General command line options
371 var opts
= new OptionContext
374 var opt_help
= new OptionBool("Show help message", "-h", "--help")
376 # Verbose mode option
377 var opt_verbose
= new OptionBool("Print more information", "-v", "--verbose")
378 private fun verbose
: Bool do return opt_verbose
.value
380 # All command line actions, mapped to their short `name`
381 var commands
= new Map[String, Command]
383 private var command_install
= new CommandInstall(commands
)
384 private var command_list
= new CommandList(commands
)
385 private var command_update
= new CommandUpgrade(commands
)
386 private var command_uninstall
= new CommandUninstall(commands
)
387 private var command_help
= new CommandHelp(commands
)
390 redef fun nitpm_lib_dir
392 if "NIT_TESTING".environ
== "true" then
393 return "/tmp/nitpm-test-" + "NIT_TESTING_ID".environ
397 # Print the general help message
398 private fun print_help
400 print
"usage: nitpm <command> [options]"
404 for command
in commands
.values
do
405 print
" {command.name.justify(11, 0.0)} {command.description}"
413 # Check if `git` is available, exit if not
414 private fun check_git
416 var proc
= new ProcessReader("git", "--version")
420 if proc
.status
!= 0 then
421 print_error
"Please install `git`"
427 opts
.add_option
(opt_help
, opt_verbose
)
431 if opt_help
.value
then
436 if opts
.errors
.not_empty
then
437 for error
in opts
.errors
do print error
443 if rest
.is_empty
then
448 # Find and apply action
449 var action_name
= rest
.shift
450 var action
= commands
.get_or_null
(action_name
)
451 if action
!= null then