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 [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 not found by 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
)
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
)
151 print_error
"Failed to infer the package name"
157 private fun install_from_git
(git_repo
, name
: String)
161 var target_dir
= picnit_lib_dir
/ name
162 if target_dir
.file_exists
then
163 print_error
"Already installed"
167 var cmd
= "git clone {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
168 if verbose
then print
"+ {cmd}"
170 if "NIT_TESTING".environ
== "true" then
171 # Silence git output when testing
172 cmd
+= " 2> /dev/null"
175 var proc
= new Process("sh", "-c", cmd
)
178 if proc
.status
!= 0 then
179 print_error
"Install failed"
189 redef fun name
do return "upgrade"
190 redef fun usage
do return "picnit upgrade <package>"
191 redef fun description
do return "Upgrade a package"
193 redef fun apply
(args
)
195 if args
.length
!= 1 then
200 var name
= args
.first
201 var target_dir
= picnit_lib_dir
/ name
203 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
204 print_error
"Package not found"
210 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
211 if verbose
then print
"+ {cmd}"
213 var proc
= new Process("sh", "-c", cmd
)
216 if proc
.status
!= 0 then
217 print_error
"Upgrade failed"
223 # Uninstall a package
224 class CommandUninstall
227 redef fun name
do return "uninstall"
228 redef fun usage
do return "picnit uninstall <package>"
229 redef fun description
do return "Uninstall a package"
231 redef fun apply
(args
)
233 if args
.length
!= 1 then
238 var name
= args
.first
239 var target_dir
= picnit_lib_dir
/ name
241 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
242 print_error
"Package not found"
247 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
248 var accept
= response
!= null and
249 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
250 if not accept
then return
252 var cmd
= "rm -rf {target_dir.escape_to_sh}"
253 if verbose
then print
"+ {cmd}"
255 var proc
= new Process("sh", "-c", cmd
)
258 if proc
.status
!= 0 then
259 print_error
"Uninstall failed"
265 # List all installed packages
269 redef fun name
do return "list"
270 redef fun usage
do return "picnit list"
271 redef fun description
do return "List installed packages"
273 redef fun apply
(args
)
275 var files
= picnit_lib_dir
.files
277 var ini_path
= picnit_lib_dir
/ file
/ "package.ini"
278 if verbose
then print
"- Reading ini file at {ini_path}"
279 var ini
= new ConfigTree(ini_path
)
280 var tags
= ini
["package.tags"]
283 print
"{file.justify(15, 0.0)} {tags}"
291 # Show general help or help specific to a command
295 redef fun name
do return "help"
296 redef fun usage
do return "picnit help [command]"
297 redef fun description
do return "Show general help message or the help for a command"
299 redef fun apply
(args
)
301 # Try first to help about a valid action
302 if args
.length
== 1 then
303 var command
= commands
.get_or_null
(args
.first
)
304 if command
!= null then
305 command
.print_local_help
316 # General command line options
317 var opts
= new OptionContext
320 var opt_help
= new OptionBool("Show this help message", "--help", "-h")
322 # Verbose mode option
323 var opt_verbose
= new OptionBool("Print more information", "--verbose", "-v")
324 private fun verbose
: Bool do return opt_verbose
.value
326 # All command line actions, mapped to their short `name`
327 var commands
= new Map[String, Command]
329 private var command_install
= new CommandInstall(commands
)
330 private var command_list
= new CommandList(commands
)
331 private var command_update
= new CommandUpgrade(commands
)
332 private var command_uninstall
= new CommandUninstall(commands
)
333 private var command_help
= new CommandHelp(commands
)
336 redef fun picnit_lib_dir
338 if "NIT_TESTING".environ
== "true" then
339 return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ
343 # Print the general help message
344 private fun print_help
346 print
"usage: picnit <command> [options]"
350 for command
in commands
.values
do
351 print
" {command.name.justify(11, 0.0)} {command.description}"
359 # Check if `git` is available, exit if not
360 private fun check_git
362 var proc
= new ProcessReader("git", "--version")
366 if proc
.status
!= 0 then
367 print_error
"Please install `git`"
373 opts
.add_option
(opt_help
, opt_verbose
)
377 if opt_help
.value
then
382 if opts
.errors
.not_empty
then
383 for error
in opts
.errors
do print error
389 if rest
.is_empty
then
394 # Find and apply action
395 var action_name
= rest
.shift
396 var action
= commands
.get_or_null
(action_name
)
397 if action
!= null then