Merge: doc: fixed some typos and other misc. corrections
[nit.git] / src / nitpm.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15 # Nit package manager command line interface
16 module nitpm
17
18 import opts
19 import prompt
20 import ini
21 import curl
22
23 import nitpm_shared
24
25 # Command line action, passed after `nitpm`
26 abstract class Command
27
28 # Short name of the command, specified in the command line
29 fun name: String is abstract
30
31 # Short usage description
32 fun usage: String is abstract
33
34 # Command description
35 fun description: String is abstract
36
37 # Apply this command consiering the `args` that follow
38 fun apply(args: Array[String]) do end
39
40 private var all_commands: Map[String, Command]
41
42 init do all_commands[name] = self
43
44 # Print the help message for this command
45 fun print_local_help
46 do
47 print "usage: {usage}"
48 print ""
49 print " {description}"
50 end
51 end
52
53 # Install a new package
54 class CommandInstall
55 super Command
56
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"
60
61 # Packages installed in this run (identified by the full path)
62 private var installed = new Array[String]
63
64 redef fun apply(args)
65 do
66 if args.not_empty then
67 # Install each package
68 for arg in args do
69 # Parse each arg as an import string, with versions and commas
70 install_packages arg
71 end
72 else
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."
77 print_local_help
78 exit 1
79 end
80
81 var ini = new IniFile.from_file(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."
85 exit 0
86 abort
87 end
88
89 install_packages import_line
90 end
91 end
92
93 # Install packages defined by the `import_line`
94 private fun install_packages(import_line: String)
95 do
96 var imports = import_line.parse_import
97 for name, ext_package in imports do
98 install_package(ext_package.id, ext_package.version)
99 end
100 end
101
102 # Install the `package_id` at `version`
103 private fun install_package(package_id: String, version: nullable String)
104 do
105 if package_id.is_package_name then
106 # Ask a centralized server
107 # TODO customizable server list
108 # TODO parse ini file in memory
109
110 var url = "https://nitlanguage.org/catalog/p/{package_id}.ini"
111 var ini_path = "/tmp/{package_id}.ini"
112
113 if verbose then print "Looking for a package description at '{url}'"
114
115 var request = new CurlHTTPRequest(url)
116 request.verbose = verbose
117 var response = request.download_to_file(ini_path)
118
119 if response isa CurlResponseFailed then
120 print_error "Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
121 exit 1
122 end
123
124 assert response isa CurlFileResponseSuccess
125 if response.status_code == 404 then
126 print_error "Package '{package_id}' not found on the server"
127 exit 1
128 else if response.status_code != 200 then
129 print_error "Server side error: {response.status_code}"
130 exit 1
131 end
132
133 if verbose then
134 print "Found a package description:"
135 print ini_path.to_path.read_all
136 end
137
138 var ini = new IniFile.from_file(ini_path)
139 var git_repo = ini["upstream.git"]
140 if git_repo == null then
141 print_error "Package description invalid, or it does not declare a Git repository"
142 exit 1
143 abort
144 end
145
146 install_from_git(git_repo, package_id, version)
147 else
148 var name = package_id.git_name
149 if name != null and name != "." and not name.is_empty then
150 name = name.to_lower
151 install_from_git(package_id, name, version)
152 else
153 print_error "Failed to infer the package name"
154 exit 1
155 end
156 end
157 end
158
159 private fun install_from_git(git_repo, name: String, version: nullable String)
160 do
161 check_git
162
163 var target_dir = nitpm_lib_dir / name
164 if version != null then target_dir += "=" + version
165 if installed.has(target_dir) then
166 # Ignore packages installed in this run
167 return
168 end
169 installed.add target_dir
170
171 if target_dir.file_exists then
172 # Warn about packages previously installed,
173 # install dependencies anyway in case of a previous error.
174 print_error "Package '{name}' is already installed"
175 else
176 # Actually install it
177 var cmd_branch = ""
178 if version != null then cmd_branch = "--branch '{version}'"
179
180 var cmd = "git clone --depth 1 {cmd_branch} {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
181 if verbose then print "+ {cmd}"
182
183 if "NIT_TESTING".environ == "true" then
184 # Silence git output when testing
185 cmd += " 2> /dev/null"
186 end
187
188 var proc = new Process("sh", "-c", cmd)
189 proc.wait
190
191 if proc.status != 0 then
192 print_error "Install of '{name}' failed"
193 exit 1
194 end
195 end
196
197 # Recursive install
198 var ini = new IniFile.from_file(target_dir/"package.ini")
199 var import_line = ini["package.import"]
200 if import_line != null then
201 install_packages import_line
202 end
203 end
204 end
205
206 # Upgrade a package
207 class CommandUpgrade
208 super Command
209
210 redef fun name do return "upgrade"
211 redef fun usage do return "nitpm upgrade <package>"
212 redef fun description do return "Upgrade a package"
213
214 redef fun apply(args)
215 do
216 if args.length != 1 then
217 print_local_help
218 exit 1
219 end
220
221 var name = args.first
222 var target_dir = nitpm_lib_dir / name
223
224 if not target_dir.file_exists or not target_dir.to_path.is_dir then
225 print_error "Package not found"
226 exit 1
227 end
228
229 check_git
230
231 var cmd = "cd {target_dir.escape_to_sh}; git pull"
232 if verbose then print "+ {cmd}"
233
234 var proc = new Process("sh", "-c", cmd)
235 proc.wait
236
237 if proc.status != 0 then
238 print_error "Upgrade failed"
239 exit 1
240 end
241 end
242 end
243
244 # Uninstall a package
245 class CommandUninstall
246 super Command
247
248 redef fun name do return "uninstall"
249 redef fun usage do return "nitpm uninstall [-f] <package0>[=version] [package1 ...]"
250 redef fun description do return "Uninstall packages"
251
252 redef fun apply(args)
253 do
254 var opt_force = "-f"
255 var force = args.has(opt_force)
256 if force then args.remove(opt_force)
257
258 if args.is_empty then
259 print_local_help
260 exit 1
261 end
262
263 for name in args do
264
265 var clean_nitpm_lib_dir = nitpm_lib_dir.simplify_path
266 var target_dir = clean_nitpm_lib_dir / name
267
268 # Check validity of the package to delete
269 target_dir = target_dir.simplify_path
270 var within_dir = target_dir.has_prefix(clean_nitpm_lib_dir + "/") and
271 target_dir.length > clean_nitpm_lib_dir.length + 1
272 var valid_name = name.length > 0 and name.chars.first.is_lower
273 if not valid_name or not within_dir then
274 print_error "Package name '{name}' is invalid"
275 continue
276 end
277
278 if not target_dir.file_exists or not target_dir.to_path.is_dir then
279 print_error "Package not found"
280 exit 1
281 end
282
283 # Ask confirmation
284 if not force then
285 var response = prompt("Delete {target_dir.escape_to_sh}? [Y/n] ")
286 var accept = response != null and
287 (response.to_lower == "y" or response.to_lower == "yes" or response == "")
288 if not accept then return
289 end
290
291 var cmd = "rm -rf {target_dir.escape_to_sh}"
292 if verbose then print "+ {cmd}"
293
294 var proc = new Process("sh", "-c", cmd)
295 proc.wait
296
297 if proc.status != 0 then
298 print_error "Uninstall failed"
299 exit 1
300 end
301 end
302 end
303 end
304
305 # List all installed packages
306 class CommandList
307 super Command
308
309 redef fun name do return "list"
310 redef fun usage do return "nitpm list"
311 redef fun description do return "List installed packages"
312
313 redef fun apply(args)
314 do
315 var files = nitpm_lib_dir.files
316 var name_to_desc = new Map[String, nullable String]
317 var max_name_len = 0
318
319 # Collect package info
320 for file in files do
321 var ini_path = nitpm_lib_dir / file / "package.ini"
322 if verbose then print "- Reading ini file at {ini_path}"
323 var ini = new IniFile.from_file(ini_path)
324 var tags = ini["package.tags"]
325
326 name_to_desc[file] = tags
327 max_name_len = max_name_len.max(file.length)
328 end
329
330 # Sort in alphabetical order
331 var sorted_names = name_to_desc.keys.to_a
332 alpha_comparator.sort sorted_names
333
334 # Print with clear columns
335 for name in sorted_names do
336 var col0 = name.justify(max_name_len+1, 0.0)
337 var col1 = name_to_desc[name] or else ""
338 var line = col0 + col1
339 print line.trim
340 end
341 end
342 end
343
344 # Show general help or help specific to a command
345 class CommandHelp
346 super Command
347
348 redef fun name do return "help"
349 redef fun usage do return "nitpm help [command]"
350 redef fun description do return "Show general help message or the help for a command"
351
352 redef fun apply(args)
353 do
354 # Try first to help about a valid action
355 if args.length == 1 then
356 var command = commands.get_or_null(args.first)
357 if command != null then
358 command.print_local_help
359 return
360 end
361 end
362
363 print_help
364 end
365 end
366
367 redef class Sys
368
369 # General command line options
370 var opts = new OptionContext
371
372 # Help option
373 var opt_help = new OptionBool("Show help message", "-h", "--help")
374
375 # Verbose mode option
376 var opt_verbose = new OptionBool("Print more information", "-v", "--verbose")
377 private fun verbose: Bool do return opt_verbose.value
378
379 # All command line actions, mapped to their short `name`
380 var commands = new Map[String, Command]
381
382 private var command_install = new CommandInstall(commands)
383 private var command_list = new CommandList(commands)
384 private var command_update = new CommandUpgrade(commands)
385 private var command_uninstall = new CommandUninstall(commands)
386 private var command_help = new CommandHelp(commands)
387 end
388
389 redef fun nitpm_lib_dir
390 do
391 if "NIT_TESTING".environ == "true" then
392 return "/tmp/nitpm-test-" + "NIT_TESTING_ID".environ
393 else return super
394 end
395
396 # Print the general help message
397 private fun print_help
398 do
399 print "usage: nitpm <command> [options]"
400 print ""
401
402 print "commands:"
403 for command in commands.values do
404 print " {command.name.justify(11, 0.0)} {command.description}"
405 end
406 print ""
407
408 print "options:"
409 opts.usage
410 end
411
412 # Check if `git` is available, exit if not
413 private fun check_git
414 do
415 var proc = new ProcessReader("git", "--version")
416 proc.wait
417 proc.close
418
419 if proc.status != 0 then
420 print_error "Please install `git`"
421 exit 1
422 end
423 end
424
425 # Parse main options
426 opts.add_option(opt_help, opt_verbose)
427 opts.parse
428 var rest = opts.rest
429
430 if opt_help.value then
431 print_help
432 exit 0
433 end
434
435 if opts.errors.not_empty then
436 for error in opts.errors do print error
437 print ""
438 print_help
439 exit 1
440 end
441
442 if rest.is_empty then
443 print_help
444 exit 1
445 end
446
447 # Find and apply action
448 var action_name = rest.shift
449 var action = commands.get_or_null(action_name)
450 if action != null then
451 action.apply rest
452 else
453 print_help
454 exit 1
455 end