opencos-eda 0.2.47__py3-none-any.whl → 0.2.49__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. opencos/__init__.py +4 -2
  2. opencos/_version.py +10 -7
  3. opencos/commands/flist.py +8 -7
  4. opencos/commands/multi.py +35 -18
  5. opencos/commands/sweep.py +9 -4
  6. opencos/commands/waves.py +1 -1
  7. opencos/deps/__init__.py +0 -0
  8. opencos/deps/defaults.py +69 -0
  9. opencos/deps/deps_commands.py +419 -0
  10. opencos/deps/deps_file.py +326 -0
  11. opencos/deps/deps_processor.py +670 -0
  12. opencos/deps_schema.py +7 -8
  13. opencos/eda.py +92 -67
  14. opencos/eda_base.py +625 -332
  15. opencos/eda_config.py +80 -14
  16. opencos/eda_extract_targets.py +22 -14
  17. opencos/eda_tool_helper.py +33 -7
  18. opencos/export_helper.py +166 -86
  19. opencos/export_json_convert.py +31 -23
  20. opencos/files.py +2 -1
  21. opencos/hw/__init__.py +0 -0
  22. opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
  23. opencos/names.py +0 -4
  24. opencos/peakrdl_cleanup.py +13 -7
  25. opencos/seed.py +19 -11
  26. opencos/tests/helpers.py +27 -14
  27. opencos/tests/test_deps_helpers.py +35 -32
  28. opencos/tests/test_eda.py +47 -41
  29. opencos/tests/test_eda_elab.py +5 -3
  30. opencos/tests/test_eda_synth.py +1 -1
  31. opencos/tests/test_oc_cli.py +1 -1
  32. opencos/tests/test_tools.py +3 -2
  33. opencos/tools/iverilog.py +2 -2
  34. opencos/tools/modelsim_ase.py +2 -2
  35. opencos/tools/riviera.py +1 -1
  36. opencos/tools/slang.py +1 -1
  37. opencos/tools/surelog.py +1 -1
  38. opencos/tools/verilator.py +1 -1
  39. opencos/tools/vivado.py +1 -1
  40. opencos/tools/yosys.py +4 -3
  41. opencos/util.py +440 -483
  42. opencos/utils/__init__.py +0 -0
  43. opencos/utils/markup_helpers.py +98 -0
  44. opencos/utils/str_helpers.py +111 -0
  45. opencos/utils/subprocess_helpers.py +108 -0
  46. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
  47. opencos_eda-0.2.49.dist-info/RECORD +88 -0
  48. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
  49. opencos/deps_helpers.py +0 -1346
  50. opencos_eda-0.2.47.dist-info/RECORD +0 -79
  51. /opencos/{pcie.py → hw/pcie.py} +0 -0
  52. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
  53. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
  54. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
  55. {opencos_eda-0.2.47.dist-info → opencos_eda-0.2.49.dist-info}/top_level.txt +0 -0
opencos/deps_schema.py CHANGED
@@ -133,7 +133,8 @@ import sys
133
133
 
134
134
  from schema import Schema, Or, Optional, SchemaError
135
135
 
136
- from opencos import util, deps_helpers
136
+ from opencos import util
137
+ from opencos.deps import deps_file
137
138
 
138
139
  # Because we deal with YAML, where a Table Key with dangling/empty value is allowed
139
140
  # and we have things like SystemVerilog defines where there's a Table key with no Value,
@@ -312,21 +313,19 @@ FILE_SIMPLIFIED = Schema(
312
313
 
313
314
  def check(data: dict, schema_obj=FILE) -> (bool, str):
314
315
  '''Returns (bool, str) for checking dict against FILE schema'''
315
-
316
-
317
316
  try:
318
317
  schema_obj.validate(data)
319
318
  return True, None
320
319
  except SchemaError as e:
321
- return False, str(e)
322
- except Exception as e: # pylint: disable=broad-exception-caught
320
+ return False, f'SchemaError: {e}'
321
+ except Exception as e:
323
322
  return False, str(e)
324
323
 
325
324
 
326
325
  def deps_markup_safe_load(deps_filepath: str) -> (bool, dict):
327
326
  '''Returns tuple (bool False if took errors, dict of markp data)'''
328
327
  current_errors = util.args['errors']
329
- data = deps_helpers.deps_markup_safe_load(deps_filepath)
328
+ data = deps_file.deps_markup_safe_load(deps_filepath)
330
329
  if util.args['errors'] > current_errors:
331
330
  return False, data
332
331
  return True, data
@@ -337,10 +336,10 @@ def check_file(filepath: str, schema_obj=FILE) -> (bool, str, str):
337
336
 
338
337
  deps_filepath = filepath
339
338
  if os.path.isdir(filepath):
340
- deps_filepath = deps_helpers.get_deps_markup_file(base_path=filepath)
339
+ deps_filepath = deps_file.get_deps_markup_file(base_path=filepath)
341
340
 
342
341
  # get deps file
343
- if not os.path.exists(deps_filepath):
342
+ if not os.path.isfile(deps_filepath):
344
343
  print(f'ERROR: internal error(s) no DEPS.[yml|..] found in {filepath=}')
345
344
  return False, '', deps_filepath
346
345
 
opencos/eda.py CHANGED
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- # SPDX-License-Identifier: MPL-2.0
3
+ '''opencos.eda is an executable script (as `eda <command> ...`)
4
+
5
+ This is the entrypoint for tool discovery, and running targets from command line or DEPS
6
+ markup files
7
+ '''
4
8
 
5
9
  import subprocess
6
10
  import os
@@ -13,14 +17,13 @@ import shlex
13
17
  import importlib.util
14
18
 
15
19
  import opencos
16
- from opencos import util, files
17
- from opencos import eda_config
18
- from opencos import eda_base
20
+ from opencos import util, eda_config, eda_base
19
21
  from opencos.eda_base import Tool, which_tool
20
22
 
21
- # Globals
22
-
23
+ # Configure util:
23
24
  util.progname = "EDA"
25
+ util.global_log.default_log_enabled = True
26
+ util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
24
27
 
25
28
 
26
29
  # ******************************************************************************
@@ -45,7 +48,7 @@ def init_config(
45
48
  else:
46
49
  config['command_handler'][command] = cls
47
50
 
48
- config['auto_tools_found'] = dict()
51
+ config['auto_tools_found'] = {}
49
52
  config['tools_loaded'] = set()
50
53
  if run_auto_tool_setup:
51
54
  config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
@@ -54,7 +57,7 @@ def init_config(
54
57
 
55
58
 
56
59
 
57
- def usage(tokens: list, config: dict, command=""):
60
+ def usage(tokens: list, config: dict, command="") -> int:
58
61
  '''Returns an int shell return code, given remaining args (tokens list) and eda command.
59
62
 
60
63
  config is the config dict. Used to check valid commands in config['command_handler']
@@ -100,41 +103,44 @@ And <files|targets, ...> is one or more source file or DEPS markup file target,
100
103
  )
101
104
  eda_base.print_base_help()
102
105
  return 0
103
- elif command in config['command_handler'].keys():
106
+
107
+ if command in config['command_handler'].keys():
104
108
  sco = config['command_handler'][command](config=config) # sub command object
105
109
  sco.help(tokens=tokens)
106
110
  return util.exit(0)
107
- else:
108
- util.info(f"Valid commands are: ")
109
- for k in sorted(config['command_handler'].keys()):
110
- util.info(f" {k:20}")
111
- return util.error(f"Cannot provide help, don't understand command: '{command}'")
112
111
 
113
- def interactive(config: dict):
114
- read_file = False
112
+ util.info("Valid commands are:")
113
+ for k in sorted(config['command_handler'].keys()):
114
+ util.info(f" {k:20}")
115
+ return util.error(f"Cannot provide help, don't understand command: '{command}'")
116
+
117
+
118
+ def interactive(config: dict) -> int:
119
+ '''Returns bash/sh return code, entry point for running interactively in CLI if
120
+
121
+ args are not present to directly call a command handler, --help, etc
122
+ '''
123
+ rc = 0
115
124
  while True:
116
- if read_file:
117
- line = f.readline()
118
- if line:
119
- print("%s->%s" % (fname, line), end="")
120
- else:
121
- read_file = False
122
- f.close()
123
- continue
124
- else:
125
- line = input('EDA->')
125
+ line = input('EDA->')
126
126
  m = re.match(r'^([^\#]*)\#.*$', line)
127
- if m: line = m.group(1)
127
+ if m:
128
+ line = m.group(1)
128
129
  tokens = line.split()
129
130
  original_args = tokens.copy()
130
131
  # NOTE: interactive will not correctly handle --config-yml arg (from eda_config.py),
131
132
  # but we should do a best effor to re-parse args from util.py, such as
132
133
  # --quiet, --color, --fancy, --logfile, --debug or --debug-level, etc
133
134
  _, tokens = util.process_tokens(tokens)
134
- process_tokens(tokens=tokens, original_args=original_args, config=config, interactive=True)
135
+ rc = process_tokens(
136
+ tokens=tokens, original_args=original_args, config=config, is_interactive=True
137
+ )
138
+ return rc
135
139
 
136
140
 
137
- def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) -> dict:
141
+ def auto_tool_setup( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
142
+ warnings: bool = True, config = None, quiet: bool = False, tool: str = ''
143
+ ) -> dict:
138
144
  '''Returns an updated config, uses config['auto_tools_order'][0] dict, calls tool_setup(..)
139
145
 
140
146
  -- adds items to config['tools_loaded'] set
@@ -150,8 +156,8 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
150
156
  )
151
157
 
152
158
  assert 'auto_tools_order' in config
153
- assert type(config['auto_tools_order']) is list
154
- assert type(config['auto_tools_order'][0]) is dict
159
+ assert isinstance(config['auto_tools_order'], list)
160
+ assert isinstance(config['auto_tools_order'][0], dict)
155
161
 
156
162
  for name, value in config['auto_tools_order'][0].items():
157
163
  if tool and tool != name:
@@ -159,12 +165,13 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
159
165
 
160
166
  util.debug(f"Checking for ability to run tool: {name}")
161
167
  exe = value.get('exe', str())
162
- if type(exe) is list:
168
+ if isinstance(exe, list):
163
169
  exe_list = exe
164
- elif type(exe) is str:
170
+ elif isinstance(exe, str):
165
171
  exe_list = [exe] # make it a list
166
172
  else:
167
- util.error(f'eda.py: config["auto_tools_order"][0] for {name=} {value=} has bad type for {exe=}')
173
+ util.error(f'eda.py: config["auto_tools_order"][0] for {name=} {value=} has bad type'
174
+ f'for {exe=}')
168
175
  continue
169
176
 
170
177
  has_all_py = True
@@ -200,14 +207,16 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
200
207
  for cmd in requires_cmd_list:
201
208
  cmd_list = shlex.split(cmd)
202
209
  try:
203
- proc = subprocess.run(cmd_list, capture_output=True, input=b'exit\n\n')
210
+ proc = subprocess.run(cmd_list, capture_output=True, check=False,
211
+ input=b'exit\n\n')
204
212
  if proc.returncode != 0:
205
213
  if not quiet:
206
- util.debug(f"For tool {name} missing required command ({proc.returncode=}): {cmd_list=}")
214
+ util.debug(f"For tool {name} missing required command",
215
+ f"({proc.returncode=}): {cmd_list=}")
207
216
  has_all_exe = False
208
- except:
217
+ except Exception as e:
209
218
  has_all_exe = False
210
- util.debug(f"... No, exception running {cmd_list}")
219
+ util.debug(f"... No, exception {e} running {cmd_list}")
211
220
 
212
221
 
213
222
  if all([has_all_py, has_all_env, has_all_exe, has_all_in_exe_path]):
@@ -262,12 +271,11 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
262
271
  if warnings:
263
272
  util.warning(f"tool_setup: {auto_setup=} already setup for {tool}?")
264
273
 
265
- entry = config['auto_tools_order'][0].get(tool, dict())
266
- tool_cmd_handler_dict = entry.get('handlers', dict())
274
+ entry = config['auto_tools_order'][0].get(tool, {})
275
+ tool_cmd_handler_dict = entry.get('handlers', {})
267
276
 
268
277
  for command, str_class_name in tool_cmd_handler_dict.items():
269
278
  current_handler_cls = config['command_handler'].get(command, None)
270
- ext_mod = None
271
279
 
272
280
  if auto_setup and current_handler_cls is not None and issubclass(current_handler_cls, Tool):
273
281
  # If we're not in auto_setup, then always override (aka arg --tool=<this tool>)
@@ -276,19 +284,28 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
276
284
 
277
285
  cls = util.import_class_from_string(str_class_name)
278
286
 
279
- assert issubclass(cls, Tool), f'{str_class_name=} is does not have Tool class associated with it'
287
+ assert issubclass(cls, Tool), \
288
+ f'{str_class_name=} is does not have Tool class associated with it'
280
289
  util.debug(f'Setting {cls=} for {command=} in config.command_handler')
281
290
  config['command_handler'][command] = cls
282
291
 
283
292
  config['tools_loaded'].add(tool)
284
293
 
285
294
 
286
- def process_tokens(tokens: list, original_args: list, config: dict, interactive=False):
287
- # this is the top level token processing function. tokens can come from command line, setup file, or interactively.
288
- # we do one pass through all the tokens, triaging them into:
289
- # - those we can execute immediate (help, quit, and global opens like --debug, --color)
290
- # - a command (sim, synth, etc)
291
- # - command arguments (--seed, +define, +incdir, etc) which will be deferred and processed by the command
295
+ def process_tokens( # pylint: disable=too-many-branches,too-many-statements
296
+ tokens: list, original_args: list, config: dict, is_interactive=False
297
+ ) -> int:
298
+ '''Returns bash/sh style return code int (0 pass, non-zero fail).
299
+
300
+ This is the top level token processing function, and entry point (after util and eda_config
301
+ have performed their argparsing). Tokens can come from command line, DEPS target markup file,
302
+ or interactively. We do one pass through all the tokens, triaging them into:
303
+ - those we can execute immediate (help, quit, and global opens like --debug, --color)
304
+ -- some of this has already been performed in main() by util for --color.
305
+ - a command (sim, synth, etc)
306
+ - command arguments (--seed, +define, +incdir, etc) which will be deferred and processed by
307
+ the command. Some are processed here (--tool)
308
+ '''
292
309
 
293
310
  deferred_tokens = []
294
311
  command = ""
@@ -328,7 +345,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
328
345
  unparsed.remove(value) # remove command (flist, export, targets, etc)
329
346
  break
330
347
 
331
- if not interactive:
348
+ if not is_interactive:
332
349
  # Run init_config() now, we deferred it in main(), but only run it
333
350
  # for this tool (or tool=None to figure it out)
334
351
  config = init_config(
@@ -336,7 +353,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
336
353
  run_auto_tool_setup=run_auto_tool_setup
337
354
  )
338
355
  if not config:
339
- util.error(f'eda.py main: problem loading config, {args=}')
356
+ util.error(f'eda.py main: problem loading config, {tokens=}')
340
357
  return 3
341
358
 
342
359
  # Deal with help, now that we have the command (if it was set).
@@ -354,9 +371,9 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
354
371
  tool_setup(parsed.tool, config=config)
355
372
 
356
373
  deferred_tokens = unparsed
357
- if command == "":
374
+ if not command:
358
375
  util.error("Didn't get a command!")
359
- return 1
376
+ return 2
360
377
 
361
378
  sco = config['command_handler'][command](config=config) # sub command object
362
379
  util.debug(f'{command=}')
@@ -377,16 +394,23 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
377
394
  # Add the original, nothing-parsed args to the Command.config dict.
378
395
  sco.config['eda_original_args'] = original_args
379
396
 
380
- setattr(sco, 'command_name', command) # as a safeguard, b/c 'command' is not always passed to 'sco'
397
+ setattr(sco, 'command_name', command) # as a safeguard, 'command' is not always passed to 'sco'
381
398
  unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
382
399
 
383
- # query the status from the Command object (0 is pass, > 0 is fail)
384
- rc = getattr(sco, 'status', 1)
400
+ # query the status from the Command object (0 is pass, > 0 is fail, but we'd prefer to avoid
401
+ # rc=1 because that's the python exception rc)
402
+ rc = getattr(sco, 'status', 2)
385
403
  util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
386
404
  return rc
387
405
 
388
406
 
389
407
  def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> int:
408
+ '''Returns bash/sh return code, checks that a command handling class has all
409
+
410
+ internal CHECK_REQUIRES list items. For example, sim.py has CHECK_REQUIRES=[Tool],
411
+ so if a 'sim' command handler does not also inherit a Tool class, then reports this as an
412
+ error.
413
+ '''
390
414
  sco = command_obj
391
415
  for cls in getattr(sco, 'CHECK_REQUIRES', []):
392
416
  if not isinstance(sco, cls):
@@ -404,7 +428,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
404
428
  # **************************************************************
405
429
  # **** Interrupt Handler
406
430
 
407
- def signal_handler(sig, frame):
431
+ def signal_handler(sig, frame) -> None: # pylint: disable=unused-argument
432
+ '''Handles Ctrl-C, called by main_cli() if running from command line'''
408
433
  util.fancy_stop()
409
434
  util.info('Received Ctrl+C...', start='\nINFO: [EDA] ')
410
435
  util.exit(-1)
@@ -436,6 +461,8 @@ def main(*args):
436
461
 
437
462
  if not util.args['quiet']:
438
463
  util.info(f'eda: version {opencos.__version__}')
464
+ # And show the command that was run (all args):
465
+ util.info(f"main: eda {' '.join(args)}; (run from {os.getcwd()})")
439
466
 
440
467
  # Handle --config-yml= arg
441
468
  config, unparsed = eda_config.get_eda_config(unparsed)
@@ -448,15 +475,20 @@ def main(*args):
448
475
  if len(args) == 0 or (len(args) == 1 and '--debug' in args):
449
476
  # special snowflake case if someone called with a singular arg --debug
450
477
  # (without --help or exit)
451
- util.debug(f"Starting automatic tool setup: init_config()")
478
+ util.debug("Starting automatic tool setup: init_config()")
452
479
  config = init_config(config=config)
453
480
  if not config:
454
481
  util.error(f'eda.py main: problem loading config, {args=}')
455
482
  return 3
456
- return interactive(config=config)
483
+ main_ret = interactive(config=config)
457
484
  else:
458
- return process_tokens(tokens=list(unparsed), original_args=original_args,
459
- config=config)
485
+ main_ret = process_tokens(
486
+ tokens=list(unparsed), original_args=original_args, config=config
487
+ )
488
+ # Stop the util log, needed for pytests that call eda.main directly that otherwise
489
+ # won't close the log file via util's atexist.register(stop_log)
490
+ util.global_log.stop()
491
+ return main_ret
460
492
 
461
493
 
462
494
  def main_cli() -> None:
@@ -470,10 +502,3 @@ def main_cli() -> None:
470
502
 
471
503
  if __name__ == '__main__':
472
504
  main_cli()
473
-
474
- # IDEAS:
475
- # * options with no default (i.e. if user doesn't override, THEN we set it, like "seed" or "work-dir") can be given a
476
- # special type (DefaultVar) versus saying "None" so that help can say more about it (it's a string, it's default val
477
- # is X, etc) and it can be queried as to whether it's really a default val. This avoids having to avoid default vals
478
- # that user can never set (-1, None, etc) which make it hard to infer the type. this same object can be given help
479
- # info and simply "render" to the expected type (str, integer, etc) when used.