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 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
, 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 failed"
193 redef fun name
do return "upgrade"
194 redef fun usage
do return "picnit upgrade <package>"
195 redef fun description
do return "Upgrade a package"
197 redef fun apply
(args
)
199 if args
.length
!= 1 then
204 var name
= args
.first
205 var target_dir
= picnit_lib_dir
/ name
207 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
208 print_error
"Package not found"
214 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
215 if verbose
then print
"+ {cmd}"
217 var proc
= new Process("sh", "-c", cmd
)
220 if proc
.status
!= 0 then
221 print_error
"Upgrade failed"
227 # Uninstall a package
228 class CommandUninstall
231 redef fun name
do return "uninstall"
232 redef fun usage
do return "picnit uninstall <package>"
233 redef fun description
do return "Uninstall a package"
235 redef fun apply
(args
)
237 if args
.length
!= 1 then
242 var name
= args
.first
243 var target_dir
= picnit_lib_dir
/ name
245 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
246 print_error
"Package not found"
251 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
252 var accept
= response
!= null and
253 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
254 if not accept
then return
256 var cmd
= "rm -rf {target_dir.escape_to_sh}"
257 if verbose
then print
"+ {cmd}"
259 var proc
= new Process("sh", "-c", cmd
)
262 if proc
.status
!= 0 then
263 print_error
"Uninstall failed"
269 # List all installed packages
273 redef fun name
do return "list"
274 redef fun usage
do return "picnit list"
275 redef fun description
do return "List installed packages"
277 redef fun apply
(args
)
279 var files
= picnit_lib_dir
.files
281 var ini_path
= picnit_lib_dir
/ file
/ "package.ini"
282 if verbose
then print
"- Reading ini file at {ini_path}"
283 var ini
= new ConfigTree(ini_path
)
284 var tags
= ini
["package.tags"]
287 print
"{file.justify(15, 0.0)} {tags}"
295 # Show general help or help specific to a command
299 redef fun name
do return "help"
300 redef fun usage
do return "picnit help [command]"
301 redef fun description
do return "Show general help message or the help for a command"
303 redef fun apply
(args
)
305 # Try first to help about a valid action
306 if args
.length
== 1 then
307 var command
= commands
.get_or_null
(args
.first
)
308 if command
!= null then
309 command
.print_local_help
320 # General command line options
321 var opts
= new OptionContext
324 var opt_help
= new OptionBool("Show this help message", "--help", "-h")
326 # Verbose mode option
327 var opt_verbose
= new OptionBool("Print more information", "--verbose", "-v")
328 private fun verbose
: Bool do return opt_verbose
.value
330 # All command line actions, mapped to their short `name`
331 var commands
= new Map[String, Command]
333 private var command_install
= new CommandInstall(commands
)
334 private var command_list
= new CommandList(commands
)
335 private var command_update
= new CommandUpgrade(commands
)
336 private var command_uninstall
= new CommandUninstall(commands
)
337 private var command_help
= new CommandHelp(commands
)
340 redef fun picnit_lib_dir
342 if "NIT_TESTING".environ
== "true" then
343 return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ
347 # Print the general help message
348 private fun print_help
350 print
"usage: picnit <command> [options]"
354 for command
in commands
.values
do
355 print
" {command.name.justify(11, 0.0)} {command.description}"
363 # Check if `git` is available, exit if not
364 private fun check_git
366 var proc
= new ProcessReader("git", "--version")
370 if proc
.status
!= 0 then
371 print_error
"Please install `git`"
377 opts
.add_option
(opt_help
, opt_verbose
)
381 if opt_help
.value
then
386 if opts
.errors
.not_empty
then
387 for error
in opts
.errors
do print error
393 if rest
.is_empty
then
398 # Find and apply action
399 var action_name
= rest
.shift
400 var action
= commands
.get_or_null
(action_name
)
401 if action
!= null then