opencos-eda 0.2.48__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 (54) 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 +13 -14
  5. opencos/commands/sweep.py +3 -2
  6. opencos/deps/__init__.py +0 -0
  7. opencos/deps/defaults.py +69 -0
  8. opencos/deps/deps_commands.py +419 -0
  9. opencos/deps/deps_file.py +326 -0
  10. opencos/deps/deps_processor.py +670 -0
  11. opencos/deps_schema.py +7 -8
  12. opencos/eda.py +84 -64
  13. opencos/eda_base.py +572 -316
  14. opencos/eda_config.py +80 -14
  15. opencos/eda_extract_targets.py +22 -14
  16. opencos/eda_tool_helper.py +33 -7
  17. opencos/export_helper.py +166 -86
  18. opencos/export_json_convert.py +31 -23
  19. opencos/files.py +2 -1
  20. opencos/hw/__init__.py +0 -0
  21. opencos/{oc_cli.py → hw/oc_cli.py} +9 -4
  22. opencos/names.py +0 -4
  23. opencos/peakrdl_cleanup.py +13 -7
  24. opencos/seed.py +19 -11
  25. opencos/tests/helpers.py +3 -2
  26. opencos/tests/test_deps_helpers.py +35 -32
  27. opencos/tests/test_eda.py +36 -29
  28. opencos/tests/test_eda_elab.py +5 -3
  29. opencos/tests/test_eda_synth.py +1 -1
  30. opencos/tests/test_oc_cli.py +1 -1
  31. opencos/tests/test_tools.py +3 -2
  32. opencos/tools/iverilog.py +2 -2
  33. opencos/tools/modelsim_ase.py +2 -2
  34. opencos/tools/riviera.py +1 -1
  35. opencos/tools/slang.py +1 -1
  36. opencos/tools/surelog.py +1 -1
  37. opencos/tools/verilator.py +1 -1
  38. opencos/tools/vivado.py +1 -1
  39. opencos/tools/yosys.py +4 -3
  40. opencos/util.py +374 -468
  41. opencos/utils/__init__.py +0 -0
  42. opencos/utils/markup_helpers.py +98 -0
  43. opencos/utils/str_helpers.py +111 -0
  44. opencos/utils/subprocess_helpers.py +108 -0
  45. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/METADATA +1 -1
  46. opencos_eda-0.2.49.dist-info/RECORD +88 -0
  47. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/entry_points.txt +1 -1
  48. opencos/deps_helpers.py +0 -1346
  49. opencos_eda-0.2.48.dist-info/RECORD +0 -79
  50. /opencos/{pcie.py → hw/pcie.py} +0 -0
  51. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/WHEEL +0 -0
  52. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE +0 -0
  53. {opencos_eda-0.2.48.dist-info → opencos_eda-0.2.49.dist-info}/licenses/LICENSE.spdx +0 -0
  54. {opencos_eda-0.2.48.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,9 +17,7 @@ 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
23
  # Configure util:
@@ -46,7 +48,7 @@ def init_config(
46
48
  else:
47
49
  config['command_handler'][command] = cls
48
50
 
49
- config['auto_tools_found'] = dict()
51
+ config['auto_tools_found'] = {}
50
52
  config['tools_loaded'] = set()
51
53
  if run_auto_tool_setup:
52
54
  config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
@@ -55,7 +57,7 @@ def init_config(
55
57
 
56
58
 
57
59
 
58
- def usage(tokens: list, config: dict, command=""):
60
+ def usage(tokens: list, config: dict, command="") -> int:
59
61
  '''Returns an int shell return code, given remaining args (tokens list) and eda command.
60
62
 
61
63
  config is the config dict. Used to check valid commands in config['command_handler']
@@ -101,41 +103,44 @@ And <files|targets, ...> is one or more source file or DEPS markup file target,
101
103
  )
102
104
  eda_base.print_base_help()
103
105
  return 0
104
- elif command in config['command_handler'].keys():
106
+
107
+ if command in config['command_handler'].keys():
105
108
  sco = config['command_handler'][command](config=config) # sub command object
106
109
  sco.help(tokens=tokens)
107
110
  return util.exit(0)
108
- else:
109
- util.info(f"Valid commands are: ")
110
- for k in sorted(config['command_handler'].keys()):
111
- util.info(f" {k:20}")
112
- return util.error(f"Cannot provide help, don't understand command: '{command}'")
113
111
 
114
- def interactive(config: dict):
115
- 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
116
124
  while True:
117
- if read_file:
118
- line = f.readline()
119
- if line:
120
- print("%s->%s" % (fname, line), end="")
121
- else:
122
- read_file = False
123
- f.close()
124
- continue
125
- else:
126
- line = input('EDA->')
125
+ line = input('EDA->')
127
126
  m = re.match(r'^([^\#]*)\#.*$', line)
128
- if m: line = m.group(1)
127
+ if m:
128
+ line = m.group(1)
129
129
  tokens = line.split()
130
130
  original_args = tokens.copy()
131
131
  # NOTE: interactive will not correctly handle --config-yml arg (from eda_config.py),
132
132
  # but we should do a best effor to re-parse args from util.py, such as
133
133
  # --quiet, --color, --fancy, --logfile, --debug or --debug-level, etc
134
134
  _, tokens = util.process_tokens(tokens)
135
- 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
136
139
 
137
140
 
138
- 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:
139
144
  '''Returns an updated config, uses config['auto_tools_order'][0] dict, calls tool_setup(..)
140
145
 
141
146
  -- adds items to config['tools_loaded'] set
@@ -151,8 +156,8 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
151
156
  )
152
157
 
153
158
  assert 'auto_tools_order' in config
154
- assert type(config['auto_tools_order']) is list
155
- 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)
156
161
 
157
162
  for name, value in config['auto_tools_order'][0].items():
158
163
  if tool and tool != name:
@@ -160,12 +165,13 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
160
165
 
161
166
  util.debug(f"Checking for ability to run tool: {name}")
162
167
  exe = value.get('exe', str())
163
- if type(exe) is list:
168
+ if isinstance(exe, list):
164
169
  exe_list = exe
165
- elif type(exe) is str:
170
+ elif isinstance(exe, str):
166
171
  exe_list = [exe] # make it a list
167
172
  else:
168
- 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=}')
169
175
  continue
170
176
 
171
177
  has_all_py = True
@@ -201,14 +207,16 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
201
207
  for cmd in requires_cmd_list:
202
208
  cmd_list = shlex.split(cmd)
203
209
  try:
204
- 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')
205
212
  if proc.returncode != 0:
206
213
  if not quiet:
207
- 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=}")
208
216
  has_all_exe = False
209
- except:
217
+ except Exception as e:
210
218
  has_all_exe = False
211
- util.debug(f"... No, exception running {cmd_list}")
219
+ util.debug(f"... No, exception {e} running {cmd_list}")
212
220
 
213
221
 
214
222
  if all([has_all_py, has_all_env, has_all_exe, has_all_in_exe_path]):
@@ -263,12 +271,11 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
263
271
  if warnings:
264
272
  util.warning(f"tool_setup: {auto_setup=} already setup for {tool}?")
265
273
 
266
- entry = config['auto_tools_order'][0].get(tool, dict())
267
- 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', {})
268
276
 
269
277
  for command, str_class_name in tool_cmd_handler_dict.items():
270
278
  current_handler_cls = config['command_handler'].get(command, None)
271
- ext_mod = None
272
279
 
273
280
  if auto_setup and current_handler_cls is not None and issubclass(current_handler_cls, Tool):
274
281
  # If we're not in auto_setup, then always override (aka arg --tool=<this tool>)
@@ -277,19 +284,28 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
277
284
 
278
285
  cls = util.import_class_from_string(str_class_name)
279
286
 
280
- 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'
281
289
  util.debug(f'Setting {cls=} for {command=} in config.command_handler')
282
290
  config['command_handler'][command] = cls
283
291
 
284
292
  config['tools_loaded'].add(tool)
285
293
 
286
294
 
287
- def process_tokens(tokens: list, original_args: list, config: dict, interactive=False):
288
- # this is the top level token processing function. tokens can come from command line, setup file, or interactively.
289
- # we do one pass through all the tokens, triaging them into:
290
- # - those we can execute immediate (help, quit, and global opens like --debug, --color)
291
- # - a command (sim, synth, etc)
292
- # - 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
+ '''
293
309
 
294
310
  deferred_tokens = []
295
311
  command = ""
@@ -329,7 +345,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
329
345
  unparsed.remove(value) # remove command (flist, export, targets, etc)
330
346
  break
331
347
 
332
- if not interactive:
348
+ if not is_interactive:
333
349
  # Run init_config() now, we deferred it in main(), but only run it
334
350
  # for this tool (or tool=None to figure it out)
335
351
  config = init_config(
@@ -337,7 +353,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
337
353
  run_auto_tool_setup=run_auto_tool_setup
338
354
  )
339
355
  if not config:
340
- util.error(f'eda.py main: problem loading config, {args=}')
356
+ util.error(f'eda.py main: problem loading config, {tokens=}')
341
357
  return 3
342
358
 
343
359
  # Deal with help, now that we have the command (if it was set).
@@ -355,9 +371,9 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
355
371
  tool_setup(parsed.tool, config=config)
356
372
 
357
373
  deferred_tokens = unparsed
358
- if command == "":
374
+ if not command:
359
375
  util.error("Didn't get a command!")
360
- return 1
376
+ return 2
361
377
 
362
378
  sco = config['command_handler'][command](config=config) # sub command object
363
379
  util.debug(f'{command=}')
@@ -378,16 +394,23 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
378
394
  # Add the original, nothing-parsed args to the Command.config dict.
379
395
  sco.config['eda_original_args'] = original_args
380
396
 
381
- 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'
382
398
  unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
383
399
 
384
- # query the status from the Command object (0 is pass, > 0 is fail)
385
- 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)
386
403
  util.debug(f'Return from main process_tokens({tokens=}), {rc=}, {type(sco)=}, {unparsed=}')
387
404
  return rc
388
405
 
389
406
 
390
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
+ '''
391
414
  sco = command_obj
392
415
  for cls in getattr(sco, 'CHECK_REQUIRES', []):
393
416
  if not isinstance(sco, cls):
@@ -405,7 +428,8 @@ def check_command_handler_cls(command_obj:object, command:str, parsed_args) -> i
405
428
  # **************************************************************
406
429
  # **** Interrupt Handler
407
430
 
408
- 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'''
409
433
  util.fancy_stop()
410
434
  util.info('Received Ctrl+C...', start='\nINFO: [EDA] ')
411
435
  util.exit(-1)
@@ -437,6 +461,8 @@ def main(*args):
437
461
 
438
462
  if not util.args['quiet']:
439
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()})")
440
466
 
441
467
  # Handle --config-yml= arg
442
468
  config, unparsed = eda_config.get_eda_config(unparsed)
@@ -449,15 +475,16 @@ def main(*args):
449
475
  if len(args) == 0 or (len(args) == 1 and '--debug' in args):
450
476
  # special snowflake case if someone called with a singular arg --debug
451
477
  # (without --help or exit)
452
- util.debug(f"Starting automatic tool setup: init_config()")
478
+ util.debug("Starting automatic tool setup: init_config()")
453
479
  config = init_config(config=config)
454
480
  if not config:
455
481
  util.error(f'eda.py main: problem loading config, {args=}')
456
482
  return 3
457
483
  main_ret = interactive(config=config)
458
484
  else:
459
- main_ret = process_tokens(tokens=list(unparsed), original_args=original_args,
460
- config=config)
485
+ main_ret = process_tokens(
486
+ tokens=list(unparsed), original_args=original_args, config=config
487
+ )
461
488
  # Stop the util log, needed for pytests that call eda.main directly that otherwise
462
489
  # won't close the log file via util's atexist.register(stop_log)
463
490
  util.global_log.stop()
@@ -475,10 +502,3 @@ def main_cli() -> None:
475
502
 
476
503
  if __name__ == '__main__':
477
504
  main_cli()
478
-
479
- # IDEAS:
480
- # * options with no default (i.e. if user doesn't override, THEN we set it, like "seed" or "work-dir") can be given a
481
- # special type (DefaultVar) versus saying "None" so that help can say more about it (it's a string, it's default val
482
- # is X, etc) and it can be queried as to whether it's really a default val. This avoids having to avoid default vals
483
- # that user can never set (-1, None, etc) which make it hard to infer the type. this same object can be given help
484
- # info and simply "render" to the expected type (str, integer, etc) when used.