cli: introduce Git-like commands
[barectf.git] / barectf / cli.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2014-2020 Philippe Proulx <pproulx@efficios.com>
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
12 #
13 # The above copyright notice and this permission notice shall be
14 # included in all copies or substantial portions of the Software.
15 #
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
24 import pkg_resources
25 import termcolor
26 import argparse
27 import os.path
28 import barectf
29 import barectf.config_parse_common as barectf_config_parse_common
30 import barectf.argpar as barectf_argpar
31 import sys
32 import os
33
34
35 # Colors and prints the error message `msg` and exits with status code
36 # 1.
37 def _print_error(msg):
38 termcolor.cprint('Error: ', 'red', end='', file=sys.stderr)
39 termcolor.cprint(msg, 'red', attrs=['bold'], file=sys.stderr)
40 sys.exit(1)
41
42
43 # Pretty-prints the barectf configuration error `exc` and exits with
44 # status code 1.
45 def _print_config_error(exc):
46 # reverse: most precise message comes last
47 for ctx in reversed(exc.context):
48 msg = ''
49
50 if ctx.message is not None:
51 msg = f' {ctx.message}'
52
53 color = 'red'
54 termcolor.cprint(f'{ctx.name}', color, attrs=['bold'], file=sys.stderr, end='')
55 termcolor.cprint(':', color, file=sys.stderr, end='')
56 termcolor.cprint(msg, color, file=sys.stderr)
57
58 sys.exit(1)
59
60
61 # Pretty-prints the unknown exception `exc`.
62 def _print_unknown_exc(exc):
63 import traceback
64
65 traceback.print_exc()
66 _print_error(f'Unknown exception: {exc}')
67
68
69 # Finds and returns all the option items in `items` having the long name
70 # `long_name`.
71 def _find_opt_items(items, long_name):
72 ret_items = []
73
74 for item in items:
75 if type(item) is barectf_argpar._OptItem and item.descr.long_name == long_name:
76 ret_items.append(item)
77
78 return ret_items
79
80
81 # Returns:
82 #
83 # For an option item without an argument:
84 # `True`.
85 #
86 # For an option item with an argument:
87 # Its argument.
88 #
89 # Uses the last option item having the long name `long_name` found in
90 # `items`.
91 #
92 # Returns `default` if there's no such option item.
93 def _opt_item_val(items, long_name, default=None):
94 opt_items = _find_opt_items(items, long_name)
95
96 if len(opt_items) == 0:
97 return default
98
99 opt_item = opt_items[-1]
100
101 if opt_item.descr.has_arg:
102 return opt_item.arg_text
103
104 return True
105
106
107 class _CliCfg:
108 pass
109
110
111 class _CliGenCmdCfg(_CliCfg):
112 def __init__(self, config_file_path, c_source_dir, c_header_dir, metadata_stream_dir,
113 inclusion_dirs, ignore_inclusion_not_found, dump_config, v2_prefix):
114 self._config_file_path = config_file_path
115 self._c_source_dir = c_source_dir
116 self._c_header_dir = c_header_dir
117 self._metadata_stream_dir = metadata_stream_dir
118 self._inclusion_dirs = inclusion_dirs
119 self._ignore_inclusion_not_found = ignore_inclusion_not_found
120 self._dump_config = dump_config
121 self._v2_prefix = v2_prefix
122
123 @property
124 def config_file_path(self):
125 return self._config_file_path
126
127 @property
128 def c_source_dir(self):
129 return self._c_source_dir
130
131 @property
132 def c_header_dir(self):
133 return self._c_header_dir
134
135 @property
136 def metadata_stream_dir(self):
137 return self._metadata_stream_dir
138
139 @property
140 def inclusion_dirs(self):
141 return self._inclusion_dirs
142
143 @property
144 def ignore_inclusion_not_found(self):
145 return self._ignore_inclusion_not_found
146
147 @property
148 def dump_config(self):
149 return self._dump_config
150
151 @property
152 def v2_prefix(self):
153 return self._v2_prefix
154
155
156 def _print_gen_cmd_usage():
157 print('''Usage: barectf generate [--code-dir=DIR] [--headers-dir=DIR]
158 [--metadata-dir=DIR] [--prefix=PREFIX]
159 [--include-dir=DIR]... [--ignore-include-not-found]
160 [--dump-config] CONFIG-FILE-PATH
161
162 Options:
163 -c DIR, --code-dir=DIR Write C source files to DIR
164 --dump-config Print the effective configuration file
165 -H DIR, --headers-dir=DIR Write C header files to DIR
166 --ignore-include-not-found Continue to process the configuration file when
167 included files are not found
168 -I DIR, --include-dir=DIR Add DIR to the list of directories to be
169 searched for inclusion files
170 -m DIR, --metadata-dir=DIR Write the metadata stream file to DIR
171 -p PREFIX, --prefix=PREFIX Set the configuration prefix to PREFIX''')
172
173
174 class _CliError(Exception):
175 pass
176
177
178 def _cli_gen_cfg_from_args(orig_args):
179 # parse original arguments
180 opt_descrs = [
181 barectf_argpar.OptDescr('h', 'help'),
182 barectf_argpar.OptDescr('c', 'code-dir', True),
183 barectf_argpar.OptDescr('H', 'headers-dir', True),
184 barectf_argpar.OptDescr('I', 'include-dir', True),
185 barectf_argpar.OptDescr('m', 'metadata-dir', True),
186 barectf_argpar.OptDescr('p', 'prefix', True),
187 barectf_argpar.OptDescr(long_name='dump-config'),
188 barectf_argpar.OptDescr(long_name='ignore-include-not-found'),
189 ]
190 res = barectf_argpar.parse(orig_args, opt_descrs)
191 assert len(res.ingested_orig_args) == len(orig_args)
192
193 # command help?
194 if len(_find_opt_items(res.items, 'help')) > 0:
195 _print_gen_cmd_usage()
196 sys.exit()
197
198 # check configuration file path
199 config_file_path = None
200
201 for item in res.items:
202 if type(item) is barectf_argpar._NonOptItem:
203 if config_file_path is not None:
204 raise _CliError('Multiple configuration file paths provided')
205
206 config_file_path = item.text
207
208 if config_file_path is None:
209 raise _CliError('Missing configuration file path')
210
211 if not os.path.isfile(config_file_path):
212 raise _CliError(f'`{config_file_path}` is not an existing, regular file')
213
214 # directories
215 c_source_dir = _opt_item_val(res.items, 'code-dir', os.getcwd())
216 c_header_dir = _opt_item_val(res.items, 'headers-dir', os.getcwd())
217 metadata_stream_dir = _opt_item_val(res.items, 'metadata-dir', os.getcwd())
218 inclusion_dirs = [item.arg_text for item in _find_opt_items(res.items, 'include-dir')]
219
220 for dir in [c_source_dir, c_header_dir, metadata_stream_dir] + inclusion_dirs:
221 if not os.path.isdir(dir):
222 raise _CliError(f'`{dir}` is not an existing directory')
223
224 inclusion_dirs.append(os.getcwd())
225
226 # other options
227 ignore_inclusion_not_found = _opt_item_val(res.items, 'ignore-include-not-found', False)
228 dump_config = _opt_item_val(res.items, 'dump-config', False)
229 v2_prefix = _opt_item_val(res.items, 'prefix')
230
231 return _CliGenCmdCfg(config_file_path, c_source_dir, c_header_dir, metadata_stream_dir,
232 inclusion_dirs, ignore_inclusion_not_found, dump_config, v2_prefix)
233
234
235 def _print_general_usage():
236 print('''Usage: barectf COMMAND COMMAND-ARGS
237 barectf --help
238 barectf --version
239
240 General options:
241 -h, --help Show this help and quit
242 -V, --version Show version and quit
243
244 Available commands:
245 gen, generate Generate the C source and CTF metadata files of a tracer
246 from a configuration file
247
248 Run `barectf COMMAND --help` to show the help of COMMAND.''')
249
250
251 def _cli_cfg_from_args():
252 # We use our `argpar` module here instead of Python's `argparse`
253 # because we need to support the two following use cases:
254 #
255 # $ barectf config.yaml
256 # $ barectf generate config.yaml
257 #
258 # In other words, the default command is `generate` (for backward
259 # compatibility reasons). The argument parser must not consider
260 # `config.yaml` as being a command name.
261 general_opt_descrs = [
262 barectf_argpar.OptDescr('V', 'version'),
263 barectf_argpar.OptDescr('h', 'help'),
264 ]
265 orig_args = sys.argv[1:]
266 res = barectf_argpar.parse(orig_args, general_opt_descrs, False)
267
268 # find command name, collecting preceding (common) option items
269 general_opt_items = []
270 cmd_first_orig_arg_index = None
271 cmd_name = None
272
273 for item in res.items:
274 if type(item) is barectf_argpar._NonOptItem:
275 if item.text in ['gen', 'generate']:
276 cmd_name = 'generate'
277 cmd_first_orig_arg_index = item.orig_arg_index + 1
278 else:
279 cmd_first_orig_arg_index = item.orig_arg_index
280
281 break
282 else:
283 assert type(item) is barectf_argpar._OptItem
284 general_opt_items.append(item)
285
286 # general help?
287 if len(_find_opt_items(general_opt_items, 'help')) > 0:
288 _print_general_usage()
289 sys.exit()
290
291 # version?
292 if len(_find_opt_items(general_opt_items, 'version')) > 0:
293 print(f'barectf {barectf.__version__}')
294 sys.exit()
295
296 # execute command
297 cmd_orig_args = orig_args[cmd_first_orig_arg_index:]
298
299 if cmd_name is None:
300 # default `generate` command
301 return _cli_gen_cfg_from_args(cmd_orig_args)
302 else:
303 assert cmd_name == 'generate'
304 return _cli_gen_cfg_from_args(cmd_orig_args)
305
306
307 def _run():
308 # parse arguments
309 try:
310 cli_cfg = _cli_cfg_from_args()
311 except barectf_argpar._Error as exc:
312 _print_error(f'Command-line: For argument `{exc.orig_arg}`: {exc.msg}')
313 except _CliError as exc:
314 _print_error(f'Command-line: {exc}')
315
316 assert type(cli_cfg) is _CliGenCmdCfg
317
318 # create configuration
319 try:
320 with open(cli_cfg.config_file_path) as f:
321 if cli_cfg.dump_config:
322 # print effective configuration file
323 print(barectf.effective_configuration_file(f, True, cli_cfg.inclusion_dirs,
324 cli_cfg.ignore_inclusion_not_found))
325
326 # barectf.configuration_from_file() reads the file again
327 # below: rewind.
328 f.seek(0)
329
330 config = barectf.configuration_from_file(f, True, cli_cfg.inclusion_dirs,
331 cli_cfg.ignore_inclusion_not_found)
332 except barectf._ConfigurationParseError as exc:
333 _print_config_error(exc)
334 except Exception as exc:
335 _print_unknown_exc(exc)
336
337 if cli_cfg.v2_prefix is not None:
338 # Override prefixes.
339 #
340 # For historical reasons, the `--prefix` option applies the
341 # barectf 2 configuration prefix rules. Therefore, get the
342 # equivalent barectf 3 prefixes first.
343 v3_prefixes = barectf_config_parse_common._v3_prefixes_from_v2_prefix(cli_cfg.v2_prefix)
344 cg_opts = config.options.code_generation_options
345 cg_opts = barectf.ConfigurationCodeGenerationOptions(v3_prefixes.identifier,
346 v3_prefixes.file_name,
347 cg_opts.default_stream_type,
348 cg_opts.header_options,
349 cg_opts.clock_type_c_types)
350 config = barectf.Configuration(config.trace, barectf.ConfigurationOptions(cg_opts))
351
352 # create a barectf code generator
353 code_gen = barectf.CodeGenerator(config)
354
355 def write_file(dir, file):
356 with open(os.path.join(dir, file.name), 'w') as f:
357 f.write(file.contents)
358
359 def write_files(dir, files):
360 for file in files:
361 write_file(dir, file)
362
363 try:
364 # generate and write metadata stream file
365 write_file(cli_cfg.metadata_stream_dir, code_gen.generate_metadata_stream())
366
367 # generate and write C header files
368 write_files(cli_cfg.c_header_dir, code_gen.generate_c_headers())
369
370 # generate and write C source files
371 write_files(cli_cfg.c_source_dir, code_gen.generate_c_sources())
372 except Exception as exc:
373 # We know `config` is valid, therefore the code generator cannot
374 # fail for a reason known to barectf.
375 _print_unknown_exc(exc)
This page took 0.061282 seconds and 4 git commands to generate.