nitg: Extract exec_and_check from Android
[nit.git] / src / toolcontext.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2006-2008 Floréal Morandat <morandat@lirmm.fr>
4 # Copyright 2008-2012 Jean Privat <jean@pryen.org>
5 # Copyright 2009 Jean-Sebastien Gelinas <calestar@gmail.com>
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18
19 # Common command-line tool infractructure than handle options and error messages
20 module toolcontext
21
22 import opts
23 import location
24 import version
25
26 class Message
27 super Comparable
28 redef type OTHER: Message
29
30 var location: nullable Location
31 var text: String
32
33 # Comparisons are made on message locations.
34 redef fun <(other: OTHER): Bool do
35 if location == null then return true
36 if other.location == null then return false
37
38 return location.as(not null) < other.location.as(not null)
39 end
40
41 redef fun to_s: String
42 do
43 var l = location
44 if l == null then
45 return text
46 else
47 return "{l}: {text}"
48 end
49 end
50
51 fun to_color_string: String
52 do
53 var esc = 27.ascii
54 var red = "{esc}[0;31m"
55 var bred = "{esc}[1;31m"
56 var green = "{esc}[0;32m"
57 var yellow = "{esc}[0;33m"
58 var def = "{esc}[0m"
59
60 var l = location
61 if l == null then
62 return text
63 else if l.file == null then
64 return "{yellow}{l}{def}: {text}"
65 else
66 return "{yellow}{l}{def}: {text}\n{l.colored_line("1;31")}"
67 end
68 end
69 end
70
71 # Global context for tools
72 class ToolContext
73 # Number of errors
74 var error_count: Int = 0
75
76 # Number of warnings
77 var warning_count: Int = 0
78
79 # Directory where to generate log files
80 var log_directory: String = "logs"
81
82 # Messages
83 private var messages: Array[Message] = new Array[Message]
84 private var message_sorter: ComparableSorter[Message] = new ComparableSorter[Message]
85
86 fun check_errors
87 do
88 if messages.length > 0 then
89 message_sorter.sort(messages)
90
91 for m in messages do
92 if opt_no_color.value then
93 stderr.write("{m}\n")
94 else
95 stderr.write("{m.to_color_string}\n")
96 end
97 end
98
99 messages.clear
100 end
101
102 if error_count > 0 then exit(1)
103 end
104
105 # Display an error
106 fun error(l: nullable Location, s: String)
107 do
108 messages.add(new Message(l,s))
109 error_count = error_count + 1
110 if opt_stop_on_first_error.value then check_errors
111 end
112
113 # Add an error, show errors and quit
114 fun fatal_error(l: nullable Location, s: String)
115 do
116 error(l,s)
117 check_errors
118 end
119
120 # Display a warning
121 fun warning(l: nullable Location, s: String)
122 do
123 if opt_warn.value == 0 then return
124 messages.add(new Message(l,s))
125 warning_count = warning_count + 1
126 if opt_stop_on_first_error.value then check_errors
127 end
128
129 # Display an info
130 fun info(s: String, level: Int)
131 do
132 if level <= verbose_level then
133 print "{s}"
134 end
135 end
136
137 fun exec_and_check(args: Array[String], error: String)
138 do
139 var prog = args.first
140 args.remove_at 0
141
142 # Is the wanted program available?
143 var proc_which = new IProcess.from_a("which", [prog])
144 proc_which.wait
145 var res = proc_which.status
146 if res != 0 then
147 print "{error}: executable \"{prog}\" not found"
148 exit 1
149 end
150
151 # Execute the wanted program
152 var proc = new Process.from_a(prog, args)
153 proc.wait
154 res = proc.status
155 if res != 0 then
156 print "{error}: execution of \"{prog} {args.join(" ")}\" failed"
157 exit 1
158 end
159 end
160
161 # Global OptionContext
162 var option_context: OptionContext = new OptionContext
163
164 # Option --warn
165 var opt_warn: OptionCount = new OptionCount("Show warnings", "-W", "--warn")
166
167 # Option --quiet
168 var opt_quiet: OptionBool = new OptionBool("Do not show warnings", "-q", "--quiet")
169
170 # Option --log
171 var opt_log: OptionBool = new OptionBool("Generate various log files", "--log")
172
173 # Option --log-dir
174 var opt_log_dir: OptionString = new OptionString("Directory where to generate log files", "--log-dir")
175
176 # Option --help
177 var opt_help: OptionBool = new OptionBool("Show Help (This screen)", "-h", "-?", "--help")
178
179 # Option --version
180 var opt_version: OptionBool = new OptionBool("Show version and exit", "--version")
181
182 # Option --verbose
183 var opt_verbose: OptionCount = new OptionCount("Verbose", "-v", "--verbose")
184
185 # Option --stop-on-first-error
186 var opt_stop_on_first_error: OptionBool = new OptionBool("Stop on first error", "--stop-on-first-error")
187
188 # Option --no-color
189 var opt_no_color: OptionBool = new OptionBool("Do not use color to display errors and warnings", "--no-color")
190
191 # Verbose level
192 var verbose_level: Int = 0
193
194 init
195 do
196 option_context.add_option(opt_warn, opt_quiet, opt_stop_on_first_error, opt_no_color, opt_log, opt_log_dir, opt_help, opt_version, opt_verbose)
197 end
198
199 # Name, usage and synopsis of the tool.
200 # It is mainly used in `usage`.
201 # Should be correctly set by the client before calling `process_options`
202 # A multi-line string is recommmended.
203 #
204 # eg. `"Usage: tool [OPTION]... [FILE]...\nDo some things."`
205 var tooldescription: String writable = "Usage: [OPTION]... [ARG]..."
206
207 # Does `process_options` should accept an empty sequence of arguments.
208 # ie. nothing except options.
209 # Is `false` by default.
210 #
211 # If required, if should be set by the client before calling `process_options`
212 var accept_no_arguments writable = false
213
214 # print the full usage of the tool.
215 # Is called by `process_option` on `--help`.
216 # It also could be called by the client.
217 fun usage
218 do
219 print tooldescription
220 option_context.usage
221 end
222
223 # Parse and process the options given on the command line
224 fun process_options(args: Sequence[String])
225 do
226 self.opt_warn.value = 1
227
228 # init options
229 option_context.parse(args)
230
231 if opt_help.value then
232 usage
233 exit 0
234 end
235
236 if opt_version.value then
237 print nit_version
238 exit 0
239 end
240
241 var errors = option_context.get_errors
242 if not errors.is_empty then
243 for e in errors do print "Error: {e}"
244 print tooldescription
245 print "Use --help for help"
246 exit 1
247 end
248
249 if option_context.rest.is_empty and not accept_no_arguments then
250 print tooldescription
251 print "Use --help for help"
252 exit 1
253 end
254
255 # Set verbose level
256 verbose_level = opt_verbose.value
257
258 if self.opt_quiet.value then self.opt_warn.value = 0
259
260 if opt_log_dir.value != null then log_directory = opt_log_dir.value.as(not null)
261 if opt_log.value then
262 # Make sure the output directory exists
263 log_directory.mkdir
264 end
265
266 nit_dir = compute_nit_dir
267 end
268
269 # The identified root directory of the Nit project
270 var nit_dir: nullable String
271
272 private fun compute_nit_dir: nullable String
273 do
274 # a environ variable has precedence
275 var res = "NIT_DIR".environ
276 if not res.is_empty then return res
277
278 # find the runpath of the program from argv[0]
279 res = "{sys.program_name.dirname}/.."
280 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
281
282 # find the runpath of the process from /proc
283 var exe = "/proc/self/exe"
284 if exe.file_exists then
285 res = exe.realpath
286 res = res.dirname.join_path("..")
287 if res.file_exists and "{res}/src/nit.nit".file_exists then return res.simplify_path
288 end
289
290 return null
291 end
292 end