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 `picnit`
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 "picnit install [package0[=version] [package1 ...]]"
59 redef fun description
do return "Install packages by name, Git repository address or from the local package.ini"
63 if args
.not_empty
then
64 # Install each package
66 # Parse each arg as an import string, with versions and commas
70 # Install packages from local package.ini
71 var ini_path
= "package.ini"
72 if not ini_path
.file_exists
then
73 print_error
"Local `package.ini` not found."
78 var ini
= new ConfigTree(ini_path
)
79 var import_line
= ini
["package.import"]
80 if import_line
== null then
81 print_error
"The local `package.ini` declares no external dependencies."
86 install_packages import_line
90 # Install packages defined by the `import_line`
91 private fun install_packages
(import_line
: String)
93 var imports
= import_line
.parse_import
94 for name
, ext_package
in imports
do
95 install_package
(ext_package
.id
, ext_package
.version
)
99 # Install the `package_id` at `version`
100 private fun install_package
(package_id
: String, version
: nullable String)
102 if package_id
.is_package_name
then
103 # Ask a centralized server
104 # TODO choose a future safe URL
105 # TODO customizable server list
106 # TODO parse ini file in memory
108 var url
= "https://xymus.net/picnit/{package_id}.ini"
109 var ini_path
= "/tmp/{package_id}.ini"
111 if verbose
then print
"Looking for a package description at '{url}'"
113 var request
= new CurlHTTPRequest(url
)
114 request
.verbose
= verbose
115 var response
= request
.download_to_file
(ini_path
)
117 if response
isa CurlResponseFailed then
118 print_error
"Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
122 assert response
isa CurlFileResponseSuccess
123 if response
.status_code
== 404 then
124 print_error
"Package '{package_id}' not found on the server"
126 else if response
.status_code
!= 200 then
127 print_error
"Server side error: {response.status_code}"
132 print
"Found a package description:"
133 print ini_path
.to_path
.read_all
136 var ini
= new ConfigTree(ini_path
)
137 var git_repo
= ini
["upstream.git"]
138 if git_repo
== null then
139 print_error
"Package description invalid, or it does not declare a Git repository"
144 install_from_git
(git_repo
, package_id
, version
)
146 var name
= package_id
.git_name
147 if name
!= null and name
!= "." and not name
.is_empty
then
149 install_from_git
(package_id
, name
, version
)
151 print_error
"Failed to infer the package name"
157 private fun install_from_git
(git_repo
, name
: String, version
: nullable String)
161 var target_dir
= picnit_lib_dir
/ name
162 if version
!= null then target_dir
+= "=" + version
163 if target_dir
.file_exists
then
164 print_error
"Already installed"
169 if version
!= null then cmd_branch
= "--branch '{version}'"
171 var cmd
= "git clone --depth 1 {cmd_branch} {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
172 if verbose
then print
"+ {cmd}"
174 if "NIT_TESTING".environ
== "true" then
175 # Silence git output when testing
176 cmd
+= " 2> /dev/null"
179 var proc
= new Process("sh", "-c", cmd
)
182 if proc
.status
!= 0 then
183 print_error
"Install of '{name}' failed"
188 var ini
= new ConfigTree(target_dir
/"package.ini")
189 var import_line
= ini
["package.import"]
190 if import_line
!= null then
191 install_packages import_line
200 redef fun name
do return "upgrade"
201 redef fun usage
do return "picnit upgrade <package>"
202 redef fun description
do return "Upgrade a package"
204 redef fun apply
(args
)
206 if args
.length
!= 1 then
211 var name
= args
.first
212 var target_dir
= picnit_lib_dir
/ name
214 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
215 print_error
"Package not found"
221 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
222 if verbose
then print
"+ {cmd}"
224 var proc
= new Process("sh", "-c", cmd
)
227 if proc
.status
!= 0 then
228 print_error
"Upgrade failed"
234 # Uninstall a package
235 class CommandUninstall
238 redef fun name
do return "uninstall"
239 redef fun usage
do return "picnit uninstall <package>"
240 redef fun description
do return "Uninstall a package"
242 redef fun apply
(args
)
244 if args
.length
!= 1 then
249 var name
= args
.first
250 var target_dir
= picnit_lib_dir
/ name
252 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
253 print_error
"Package not found"
258 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
259 var accept
= response
!= null and
260 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
261 if not accept
then return
263 var cmd
= "rm -rf {target_dir.escape_to_sh}"
264 if verbose
then print
"+ {cmd}"
266 var proc
= new Process("sh", "-c", cmd
)
269 if proc
.status
!= 0 then
270 print_error
"Uninstall failed"
276 # List all installed packages
280 redef fun name
do return "list"
281 redef fun usage
do return "picnit list"
282 redef fun description
do return "List installed packages"
284 redef fun apply
(args
)
286 var files
= picnit_lib_dir
.files
288 var ini_path
= picnit_lib_dir
/ file
/ "package.ini"
289 if verbose
then print
"- Reading ini file at {ini_path}"
290 var ini
= new ConfigTree(ini_path
)
291 var tags
= ini
["package.tags"]
294 print
"{file.justify(15, 0.0)} {tags}"
302 # Show general help or help specific to a command
306 redef fun name
do return "help"
307 redef fun usage
do return "picnit help [command]"
308 redef fun description
do return "Show general help message or the help for a command"
310 redef fun apply
(args
)
312 # Try first to help about a valid action
313 if args
.length
== 1 then
314 var command
= commands
.get_or_null
(args
.first
)
315 if command
!= null then
316 command
.print_local_help
327 # General command line options
328 var opts
= new OptionContext
331 var opt_help
= new OptionBool("Show this help message", "--help", "-h")
333 # Verbose mode option
334 var opt_verbose
= new OptionBool("Print more information", "--verbose", "-v")
335 private fun verbose
: Bool do return opt_verbose
.value
337 # All command line actions, mapped to their short `name`
338 var commands
= new Map[String, Command]
340 private var command_install
= new CommandInstall(commands
)
341 private var command_list
= new CommandList(commands
)
342 private var command_update
= new CommandUpgrade(commands
)
343 private var command_uninstall
= new CommandUninstall(commands
)
344 private var command_help
= new CommandHelp(commands
)
347 redef fun picnit_lib_dir
349 if "NIT_TESTING".environ
== "true" then
350 return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ
354 # Print the general help message
355 private fun print_help
357 print
"usage: picnit <command> [options]"
361 for command
in commands
.values
do
362 print
" {command.name.justify(11, 0.0)} {command.description}"
370 # Check if `git` is available, exit if not
371 private fun check_git
373 var proc
= new ProcessReader("git", "--version")
377 if proc
.status
!= 0 then
378 print_error
"Please install `git`"
384 opts
.add_option
(opt_help
, opt_verbose
)
388 if opt_help
.value
then
393 if opts
.errors
.not_empty
then
394 for error
in opts
.errors
do print error
400 if rest
.is_empty
then
405 # Find and apply action
406 var action_name
= rest
.shift
407 var action
= commands
.get_or_null
(action_name
)
408 if action
!= null then