5df3bcdde7066ea7f0382d8108c50b598307f5ba
[nit.git] / src / picnit.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 picnit
17
18 import opts
19 import prompt
20 import ini
21 import curl
22
23 import picnit_shared
24
25 # Command line action, passed after `picnit`
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 "picnit install [package or git-repository]"
59 redef fun description do return "Install a package by its name or from a git-repository"
60
61 redef fun apply(args)
62 do
63 if args.length != 1 then
64 print_local_help
65 exit 1
66 end
67
68 var package_id = args.first
69 if package_id.is_package_name then
70 # Ask a centralized server
71 # TODO choose a future safe URL
72 # TODO customizable server list
73 # TODO parse ini file in memory
74
75 var url = "https://xymus.net/picnit/{package_id}.ini"
76 var ini_path = "/tmp/{package_id}.ini"
77
78 if verbose then print "Looking for a package description at '{url}'"
79
80 var request = new CurlHTTPRequest(url)
81 request.verbose = verbose
82 var response = request.download_to_file(ini_path)
83
84 if response isa CurlResponseFailed then
85 print_error "Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
86 exit 1
87 end
88
89 assert response isa CurlFileResponseSuccess
90 if response.status_code == 404 then
91 print_error "Package not found by the server"
92 exit 1
93 else if response.status_code != 200 then
94 print_error "Server side error: {response.status_code}"
95 exit 1
96 end
97
98 if verbose then
99 print "Found a package description:"
100 print ini_path.to_path.read_all
101 end
102
103 var ini = new ConfigTree(ini_path)
104 var git_repo = ini["upstream.git"]
105 if git_repo == null then
106 print_error "Package description invalid, or it does not declare a Git repository"
107 exit 1
108 abort
109 end
110
111 install_from_git(git_repo, package_id)
112 else
113 var name = package_id.git_name
114 if name != null and name != "." and not name.is_empty then
115 name = name.to_lower
116 install_from_git(package_id, name)
117 else
118 print_error "Failed to infer the package name"
119 exit 1
120 end
121 end
122 end
123
124 private fun install_from_git(git_repo, name: String)
125 do
126 check_git
127
128 var target_dir = picnit_lib_dir / name
129 if target_dir.file_exists then
130 print_error "Already installed"
131 exit 1
132 end
133
134 var cmd = "git clone {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
135 if verbose then print "+ {cmd}"
136
137 if "NIT_TESTING".environ == "true" then
138 # Silence git output when testing
139 cmd += " 2> /dev/null"
140 end
141
142 var proc = new Process("sh", "-c", cmd)
143 proc.wait
144
145 if proc.status != 0 then
146 print_error "Install failed"
147 exit 1
148 end
149 end
150 end
151
152 # Upgrade a package
153 class CommandUpgrade
154 super Command
155
156 redef fun name do return "upgrade"
157 redef fun usage do return "picnit upgrade <package>"
158 redef fun description do return "Upgrade a package"
159
160 redef fun apply(args)
161 do
162 if args.length != 1 then
163 print_local_help
164 exit 1
165 end
166
167 var name = args.first
168 var target_dir = picnit_lib_dir / name
169
170 if not target_dir.file_exists or not target_dir.to_path.is_dir then
171 print_error "Package not found"
172 exit 1
173 end
174
175 check_git
176
177 var cmd = "cd {target_dir.escape_to_sh}; git pull"
178 if verbose then print "+ {cmd}"
179
180 var proc = new Process("sh", "-c", cmd)
181 proc.wait
182
183 if proc.status != 0 then
184 print_error "Upgrade failed"
185 exit 1
186 end
187 end
188 end
189
190 # Uninstall a package
191 class CommandUninstall
192 super Command
193
194 redef fun name do return "uninstall"
195 redef fun usage do return "picnit uninstall <package>"
196 redef fun description do return "Uninstall a package"
197
198 redef fun apply(args)
199 do
200 if args.length != 1 then
201 print_local_help
202 exit 1
203 end
204
205 var name = args.first
206 var target_dir = picnit_lib_dir / name
207
208 if not target_dir.file_exists or not target_dir.to_path.is_dir then
209 print_error "Package not found"
210 exit 1
211 end
212
213 # Ask confirmation
214 var response = prompt("Delete {target_dir.escape_to_sh}? [Y/n] ")
215 var accept = response != null and
216 (response.to_lower == "y" or response.to_lower == "yes" or response == "")
217 if not accept then return
218
219 var cmd = "rm -rf {target_dir.escape_to_sh}"
220 if verbose then print "+ {cmd}"
221
222 var proc = new Process("sh", "-c", cmd)
223 proc.wait
224
225 if proc.status != 0 then
226 print_error "Uninstall failed"
227 exit 1
228 end
229 end
230 end
231
232 # List all installed packages
233 class CommandList
234 super Command
235
236 redef fun name do return "list"
237 redef fun usage do return "picnit list"
238 redef fun description do return "List installed packages"
239
240 redef fun apply(args)
241 do
242 var files = picnit_lib_dir.files
243 for file in files do
244 var ini_path = picnit_lib_dir / file / "package.ini"
245 if verbose then print "- Reading ini file at {ini_path}"
246 var ini = new ConfigTree(ini_path)
247 var tags = ini["package.tags"]
248
249 if tags != null then
250 print "{file.justify(15, 0.0)} {tags}"
251 else
252 print file
253 end
254 end
255 end
256 end
257
258 # Show general help or help specific to a command
259 class CommandHelp
260 super Command
261
262 redef fun name do return "help"
263 redef fun usage do return "picnit help [command]"
264 redef fun description do return "Show general help message or the help for a command"
265
266 redef fun apply(args)
267 do
268 # Try first to help about a valid action
269 if args.length == 1 then
270 var command = commands.get_or_null(args.first)
271 if command != null then
272 command.print_local_help
273 return
274 end
275 end
276
277 print_help
278 end
279 end
280
281 redef class Sys
282
283 # General command line options
284 var opts = new OptionContext
285
286 # Help option
287 var opt_help = new OptionBool("Show this help message", "--help", "-h")
288
289 # Verbose mode option
290 var opt_verbose = new OptionBool("Print more information", "--verbose", "-v")
291 private fun verbose: Bool do return opt_verbose.value
292
293 # All command line actions, mapped to their short `name`
294 var commands = new Map[String, Command]
295
296 private var command_install = new CommandInstall(commands)
297 private var command_list = new CommandList(commands)
298 private var command_update = new CommandUpgrade(commands)
299 private var command_uninstall = new CommandUninstall(commands)
300 private var command_help = new CommandHelp(commands)
301 end
302
303 redef fun picnit_lib_dir
304 do
305 if "NIT_TESTING".environ == "true" then
306 return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ
307 else return super
308 end
309
310 # Print the general help message
311 private fun print_help
312 do
313 print "usage: picnit <command> [options]"
314 print ""
315
316 print "commands:"
317 for command in commands.values do
318 print " {command.name.justify(11, 0.0)} {command.description}"
319 end
320 print ""
321
322 print "options:"
323 opts.usage
324 end
325
326 # Check if `git` is available, exit if not
327 private fun check_git
328 do
329 var proc = new ProcessReader("git", "--version")
330 proc.wait
331 proc.close
332
333 if proc.status != 0 then
334 print_error "Please install `git`"
335 exit 1
336 end
337 end
338
339 # Parse main options
340 opts.add_option(opt_help, opt_verbose)
341 opts.parse
342 var rest = opts.rest
343
344 if opt_help.value then
345 print_help
346 exit 0
347 end
348
349 if opts.errors.not_empty then
350 for error in opts.errors do print error
351 print ""
352 print_help
353 exit 1
354 end
355
356 if rest.is_empty then
357 print_help
358 exit 1
359 end
360
361 # Find and apply action
362 var action_name = rest.shift
363 var action = commands.get_or_null(action_name)
364 if action != null then
365 action.apply rest
366 else
367 print_help
368 exit 1
369 end