opencos-eda 0.2.47__tar.gz → 0.2.48__tar.gz

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 (84) hide show
  1. {opencos_eda-0.2.47/opencos_eda.egg-info → opencos_eda-0.2.48}/PKG-INFO +1 -1
  2. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/multi.py +23 -5
  3. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/sweep.py +6 -2
  4. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/waves.py +1 -1
  5. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda.py +9 -4
  6. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_base.py +58 -21
  7. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/helpers.py +24 -12
  8. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_eda.py +11 -12
  9. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/util.py +131 -80
  10. {opencos_eda-0.2.47 → opencos_eda-0.2.48/opencos_eda.egg-info}/PKG-INFO +1 -1
  11. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/pyproject.toml +1 -1
  12. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/LICENSE +0 -0
  13. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/LICENSE.spdx +0 -0
  14. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/README.md +0 -0
  15. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/__init__.py +0 -0
  16. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/_version.py +0 -0
  17. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/_waves_pkg.sv +0 -0
  18. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/__init__.py +0 -0
  19. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/build.py +0 -0
  20. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/elab.py +0 -0
  21. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/export.py +0 -0
  22. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/flist.py +0 -0
  23. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/lec.py +0 -0
  24. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/open.py +0 -0
  25. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/proj.py +0 -0
  26. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/shell.py +0 -0
  27. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/sim.py +0 -0
  28. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/synth.py +0 -0
  29. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/targets.py +0 -0
  30. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/commands/upload.py +0 -0
  31. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/deps_helpers.py +0 -0
  32. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/deps_schema.py +0 -0
  33. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_config.py +0 -0
  34. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_config_defaults.yml +0 -0
  35. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_config_max_verilator_waivers.yml +0 -0
  36. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_config_reduced.yml +0 -0
  37. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_deps_bash_completion.bash +0 -0
  38. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_extract_targets.py +0 -0
  39. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/eda_tool_helper.py +0 -0
  40. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/export_helper.py +0 -0
  41. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/export_json_convert.py +0 -0
  42. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/files.py +0 -0
  43. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/names.py +0 -0
  44. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/oc_cli.py +0 -0
  45. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/pcie.py +0 -0
  46. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/peakrdl_cleanup.py +0 -0
  47. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/seed.py +0 -0
  48. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/__init__.py +0 -0
  49. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/custom_config.yml +0 -0
  50. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/command_order/DEPS.yml +0 -0
  51. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/error_msgs/DEPS.yml +0 -0
  52. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/iverilog_test/DEPS.yml +0 -0
  53. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/no_deps_here/DEPS.yml +0 -0
  54. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/non_sv_reqs/DEPS.yml +0 -0
  55. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/tags_with_tools/DEPS.yml +0 -0
  56. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/deps_files/test_err_fatal/DEPS.yml +0 -0
  57. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_build.py +0 -0
  58. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_deps_helpers.py +0 -0
  59. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_deps_schema.py +0 -0
  60. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_eda_elab.py +0 -0
  61. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_eda_synth.py +0 -0
  62. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_oc_cli.py +0 -0
  63. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tests/test_tools.py +0 -0
  64. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/__init__.py +0 -0
  65. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/invio.py +0 -0
  66. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/invio_helpers.py +0 -0
  67. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/invio_yosys.py +0 -0
  68. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/iverilog.py +0 -0
  69. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/modelsim_ase.py +0 -0
  70. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/questa.py +0 -0
  71. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/riviera.py +0 -0
  72. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/slang.py +0 -0
  73. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/slang_yosys.py +0 -0
  74. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/surelog.py +0 -0
  75. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/tabbycad_yosys.py +0 -0
  76. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/verilator.py +0 -0
  77. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/vivado.py +0 -0
  78. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos/tools/yosys.py +0 -0
  79. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos_eda.egg-info/SOURCES.txt +0 -0
  80. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos_eda.egg-info/dependency_links.txt +0 -0
  81. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos_eda.egg-info/entry_points.txt +0 -0
  82. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos_eda.egg-info/requires.txt +0 -0
  83. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/opencos_eda.egg-info/top_level.txt +0 -0
  84. {opencos_eda-0.2.47 → opencos_eda-0.2.48}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.47
3
+ Version: 0.2.48
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -68,7 +68,7 @@ class CommandMulti(CommandParallel):
68
68
  return command, level
69
69
 
70
70
 
71
- def resolve_path_and_target_patterns(
71
+ def resolve_path_and_target_patterns( # pylint: disable=too-many-locals
72
72
  self, base_path: str, target: str, level: int = -1
73
73
  ) -> dict:
74
74
  '''Returns a dict of: key = matching path, value = set of matched targets.
@@ -91,10 +91,20 @@ class CommandMulti(CommandParallel):
91
91
 
92
92
  matching_targets_dict = {}
93
93
 
94
+ # Let's not glob.glob if the path_pattern and target_pattern are
95
+ # exact, aka if it does not have special characters for glob: * or ?
96
+ # for the target, we also support re, so: * + ?
97
+ if any(x in path_pattern for x in ['*', '?']):
98
+ paths_from_pattern = list(glob.glob(path_pattern, recursive=True))
99
+ else:
100
+ paths_from_pattern = [path_pattern]
101
+
102
+ target_pattern_needs_lookup = any(x in target_pattern for x in ['*', '?', '+'])
103
+
94
104
  # resolve the path_pattern portion using glob.
95
105
  # we'll have to check for DEPS markup files in path_pattern, to match the target_wildcard
96
106
  # using fnmatch or re.
97
- for path in glob.glob(path_pattern, recursive=True):
107
+ for path in paths_from_pattern:
98
108
 
99
109
  if self.path_hidden_or_work_dir(path):
100
110
  continue
@@ -108,7 +118,11 @@ class CommandMulti(CommandParallel):
108
118
  debug(f'in {rel_path=} looking for {target_pattern=} in {deps_targets=}')
109
119
 
110
120
  for t in deps_targets:
111
- if deps_helpers.fnmatch_or_re(pattern=target_pattern, string=t):
121
+ if target_pattern_needs_lookup:
122
+ matched = deps_helpers.fnmatch_or_re(pattern=target_pattern, string=t)
123
+ else:
124
+ matched = t == target_pattern
125
+ if matched:
112
126
  if rel_path not in matching_targets_dict:
113
127
  matching_targets_dict[rel_path] = set()
114
128
  matching_targets_dict[rel_path].add(t)
@@ -392,9 +406,13 @@ class CommandMulti(CommandParallel):
392
406
  # Special case for 'multi' --export-jsonl, run reach child with --export-json
393
407
  command_list.append('--export-json')
394
408
  if tool and len(all_multi_tools) > 1:
395
- command_list.append(f'--job-name={short_target}.{command}.{tool}')
409
+ jobname = f'{short_target}.{command}.{tool}'
396
410
  else:
397
- command_list.append(f'--job-name={short_target}.{command}')
411
+ jobname = f'{short_target}.{command}'
412
+ command_list.append(f'--job-name={jobname}')
413
+ logfile = os.path.join(self.args['eda-dir'], f'eda.{jobname}.log')
414
+ command_list.append(f'--force-logfile={logfile}')
415
+
398
416
 
399
417
  def append_jobs_from_targets(self, args:list):
400
418
  '''Helper method in CommandMulti to apply 'args' (list) to all self.targets,
@@ -206,14 +206,18 @@ class CommandSweep(CommandDesign, CommandParallel):
206
206
  snapshot_name = snapshot_name.replace(os.sep, '_') \
207
207
  + f'.{self.single_command}{sweep_string}'
208
208
  eda_path = get_eda_exec('sweep')
209
+ logfile = os.path.join(self.args['eda-dir'], f'eda.{snapshot_name}.log')
209
210
  self.jobs.append({
210
211
  'name' : snapshot_name,
211
212
  'index' : len(self.jobs),
212
213
  'command': self.single_command,
213
214
  'target': self.sweep_target,
214
215
  'command_list' : (
215
- [eda_path, self.single_command, self.sweep_target,
216
- f'--job-name={snapshot_name}'] + arg_tokens
216
+ [
217
+ eda_path, self.single_command, self.sweep_target,
218
+ f'--job-name={snapshot_name}',
219
+ f'--force-logfile={logfile}',
220
+ ] + arg_tokens
217
221
  )
218
222
  })
219
223
  return
@@ -143,7 +143,7 @@ class CommandWaves(CommandDesign):
143
143
  else:
144
144
  self.error(f"Don't know how to open {wave_file} without GtkWave in PATH")
145
145
  elif wave_file.endswith('.vcd'):
146
- if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gktwave'):
146
+ if 'gtkwave' in self.config['tools_loaded'] and shutil.which('gtkwave'):
147
147
  command_list = ['gtkwave', wave_file]
148
148
  self.exec(os.path.dirname(wave_file), command_list)
149
149
  elif self._vsim_available(from_tools=self.VSIM_VCD_TOOLS):
@@ -18,9 +18,10 @@ from opencos import eda_config
18
18
  from opencos import eda_base
19
19
  from opencos.eda_base import Tool, which_tool
20
20
 
21
- # Globals
22
-
21
+ # Configure util:
23
22
  util.progname = "EDA"
23
+ util.global_log.default_log_enabled = True
24
+ util.global_log.default_log_filepath = os.path.join('eda.work', 'eda.log')
24
25
 
25
26
 
26
27
  # ******************************************************************************
@@ -453,10 +454,14 @@ def main(*args):
453
454
  if not config:
454
455
  util.error(f'eda.py main: problem loading config, {args=}')
455
456
  return 3
456
- return interactive(config=config)
457
+ main_ret = interactive(config=config)
457
458
  else:
458
- return process_tokens(tokens=list(unparsed), original_args=original_args,
459
+ main_ret = process_tokens(tokens=list(unparsed), original_args=original_args,
459
460
  config=config)
461
+ # Stop the util log, needed for pytests that call eda.main directly that otherwise
462
+ # won't close the log file via util's atexist.register(stop_log)
463
+ util.global_log.stop()
464
+ return main_ret
460
465
 
461
466
 
462
467
  def main_cli() -> None:
@@ -212,12 +212,30 @@ class Command:
212
212
  self.target = ""
213
213
  self.target_path = ""
214
214
  self.status = 0
215
+ self.errors_log_f = None
215
216
 
216
- def error(self, *args, **kwargs):
217
- '''Returns None, child classes can call self.error(..) instead of util.error, which updates their self.status.
217
+ def error(self, *args, **kwargs) -> None:
218
+ '''Returns None, child classes can call self.error(..) instead of util.error,
218
219
 
219
- Please consider using Command.error(..) (or self.error(..)) in place of util.error so self.status is updated.
220
+ which updates their self.status. Also will write out to eda.errors.log if the work-dir
221
+ exists. Please consider using Command.error(..) (or self.error(..)) in place of util.error
222
+ so self.status is updated.
220
223
  '''
224
+
225
+ if self.args['work-dir']:
226
+ if not self.errors_log_f:
227
+ try:
228
+ self.errors_log_f = open(
229
+ os.path.join(self.args['work-dir'], 'eda.errors.log'), 'w'
230
+ )
231
+ except:
232
+ pass
233
+ if self.errors_log_f:
234
+ print(
235
+ f'ERROR: [eda] ({self.command_name}) {" ".join(list(args))}',
236
+ file=self.errors_log_f
237
+ )
238
+
221
239
  self.status = util.error(*args, **kwargs)
222
240
 
223
241
  def status_any_error(self, report=True) -> bool:
@@ -1480,6 +1498,9 @@ class CommandParallel(Command):
1480
1498
  jobs_launched += 1
1481
1499
  anything_done = True
1482
1500
  job = self.jobs.pop(0)
1501
+ # TODO(drew): it might be nice to pass more items on 'job' dict, like
1502
+ # logfile or job-name, so CommandSweep or CommandMulti don't have to set
1503
+ # via args. on their command_list.
1483
1504
  if job['name'].startswith(multi_cwd): job['name'] = job['name'][len(multi_cwd):]
1484
1505
  # in all but fancy mode, we will print this text at the launch of a job. It may get a newline below
1485
1506
  job_text = sprint_job_line(jobs_launched, job['name'], hide_stats=run_parallel)
@@ -1663,34 +1684,36 @@ class CommandParallel(Command):
1663
1684
  '''Examines list self.jobs, and if leaf target names are duplicate will
1664
1685
  patch each command's job-name to:
1665
1686
  --job-name=path.leaf.command[.tool]
1687
+
1688
+ Also do for --force-logfile
1666
1689
  '''
1667
1690
 
1668
- def get_job_name(job_dict: dict) -> str:
1669
- '''Fishes the job-name out of an entry in self.jobs'''
1691
+ def get_job_arg(job_dict: dict, arg_name: str) -> str:
1692
+ '''Fishes the arg_name out of an entry in self.jobs'''
1670
1693
  for i, item in enumerate(job_dict['command_list']):
1671
- if item.startswith('--job-name='):
1672
- _, name = item.split('--job-name=')
1694
+ if item.startswith(f'--{arg_name}='):
1695
+ _, name = item.split(f'--{arg_name}=')
1673
1696
  return name
1674
- elif item == '--job-name':
1697
+ elif item == f'--{arg_name}':
1675
1698
  return job_dict['command_list'][i + 1]
1676
1699
  return ''
1677
1700
 
1678
- def replace_job_name(job_dict: dict, new_job_name: str) -> dict:
1679
- '''Replaces the job-name in an entry in self.jobs'''
1701
+ def replace_job_arg(job_dict: dict, arg_name: str, new_value: str) -> bool:
1702
+ '''Replaces the arg_name's value in an entry in self.jobs'''
1680
1703
  for i, item in enumerate(job_dict['command_list']):
1681
- if item.startswith('--job-name='):
1682
- job_dict['command_list'][i] = '--job-name=' + new_job_name
1683
- return job_dict
1684
- elif item == '--job-name':
1685
- job_dict['command_list'][i + 1] = new_job_name
1686
- return job_dict
1687
- return job_dict
1704
+ if item.startswith(f'--{arg_name}='):
1705
+ job_dict['command_list'][i] = f'--{arg_name}=' + new_value
1706
+ return True
1707
+ elif item == f'--{arg_name}':
1708
+ job_dict['command_list'][i + 1] = new_value
1709
+ return True
1710
+ return False
1688
1711
 
1689
1712
 
1690
1713
  job_names_count_dict = {}
1691
1714
  for job_dict in self.jobs:
1692
1715
 
1693
- key = get_job_name(job_dict)
1716
+ key = get_job_arg(job_dict, arg_name='job-name')
1694
1717
  if not key:
1695
1718
  self.error(f'{job_dict=} needs to have a --job-name= arg attached')
1696
1719
  if key not in job_names_count_dict:
@@ -1699,7 +1722,7 @@ class CommandParallel(Command):
1699
1722
  job_names_count_dict[key] += 1
1700
1723
 
1701
1724
  for i, job_dict in enumerate(self.jobs):
1702
- key = get_job_name(job_dict)
1725
+ key = get_job_arg(job_dict, 'job-name')
1703
1726
  if job_names_count_dict[key] < 2:
1704
1727
  continue
1705
1728
 
@@ -1709,6 +1732,20 @@ class CommandParallel(Command):
1709
1732
  patched_sub_work_dir = False
1710
1733
  patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
1711
1734
  new_job_name = f'{patched_target_path}.{key}'
1712
- new_job_dict = replace_job_name(job_dict, new_job_name)
1713
- self.jobs[i] = new_job_dict
1735
+ replace_job_arg(job_dict, arg_name='job-name', new_value=new_job_name)
1736
+
1737
+ # prepend path information to force-logfile (if present):
1738
+ force_logfile = get_job_arg(job_dict, arg_name='force-logfile')
1739
+ if force_logfile:
1740
+ left, right = os.path.split(force_logfile)
1741
+ new_force_logfile = os.path.join(left, f'{patched_target_path}.{right}')
1742
+ replace_job_arg(
1743
+ job_dict, arg_name='force-logfile', new_value=new_force_logfile
1744
+ )
1745
+ util.debug(
1746
+ f'Patched job {job_dict["name"]}: --force-logfile={new_force_logfile}'
1747
+ )
1748
+
1749
+
1750
+ self.jobs[i] = job_dict
1714
1751
  util.debug(f'Patched job {job_dict["name"]}: --job-name={new_job_name}')
@@ -124,20 +124,35 @@ def assert_export_jsonl_good(filepath:str, jsonl:bool=True) -> list:
124
124
  class Helpers:
125
125
  '''We do so much with logging in this file, might as well make it reusable'''
126
126
  DEFAULT_DIR = ''
127
- DEFAULT_LOG = 'eda.log'
127
+ DEFAULT_LOG_DIR = os.getcwd()
128
+ DEFAULT_LOG = os.path.join(DEFAULT_LOG_DIR, '.pytest.eda.log')
128
129
  def chdir(self):
129
130
  '''Changes directory to self.DEFAULT_DIR and removes eda.work, eda.export paths'''
130
131
  chdir_remove_work_dir('', self.DEFAULT_DIR)
131
132
 
133
+ def _resolve_logfile(self, logfile=None) -> str:
134
+ '''Returns the logfile's filepath'''
135
+ ret = logfile
136
+ if ret is None:
137
+ ret = self.DEFAULT_LOG
138
+ else:
139
+ left, right = os.path.split(logfile)
140
+ if not left or left in [os.path.sep, '.', '..']:
141
+ # relative logfile put in DEFAULT_LOG_DIR:
142
+ ret = os.path.join(self.DEFAULT_LOG_DIR, right)
143
+ return ret
144
+
132
145
  def log_it(self, command_str:str, logfile=None, use_eda_wrap=True) -> int:
133
146
  '''Replacement for calling eda.main or eda_wrap, when you want an internal logfile
134
147
 
135
148
  Usage:
136
- rc = self.log_it('sim foo', logfile='yes.log')
149
+ rc = self.log_it('sim foo')
137
150
  assert rc == 0
151
+
152
+ Note this will run with --no-default-log to avoid a Windows problem with stomping
153
+ on a log file.
138
154
  '''
139
- if logfile is None:
140
- logfile = self.DEFAULT_LOG
155
+ logfile = self._resolve_logfile(logfile)
141
156
  rc = 50
142
157
 
143
158
  # TODO(drew): There are some issues with log_it redirecting stdout from vivado
@@ -148,16 +163,15 @@ class Helpers:
148
163
  with open(logfile, 'w', encoding='utf-8') as f:
149
164
  with redirect_stdout(f), redirect_stderr(f):
150
165
  if use_eda_wrap:
151
- rc = eda_wrap(*(command_str.split()))
166
+ rc = eda_wrap('--no-default-log', *(command_str.split()))
152
167
  else:
153
- rc = eda.main(*(command_str.split()))
168
+ rc = eda.main('--no-default-log', *(command_str.split()))
154
169
  print(f'Wrote: {os.path.abspath(logfile)=}')
155
170
  return rc
156
171
 
157
172
  def is_in_log(self, *want_str, logfile=None, windows_path_support=False):
158
173
  '''Check if any of want_str args are in the logfile, or self.DEFAULT_LOG'''
159
- if logfile is None:
160
- logfile = self.DEFAULT_LOG
174
+ logfile = self._resolve_logfile(logfile)
161
175
  want_str0 = ' '.join(list(want_str))
162
176
  want_str1 = want_str0.replace('/', '\\')
163
177
  with open(logfile, encoding='utf-8') as f:
@@ -169,8 +183,7 @@ class Helpers:
169
183
 
170
184
  def get_log_lines_with(self, *want_str, logfile=None, windows_path_support=False):
171
185
  '''gets all log lines with any of want_str args are in the logfile, or self.DEFAULT_LOG'''
172
- if logfile is None:
173
- logfile = self.DEFAULT_LOG
186
+ logfile = self._resolve_logfile(logfile)
174
187
  ret_list = []
175
188
  want_str0 = ' '.join(list(want_str))
176
189
  want_str1 = want_str0.replace('/', '\\')
@@ -186,8 +199,7 @@ class Helpers:
186
199
  '''gets all log lines with any of *want_str within a single word
187
200
  in the logfile or self.DEFAULT_LOG
188
201
  '''
189
- if logfile is None:
190
- logfile = self.DEFAULT_LOG
202
+ logfile = self._resolve_logfile(logfile)
191
203
  ret_list = []
192
204
  want_str0 = ' '.join(list(want_str))
193
205
  want_str1 = want_str0.replace('/', '\\')
@@ -422,7 +422,6 @@ class TestsRequiresVerilator( # pylint: disable=too-many-public-methods
422
422
  class TestMissingDepsFileErrorMessages(Helpers):
423
423
  '''Test for missing DEPS.yml file, using 'eda export' to avoid tools.'''
424
424
  DEFAULT_DIR = os.path.join(THISPATH, 'deps_files', 'no_deps_here', 'empty')
425
- DEFAULT_LOG = 'eda.log'
426
425
 
427
426
  def test_bad0(self):
428
427
  '''Looks for target_bad0, but there is no DEPS file in .'''
@@ -444,7 +443,6 @@ class TestDepsResolveErrorMessages(Helpers):
444
443
  linenumber information is printed when available.'''
445
444
 
446
445
  DEFAULT_DIR = os.path.join(THISPATH, 'deps_files', 'error_msgs')
447
- DEFAULT_LOG = 'eda.log'
448
446
 
449
447
  # files foo.sv, foo2.sv, target_bad0.sv, and target_bad1.sv exist.
450
448
  # files missing*.sv and targets missing* do not exist.
@@ -741,13 +739,15 @@ def test_deps_command_order(command):
741
739
  work_dir = os.path.join(
742
740
  THISPATH, 'deps_files', 'command_order', 'eda.work', f'target_test.{command}'
743
741
  )
742
+ # Note that eda will write out the returncode INFO line to tee'd log files, so
743
+ # there is more in the log file than "hi" or "bye".
744
744
  with open(os.path.join(work_dir, 'target_echo_hi__shell_0.log'), encoding='utf-8') as f:
745
- text = ''.join(f.readlines()).strip()
746
- assert text in ['hi', '"hi"', '\\"hi\\"']
745
+ text = ' '.join(f.readlines()).strip()
746
+ assert any(text.startswith(x) for x in ['hi', '"hi"', '\\"hi\\"'])
747
747
  # Added check, one of the targets uses a custom 'tee' file name, instead of the default log.
748
748
  with open(os.path.join(work_dir, 'custom_tee_echo_bye.log'), encoding='utf-8') as f:
749
749
  text = ''.join(f.readlines()).strip()
750
- assert text in ['bye', '"bye"', '\\"bye\\"']
750
+ assert any(text.startswith(x) for x in ['bye', '"bye"', '\\"bye\\"'])
751
751
 
752
752
 
753
753
  @pytest.mark.skipif('verilator' not in tools_loaded, reason="requires verilator")
@@ -833,14 +833,13 @@ class TestDepsNoFilesTargets(Helpers):
833
833
  class TestDepsTags(Helpers):
834
834
  '''Series of tests for DEPS - target - tags, in ./deps_files/tags_with_tools'''
835
835
  DEFAULT_DIR = os.path.join(THISPATH, 'deps_files', 'tags_with_tools')
836
- DEFAULT_LOG = 'eda.log'
837
836
 
838
837
  @pytest.mark.skipif('verilator' not in tools_loaded, reason="requires verilator")
839
838
  def test_tags_with_tools_verilator(self):
840
839
  '''test for DEPS target that hits with-tools: verilator, so that
841
840
  additional args are applied from the DEPS tag.'''
842
841
  self.chdir()
843
- logfile = 'verilator_eda.log'
842
+ logfile = '.pytest.verilator_eda.log'
844
843
  rc = self.log_it('sim --tool verilator target_test', logfile=logfile)
845
844
  assert rc == 0
846
845
 
@@ -859,7 +858,7 @@ class TestDepsTags(Helpers):
859
858
  AKA, lets you replace all the Verilator waivers to the DEPS target that only affect
860
859
  with-tools: verilator'''
861
860
  self.chdir()
862
- logfile = 'target_with_replace_config_tools_test.log'
861
+ logfile = '.pytest.target_with_replace_config_tools_test.log'
863
862
  rc = self.log_it('sim --tool verilator target_with_replace_config_tools_test',
864
863
  logfile=logfile)
865
864
  assert rc == 0
@@ -877,7 +876,7 @@ class TestDepsTags(Helpers):
877
876
  AKA, lets you add Verilator waivers to the DEPS target that only affect
878
877
  with-tools: verilator'''
879
878
  self.chdir()
880
- logfile = 'target_with_additive_config_tools_test.log'
879
+ logfile = '.pytest.target_with_additive_config_tools_test.log'
881
880
  rc = self.log_it('sim --tool verilator --debug target_with_additive_config_tools_test',
882
881
  logfile=logfile)
883
882
  assert rc == 0
@@ -897,7 +896,7 @@ class TestDepsTags(Helpers):
897
896
  apply the arg --lint-only in the DEPS tag, and instead run the
898
897
  full simulation.'''
899
898
  self.chdir()
900
- logfile = 'vivado_eda.log'
899
+ logfile = '.pytest.vivado_eda.log'
901
900
  rc = self.log_it('sim --tool vivado target_test', logfile=logfile)
902
901
  assert rc == 0
903
902
 
@@ -916,7 +915,7 @@ class TestDepsTags(Helpers):
916
915
  def test_tags_with_tools_add_incdirs(self):
917
916
  '''test for DEPS target with tag that adds incdirs'''
918
917
  self.chdir()
919
- logfile = 'target_foo_sv_add_incdirs.log'
918
+ logfile = '.pytest.target_foo_sv_add_incdirs.log'
920
919
  rc = self.log_it('elab --tool verilator target_foo_sv_add_incdirs',
921
920
  logfile=logfile)
922
921
  assert rc == 0
@@ -931,7 +930,7 @@ class TestDepsTags(Helpers):
931
930
  def test_tags_with_tools_add_defines(self):
932
931
  '''test for DEPS target with tag that adds defines'''
933
932
  self.chdir()
934
- logfile = 'target_foo_sv_add_defines.log'
933
+ logfile = '.pytest.target_foo_sv_add_defines.log'
935
934
  rc = self.log_it('elab --tool verilator --debug target_foo_sv_add_defines',
936
935
  logfile=logfile)
937
936
  assert rc == 0
@@ -19,10 +19,85 @@ from opencos import eda_config
19
19
  global_exit_allowed = False
20
20
  progname = "UNKNOWN"
21
21
  progname_in_message = True
22
- logfile = None
23
- loglast = 0
24
22
  debug_level = 0
25
23
 
24
+ class UtilLogger:
25
+ file = None
26
+ filepath = ''
27
+ time_last = 0 #timestamp via time.time()
28
+
29
+ # disabled by default, eda.py enables it. Can also be disabled via
30
+ # util's argparser: --no-default-log, --logfile=<name>, or --force-logfile=<name>
31
+ default_log_enabled = False
32
+ default_log_filepath = os.path.join('eda.work', 'eda.log')
33
+
34
+ def clear(self) -> None:
35
+ self.file = None
36
+ self.filepath = ''
37
+ self.time_last = 0
38
+
39
+ def stop(self) -> None:
40
+ if self.file:
41
+ self.write_timestamp(f'stop - {self.filepath}')
42
+ info(f"Closing logfile: {self.filepath}")
43
+ self.file.close()
44
+ self.clear()
45
+
46
+ def start(self, filename: str, force: bool = False) -> None:
47
+ if not filename:
48
+ error(f'Trying to start a logfile, but filename is missing')
49
+ return
50
+ if os.path.exists(filename):
51
+ if force:
52
+ debug(f"Overwriting logfile '{filename}', which exists, due to --force-logfile.")
53
+ else:
54
+ error(f"The --logfile path '{filename}' exists. Use --force-logfile",
55
+ "(vs --logfile) to override.")
56
+ return
57
+ else:
58
+ safe_mkdir_for_file(filename)
59
+ try:
60
+ self.file = open(filename, 'w')
61
+ debug(f"Opened logfile '{filename}' for writing")
62
+ self.filepath = filename
63
+ self.write_timestamp(f'start - {self.filepath}')
64
+ except Exception as e:
65
+ error(f"Error opening '{filename}' for writing, {e}")
66
+ self.clear()
67
+
68
+ def write_timestamp(self, text: str = "") -> None:
69
+ dt = datetime.datetime.now().ctime()
70
+ print(f"INFO: [{progname}] Time: {dt} {text}", file=self.file)
71
+ self.time_last = time.time()
72
+
73
+ def write(self, text: str, end: str) -> None:
74
+ sw = text.startswith(f"INFO: [{progname}]")
75
+ if (((time.time() - self.time_last) > 10) and
76
+ (text.startswith(f"DEBUG: [{progname}]") or
77
+ text.startswith(f"INFO: [{progname}]") or
78
+ text.startswith(f"WARNING: [{progname}]") or
79
+ text.startswith(f"ERROR: [{progname}]"))):
80
+ self.write_timestamp()
81
+ print(text, end=end, file=self.file)
82
+ self.file.flush()
83
+ os.fsync(self.file)
84
+
85
+
86
+ global_log = UtilLogger()
87
+
88
+
89
+ def start_log(filename, force=False):
90
+ global_log.start(filename=filename, force=force)
91
+
92
+ def write_log(text, end):
93
+ global_log.write(text=text, end=end)
94
+
95
+ def stop_log():
96
+ global_log.stop()
97
+
98
+ atexit.register(stop_log)
99
+
100
+
26
101
  EDA_OUTPUT_CONFIG_FNAME = 'eda_output_config.yml'
27
102
 
28
103
  args = {
@@ -137,45 +212,6 @@ def yaml_safe_writer(data:dict, filepath:str) -> None:
137
212
  warning(f'{filepath=} to be written for this extension not implemented.')
138
213
 
139
214
 
140
-
141
- def start_log(filename, force=False):
142
- global logfile, loglast
143
- if os.path.exists(filename):
144
- if force:
145
- info(f"Overwriting '{filename}', which exists, due to --force-logfile.")
146
- else:
147
- error(f"The --logfile path '{filename}' exists. Use --force-logfile (vs --logfile) to override.")
148
- try:
149
- logfile = open( filename, 'w')
150
- debug(f"Opened logfile '{filename}' for writing")
151
- except Exception as e:
152
- error(f"Error opening '{filename}' for writing!")
153
-
154
- def write_log(text, end):
155
- global logfile, loglast
156
- sw = text.startswith(f"INFO: [{progname}]")
157
- if (((time.time() - loglast) > 10) and
158
- (text.startswith(f"DEBUG: [{progname}]") or
159
- text.startswith(f"INFO: [{progname}]") or
160
- text.startswith(f"WARNING: [{progname}]") or
161
- text.startswith(f"ERROR: [{progname}]"))):
162
- dt = datetime.datetime.now().ctime()
163
- print(f"INFO: [{progname}] Time: {dt}", file=logfile)
164
- loglast = time.time()
165
- print(text, end=end, file=logfile)
166
- logfile.flush()
167
- os.fsync(logfile)
168
-
169
- def stop_log():
170
- global logfile, loglast
171
- if logfile:
172
- debug(f"Closing logfile")
173
- logfile.close()
174
- logfile = None
175
- loglast = 0
176
-
177
- atexit.register(stop_log)
178
-
179
215
  def get_argparse_bool_action_kwargs() -> dict:
180
216
  bool_kwargs = dict()
181
217
  x = getattr(argparse, 'BooleanOptionalAction', None)
@@ -205,12 +241,19 @@ def get_argparser() -> argparse.ArgumentParser:
205
241
  help='Display additional debug messaging level 1 or higher')
206
242
  parser.add_argument('--debug-level', type=int, default=0,
207
243
  help='Set debug level messaging (default: 0)')
208
- parser.add_argument('--logfile', type=str, default='',
209
- help='Write eda messaging to logfile (default disabled)')
210
- parser.add_argument('--force-logfile', type=str, default='',
244
+ parser.add_argument('--logfile', type=str, default=None,
245
+ help=('Write eda messaging to safe logfile that will not be overwritten'
246
+ ' (default disabled)'))
247
+ parser.add_argument('--force-logfile', type=str, default=None,
211
248
  help='Set to force overwrite the logfile')
249
+ parser.add_argument('--default-log', **bool_action_kwargs,
250
+ default=global_log.default_log_enabled,
251
+ help=('Enable/Disable default logging to'
252
+ f' {global_log.default_log_filepath}. Default logging is disabled'
253
+ ' if --logfile or --force-logfile is set'))
212
254
  parser.add_argument('--no-respawn', action='store_true',
213
- help='Legacy mode (default respawn disabled) for respawning eda.py using $OC_ROOT/bin')
255
+ help=('Legacy mode (default respawn disabled) for respawning eda.py'
256
+ ' using $OC_ROOT/bin'))
214
257
  return parser
215
258
 
216
259
  def get_argparser_short_help(parser=None) -> str:
@@ -251,10 +294,15 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
251
294
 
252
295
  debug(f'util.process_tokens: {parsed=} {unparsed=} from {tokens=}')
253
296
 
254
- if parsed.force_logfile != '':
297
+ if parsed.force_logfile:
255
298
  start_log(parsed.force_logfile, force=True)
256
- elif parsed.logfile != '':
299
+ elif parsed.logfile:
257
300
  start_log(parsed.logfile, force=False)
301
+ elif parsed.default_log and \
302
+ (parsed.force_logfile is None and parsed.logfile is None):
303
+ # Use a forced logfile in the eda.work/eda.log:
304
+ start_log(global_log.default_log_filepath, force=True)
305
+
258
306
 
259
307
  parsed_as_dict = vars(parsed)
260
308
  for key,value in parsed_as_dict.items():
@@ -339,7 +387,7 @@ def print_post(text, end):
339
387
  print(fancy_lines_[x],flush=True,end=('' if x==(len(fancy_lines_)-1) else '\n'))
340
388
  #time.sleep(1)
341
389
  print("\033[1G", end="", flush=True)
342
- if logfile: write_log(text, end=end)
390
+ if global_log.file: write_log(text, end=end)
343
391
 
344
392
  string_red = f"\x1B[31m"
345
393
  string_green = f"\x1B[32m"
@@ -520,6 +568,11 @@ def safe_rmdir(path):
520
568
  error(f"An error occurred while removing the directory '{path}': {e}")
521
569
 
522
570
  def safe_mkdir(path : str):
571
+ if os.path.exists(path):
572
+ return
573
+ left, right = os.path.split(os.path.relpath(path))
574
+ if left and left not in ['.', '..', os.path.sep]:
575
+ safe_mkdir(left)
523
576
  try:
524
577
  os.mkdir(path)
525
578
  except FileExistsError:
@@ -534,6 +587,11 @@ def safe_mkdirs(base : str, new_dirs : list):
534
587
  for p in new_dirs:
535
588
  safe_mkdir( os.path.join(base, p) )
536
589
 
590
+ def safe_mkdir_for_file(filepath: str):
591
+ left, right = os.path.split(filepath)
592
+ if left:
593
+ safe_mkdir(left)
594
+
537
595
 
538
596
  def import_class_from_string(full_class_name):
539
597
  """
@@ -707,9 +765,8 @@ def subprocess_run_background(work_dir, command_list, background=True, fake:bool
707
765
 
708
766
  debug(f'util.subprocess_run_background: {background=} {tee_fpath=} {shell=}')
709
767
 
710
- if fake or (not background and not tee_fpath):
711
- # If tee_fpath is set, we're going to "background" but with it also
712
- # printed out (stdout and tee_fpath).
768
+ if fake:
769
+ # let subprocess_run handle it (won't run anything)
713
770
  rc = subprocess_run(work_dir, command_list, fake=fake, shell=shell)
714
771
  return '', '', rc
715
772
 
@@ -734,38 +791,32 @@ def subprocess_run_background(work_dir, command_list, background=True, fake:bool
734
791
  debug(f"util.subprocess_run_background: about to call subprocess.Popen({c}, **{proc_kwargs})")
735
792
  proc = subprocess.Popen(c, **proc_kwargs)
736
793
 
794
+ stdout = ''
795
+ stderr = ''
796
+ tee_fpath_f = None
737
797
  if tee_fpath:
738
- stdout = ''
739
- stderr = ''
740
- with open(tee_fpath, 'w') as f:
741
- for line in iter(proc.stdout.readline, b''):
742
- line = line.rstrip().decode("utf-8", errors="replace")
743
- if not background:
744
- print(line)
745
- f.write(line + '\n')
746
- stdout += line + '\n'
747
-
748
- proc.communicate()
749
- rc = proc.returncode
798
+ try:
799
+ tee_fpath_f = open(tee_fpath, 'w')
800
+ except Exception as e:
801
+ error(f'Unable to open file "{tee_fpath}" for writing, {e}')
802
+
803
+ for line in iter(proc.stdout.readline, b''):
804
+ line = line.rstrip().decode("utf-8", errors="replace")
805
+ if not background:
806
+ print(line)
807
+ if tee_fpath_f:
808
+ tee_fpath_f.write(line + '\n')
809
+ if global_log.file:
810
+ global_log.write(line, '\n')
811
+ stdout += line + '\n'
812
+
813
+ proc.communicate()
814
+ rc = proc.returncode
815
+ if tee_fpath_f:
816
+ tee_fpath_f.write(f'INFO: [{progname}] util.subprocess_run_background: returncode={rc}\n')
817
+ tee_fpath_f.close()
750
818
  info('util.subprocess_run_background: wrote: ' + os.path.abspath(tee_fpath))
751
819
 
752
-
753
- else:
754
-
755
- debug(f"util.subprocess_run_background: about to call proc.communicate()")
756
- stdout, stderr = proc.communicate()
757
- rc = proc.returncode
758
-
759
- stdout = stdout.decode('utf-8', errors="replace") if stdout else ""
760
- stderr = stderr.decode('utf-8', errors="replace") if stderr else ""
761
- debug(f"shell_run_background: {rc=}")
762
- if stdout:
763
- for lineno, line in enumerate(stdout.strip().split('\n')):
764
- debug(f"stdout:{lineno+1}: {line}")
765
- if stderr:
766
- for lineno, line in enumerate(stdout.strip().split('\n')):
767
- debug(f"stderr:{lineno+1}: {line}")
768
-
769
820
  return stdout, stderr, rc
770
821
 
771
822
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.47
3
+ Version: 0.2.48
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -2,7 +2,7 @@
2
2
 
3
3
  [project]
4
4
  name = "opencos-eda"
5
- version = "0.2.47"
5
+ version = "0.2.48"
6
6
  dependencies = [
7
7
  # opencos/eda.py dependencies
8
8
  "mergedeep >= 1.3.4",
File without changes
File without changes
File without changes
File without changes