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