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
27 # Does `self` look like a package name?
30 # assert "gamnit".is_package_name
31 # assert "n1t".is_package_name
32 # assert not ".".is_package_name
33 # assert not "./gamnit".is_package_name
34 # assert not "https://github.com/nitlang/nit.git".is_package_name
35 # assert not "git://github.com/nitlang/nit".is_package_name
36 # assert not "git@gitlab.com:xymus/gamnit.git".is_package_name
37 # assert not "4it".is_package_name
39 private fun is_package_name
: Bool
41 if is_empty
then return false
42 if not chars
.first
.is_alpha
then return false
45 if not (c
.is_alphanumeric
or c
== '_') then return false
51 # Get package name from the Git address `self`
53 # Return `null` on failure.
56 # assert "https://github.com/nitlang/nit.git".git_name == "nit"
57 # assert "git://github.com/nitlang/nit".git_name == "nit"
58 # assert "gamnit".git_name == "gamnit"
59 # assert "///".git_name == null
60 # assert "file:///".git_name == "file:"
62 private fun git_name
: nullable String
64 var parts
= split
("/")
65 for part
in parts
.reverse_iterator
do
66 if not part
.is_empty
then
67 return part
.strip_extension
(".git")
75 # Command line action, passed after `picnit`
76 abstract class Command
78 # Short name of the command, specified in the command line
79 fun name
: String is abstract
81 # Short usage description
82 fun usage
: String is abstract
85 fun description
: String is abstract
87 # Apply this command consiering the `args` that follow
88 fun apply
(args
: Array[String]) do end
90 private var all_commands
: Map[String, Command]
92 init do all_commands
[name
] = self
94 # Print the help message for this command
97 print
"usage: {usage}"
99 print
" {description}"
103 # Install a new package
107 redef fun name
do return "install"
108 redef fun usage
do return "picnit install [package or git-repository]"
109 redef fun description
do return "Install a package by its name or from a git-repository"
111 redef fun apply
(args
)
113 if args
.length
!= 1 then
118 var package_id
= args
.first
119 if package_id
.is_package_name
then
120 # Ask a centralized server
121 # TODO choose a future safe URL
122 # TODO customizable server list
123 # TODO parse ini file in memory
125 var url
= "https://xymus.net/picnit/{package_id}.ini"
126 var ini_path
= "/tmp/{package_id}.ini"
128 if verbose
then print
"Looking for a package description at '{url}'"
130 var request
= new CurlHTTPRequest(url
)
131 request
.verbose
= verbose
132 var response
= request
.download_to_file
(ini_path
)
134 if response
isa CurlResponseFailed then
135 print_error
"Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
139 assert response
isa CurlFileResponseSuccess
140 if response
.status_code
== 404 then
141 print_error
"Package not found by the server"
143 else if response
.status_code
!= 200 then
144 print_error
"Server side error: {response.status_code}"
149 print
"Found a package description:"
150 print ini_path
.to_path
.read_all
153 var ini
= new ConfigTree(ini_path
)
154 var git_repo
= ini
["upstream.git"]
155 if git_repo
== null then
156 print_error
"Package description invalid, or it does not declare a Git repository"
161 install_from_git
(git_repo
, package_id
)
163 var name
= package_id
.git_name
164 if name
!= null and name
!= "." and not name
.is_empty
then
166 install_from_git
(package_id
, name
)
168 print_error
"Failed to infer the package name"
174 private fun install_from_git
(git_repo
, name
: String)
178 var target_dir
= picnit_lib_dir
/ name
179 if target_dir
.file_exists
then
180 print_error
"Already installed"
184 var cmd
= "git clone {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
185 if verbose
then print
"+ {cmd}"
187 if "NIT_TESTING".environ
== "true" then
188 # Silence git output when testing
189 cmd
+= " 2> /dev/null"
192 var proc
= new Process("sh", "-c", cmd
)
195 if proc
.status
!= 0 then
196 print_error
"Install failed"
206 redef fun name
do return "upgrade"
207 redef fun usage
do return "picnit upgrade <package>"
208 redef fun description
do return "Upgrade a package"
210 redef fun apply
(args
)
212 if args
.length
!= 1 then
217 var name
= args
.first
218 var target_dir
= picnit_lib_dir
/ name
220 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
221 print_error
"Package not found"
227 var cmd
= "cd {target_dir.escape_to_sh}; git pull"
228 if verbose
then print
"+ {cmd}"
230 var proc
= new Process("sh", "-c", cmd
)
233 if proc
.status
!= 0 then
234 print_error
"Upgrade failed"
240 # Uninstall a package
241 class CommandUninstall
244 redef fun name
do return "uninstall"
245 redef fun usage
do return "picnit uninstall <package>"
246 redef fun description
do return "Uninstall a package"
248 redef fun apply
(args
)
250 if args
.length
!= 1 then
255 var name
= args
.first
256 var target_dir
= picnit_lib_dir
/ name
258 if not target_dir
.file_exists
or not target_dir
.to_path
.is_dir
then
259 print_error
"Package not found"
264 var response
= prompt
("Delete {target_dir.escape_to_sh}? [Y/n] ")
265 var accept
= response
!= null and
266 (response
.to_lower
== "y" or response
.to_lower
== "yes" or response
== "")
267 if not accept
then return
269 var cmd
= "rm -rf {target_dir.escape_to_sh}"
270 if verbose
then print
"+ {cmd}"
272 var proc
= new Process("sh", "-c", cmd
)
275 if proc
.status
!= 0 then
276 print_error
"Uninstall failed"
282 # List all installed packages
286 redef fun name
do return "list"
287 redef fun usage
do return "picnit list"
288 redef fun description
do return "List installed packages"
290 redef fun apply
(args
)
292 var files
= picnit_lib_dir
.files
294 var ini_path
= picnit_lib_dir
/ file
/ "package.ini"
295 if verbose
then print
"- Reading ini file at {ini_path}"
296 var ini
= new ConfigTree(ini_path
)
297 var tags
= ini
["package.tags"]
300 print
"{file.justify(15, 0.0)} {tags}"
308 # Show general help or help specific to a command
312 redef fun name
do return "help"
313 redef fun usage
do return "picnit help [command]"
314 redef fun description
do return "Show general help message or the help for a command"
316 redef fun apply
(args
)
318 # Try first to help about a valid action
319 if args
.length
== 1 then
320 var command
= commands
.get_or_null
(args
.first
)
321 if command
!= null then
322 command
.print_local_help
333 # General command line options
334 var opts
= new OptionContext
337 var opt_help
= new OptionBool("Show this help message", "--help", "-h")
339 # Verbose mode option
340 var opt_verbose
= new OptionBool("Print more information", "--verbose", "-v")
341 private fun verbose
: Bool do return opt_verbose
.value
343 # All command line actions, mapped to their short `name`
344 var commands
= new Map[String, Command]
346 private var command_install
= new CommandInstall(commands
)
347 private var command_list
= new CommandList(commands
)
348 private var command_update
= new CommandUpgrade(commands
)
349 private var command_uninstall
= new CommandUninstall(commands
)
350 private var command_help
= new CommandHelp(commands
)
353 redef fun picnit_lib_dir
355 if "NIT_TESTING".environ
== "true" then
356 return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ
360 # Print the general help message
361 private fun print_help
363 print
"usage: picnit <command> [options]"
367 for command
in commands
.values
do
368 print
" {command.name.justify(11, 0.0)} {command.description}"
376 # Check if `git` is available, exit if not
377 private fun check_git
379 var proc
= new ProcessReader("git", "--version")
383 if proc
.status
!= 0 then
384 print_error
"Please install `git`"
390 opts
.add_option
(opt_help
, opt_verbose
)
394 if opt_help
.value
then
399 if opts
.errors
.not_empty
then
400 for error
in opts
.errors
do print error
406 if rest
.is_empty
then
411 # Find and apply action
412 var action_name
= rest
.shift
413 var action
= commands
.get_or_null
(action_name
)
414 if action
!= null then