opencos-eda 0.2.46__py3-none-any.whl → 0.2.48__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.
opencos/commands/multi.py CHANGED
@@ -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,
opencos/commands/sweep.py CHANGED
@@ -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
opencos/commands/waves.py CHANGED
@@ -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):
opencos/deps_helpers.py CHANGED
@@ -68,7 +68,9 @@ class Defaults:
68
68
  'shell',
69
69
  'work-dir-add-srcs', 'work-dir-add-sources',
70
70
  'peakrdl',
71
- 'run-from-work-dir',
71
+ 'run-from-work-dir', # default True
72
+ 'filepath-subst-target-dir', # default True
73
+ 'dirpath-subst-target-dir', # default False
72
74
  'var-subst-args',
73
75
  'var-subst-os-env',
74
76
  'tee',
@@ -384,7 +386,13 @@ def deps_list_target_sanitize(entry, default_key:str='deps', target_node:str='',
384
386
  assert False, f"Can't convert to list {entry=} {default_key=} {target_node=} {deps_file=}"
385
387
 
386
388
 
387
- def path_substitutions_relative_to_work_dir(exec_list : list, info_str : str, target_path : str):
389
+ def path_substitutions_relative_to_work_dir(
390
+ exec_list: list, info_str: str, target_path: str,
391
+ enable_filepath_subst: bool, enable_dirpath_subst: bool
392
+ ) -> list:
393
+
394
+ if not enable_filepath_subst and not enable_dirpath_subst:
395
+ return exec_list
388
396
 
389
397
  # Look for path substitutions, b/c we later "work" in self.args['work-dir'], but
390
398
  # files should be relative to our target_path.
@@ -398,10 +406,18 @@ def path_substitutions_relative_to_work_dir(exec_list : list, info_str : str, ta
398
406
  # need this, and we can't assume dir levels in the work-dir.
399
407
  try:
400
408
  try_path = os.path.abspath(os.path.join(os.path.abspath(target_path), m.group(1)))
401
- if os.path.isfile(try_path) or os.path.isdir(try_path):
409
+ if enable_filepath_subst and os.path.isfile(try_path):
410
+ # make the substitution
411
+ exec_list[i] = word.replace(m.group(1), try_path)
412
+ debug(f'file path substitution {info_str=} {target_path=}: replaced - {word=}'
413
+ f'is now ={exec_list[i]}. This can be disabled in DEPS with:',
414
+ '"filepath-subst-targetdir: false"')
415
+ elif enable_dirpath_subst and os.path.isdir(try_path):
402
416
  # make the substitution
403
417
  exec_list[i] = word.replace(m.group(1), try_path)
404
- debug(f'path substitution {info_str=} {target_path=}: replaced - {word=} is now ={exec_list[i]}')
418
+ debug(f'dir path substitution {info_str=} {target_path=}: replaced - {word=}'
419
+ f'is now ={exec_list[i]}. This can be disabled in DEPS with:',
420
+ '"dirpath-subst-targetdir: false"')
405
421
  except:
406
422
  pass
407
423
 
@@ -1034,7 +1050,10 @@ class DepsProcessor:
1034
1050
 
1035
1051
 
1036
1052
 
1037
- def parse_deps_shell_str(line : str, target_path : str, target_node : str, enable : bool = True):
1053
+ def parse_deps_shell_str(line: str, target_path: str, target_node: str,
1054
+ enable_filepath_subst_target_dir: bool = True,
1055
+ enable_dirpath_subst_target_dir: bool = False,
1056
+ enable: bool = True) -> dict:
1038
1057
  '''Returns None or a dict of a possible shell command from line (str)
1039
1058
 
1040
1059
  Examples of 'line' str:
@@ -1050,25 +1069,30 @@ def parse_deps_shell_str(line : str, target_path : str, target_node : str, enabl
1050
1069
  target_node (str) -- from dependency parsing, the target containing this 'line' str.
1051
1070
  '''
1052
1071
  if not enable:
1053
- return None
1072
+ return {}
1054
1073
 
1055
1074
  m = re.match(r'^\s*shell\@(.*)\s*$', line)
1056
1075
  if not m:
1057
- return None
1076
+ return {}
1058
1077
 
1059
1078
  exec_str = m.group(1)
1060
1079
  exec_list = exec_str.split()
1061
1080
 
1062
1081
  # Look for path substitutions, b/c we later "work" in self.args['work-dir'], but
1063
1082
  # files should be relative to our target_path.
1064
- exec_list = path_substitutions_relative_to_work_dir(exec_list=exec_list, info_str='shell@', target_path=target_path)
1083
+ # Note this can be disable in DEPS via path-subst-target-dir=False
1084
+ exec_list = path_substitutions_relative_to_work_dir(
1085
+ exec_list=exec_list, info_str='shell@', target_path=target_path,
1086
+ enable_filepath_subst=enable_filepath_subst_target_dir,
1087
+ enable_dirpath_subst=enable_dirpath_subst_target_dir
1088
+ )
1065
1089
 
1066
- d = {'target_path': os.path.abspath(target_path),
1067
- 'target_node': target_node,
1068
- 'run_from_work_dir': True,
1069
- 'exec_list': exec_list,
1070
- }
1071
- return d
1090
+ return {
1091
+ 'target_path': os.path.abspath(target_path),
1092
+ 'target_node': target_node,
1093
+ 'run_from_work_dir': True, # may be overriden later.
1094
+ 'exec_list': exec_list,
1095
+ }
1072
1096
 
1073
1097
 
1074
1098
  def parse_deps_work_dir_add_srcs(line : str, target_path : str, target_node : str, enable : bool = True):
@@ -1100,8 +1124,12 @@ def parse_deps_work_dir_add_srcs(line : str, target_path : str, target_node : st
1100
1124
  return d
1101
1125
 
1102
1126
 
1103
- def parse_deps_peakrdl(line : str, target_path : str, target_node : str, enable : bool = True,
1104
- tool : str = ''):
1127
+ def parse_deps_peakrdl(
1128
+ line: str, target_path: str, target_node: str, enable: bool = True,
1129
+ enable_filepath_subst_target_dir: bool = True,
1130
+ enable_dirpath_subst_target_dir: bool = False,
1131
+ tool: str = ''
1132
+ ) -> dict:
1105
1133
  '''Returns None or a dict describing a PeakRDL CSR register generator dependency
1106
1134
 
1107
1135
  Examples of 'line' str:
@@ -1172,10 +1200,13 @@ def parse_deps_peakrdl(line : str, target_path : str, target_node : str, enable
1172
1200
  # Make these look like a dep_shell_command:
1173
1201
  for one_cmd_as_list in shell_commands:
1174
1202
  ret_dict['shell_commands_list'].append(
1175
- parse_deps_shell_str(line = ' shell@ ' + ' '.join(one_cmd_as_list),
1176
- target_path = target_path,
1177
- target_node = target_node
1178
- )
1203
+ parse_deps_shell_str(
1204
+ line=(' shell@ ' + ' '.join(one_cmd_as_list)),
1205
+ target_path=target_path,
1206
+ target_node=target_node,
1207
+ enable_filepath_subst_target_dir=enable_filepath_subst_target_dir,
1208
+ enable_dirpath_subst_target_dir=enable_dirpath_subst_target_dir
1209
+ )
1179
1210
  )
1180
1211
 
1181
1212
  # Make the work_dir_add_srcs dict:
@@ -1237,14 +1268,22 @@ def deps_commands_handler(config: dict, eda_args: dict,
1237
1268
  var_subst_dict = eda_args
1238
1269
 
1239
1270
  tee_fpath = command.get('tee', None)
1240
- run_from_work_dir = command.get('run-from-work-dir', True) # for shell, default True
1271
+
1272
+ # These are both optional bools, default True, would have to explicitly be set to False
1273
+ # to take effect:
1274
+ run_from_work_dir = command.get('run-from-work-dir', True)
1275
+ filepath_subst_target_dir = command.get('filepath-subst-target-dir', True)
1276
+ dirpath_subst_target_dir = command.get('dirpath-subst-target-dir', False)
1241
1277
 
1242
1278
  for key,item in command.items():
1243
1279
 
1244
- # skip the var-subst-* keys, since these types are bools
1245
- if key.startswith('var-subst') or \
1246
- key.startswith('tee') or \
1247
- key.startswith('run-from-work-dir'):
1280
+ # skip the tee and var-subst-* keys, since these types are bools and not commands.
1281
+ if key in ['tee',
1282
+ 'var-subst-os-env',
1283
+ 'var-subst-args',
1284
+ 'run-from-work-dir',
1285
+ 'filepath-subst-target-dir',
1286
+ 'dirpath-subst-target-dir']:
1248
1287
  continue
1249
1288
 
1250
1289
  # Optional variable substituion in commands
@@ -1255,9 +1294,11 @@ def deps_commands_handler(config: dict, eda_args: dict,
1255
1294
  if key == 'shell':
1256
1295
  # For now, piggyback on parse_deps_shell_str:
1257
1296
  ret_dict = parse_deps_shell_str(
1258
- line = 'shell@ ' + item,
1259
- target_path = target_path,
1260
- target_node = target_node,
1297
+ line=('shell@ ' + item),
1298
+ target_path=target_path,
1299
+ target_node=target_node,
1300
+ enable_filepath_subst_target_dir=filepath_subst_target_dir,
1301
+ enable_dirpath_subst_target_dir=dirpath_subst_target_dir,
1261
1302
  enable=config['dep_command_enables']['shell'],
1262
1303
  )
1263
1304
  # To support 'tee: <some-file>' need to append it to last
@@ -1286,6 +1327,8 @@ def deps_commands_handler(config: dict, eda_args: dict,
1286
1327
  line = 'peakrdl@ ' + item,
1287
1328
  target_path = target_path,
1288
1329
  target_node = target_node,
1330
+ enable_filepath_subst_target_dir=filepath_subst_target_dir,
1331
+ enable_dirpath_subst_target_dir=dirpath_subst_target_dir,
1289
1332
  enable=config['dep_command_enables']['peakrdl'],
1290
1333
  tool=eda_args.get('tool', '')
1291
1334
  )
opencos/deps_schema.py CHANGED
@@ -71,6 +71,12 @@ my_target_name:
71
71
  var-subst-os-env: <---- bool, perform var substitution using os.environ
72
72
  run-from-work-dir: <---- bool, default True, if False runs from target dir
73
73
  instead of work-dir.
74
+ filepath-subst-target-dir: <---- bool, default True, if False does not perform
75
+ file path substitution relative to target dir
76
+ (if substituted file exists).
77
+ dirpath-subst-target-dir: <---- bool, default False, if True performs
78
+ directory path substitution relative to target dir
79
+ (if substituted directory exists).
74
80
  tee: <---- string, filename to write logs to
75
81
  - work-dir-add-sources: <---- work-dir-add-sources, optional list (or string)
76
82
  - some_file_gen_from_sh.sv <---- string filename that we created with sh command
@@ -153,6 +159,8 @@ DEPS_COMMANDS_LIST = [
153
159
  Optional('var-subst-args'): bool,
154
160
  Optional('var-subst-os-env'): bool,
155
161
  Optional('run-from-work-dir'): bool,
162
+ Optional('filepath-subst-target-dir'): bool,
163
+ Optional('dirpath-subst-target-dir'): bool,
156
164
  Optional('tee'): Or(str, type(None)),
157
165
  },
158
166
  {
opencos/eda.py CHANGED
@@ -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:
opencos/eda_base.py CHANGED
@@ -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}')
opencos/tests/helpers.py CHANGED
@@ -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('/', '\\')
@@ -14,7 +14,7 @@ from opencos import eda_tool_helper, deps_helpers
14
14
 
15
15
  THISPATH = os.path.dirname(__file__)
16
16
 
17
- # Figure out what tools the system has available, without calling eda.main(..)
17
+ # Figure out what tools the system has avail, without calling eda.main(..)
18
18
  config, tools_loaded = eda_tool_helper.get_config_and_tools_loaded()
19
19
 
20
20
 
@@ -58,63 +58,100 @@ def test_get_all_targets_eda_multi():
58
58
  def test_parse_deps_shell_str__no_parse():
59
59
  line = 'some_file.sv'
60
60
  d = deps_helpers.parse_deps_shell_str(line, '', '')
61
- assert d is None, f'{d=}'
61
+ assert not d, f'{d=}'
62
62
 
63
63
  line = 'some_target:'
64
64
  d = deps_helpers.parse_deps_shell_str(line, '', '')
65
- assert d is None, f'{d=}'
65
+ assert not d, f'{d=}'
66
66
 
67
67
  line = ' csr@some_file.sv'
68
68
  d = deps_helpers.parse_deps_shell_str(line, '', '')
69
- assert d is None, f'{d=}'
69
+ assert not d, f'{d=}'
70
70
 
71
71
  def test_parse_deps_shell_str__cp():
72
72
  line = ' shell@ cp ./oclib_fifo_test.sv oclib_fifo_test_COPY.sv ;'
73
73
  d = deps_helpers.parse_deps_shell_str(line, '', '')
74
- assert d is not None, f'{d=}'
74
+ assert d, f'{d=}'
75
75
  assert d['exec_list'] == ['cp', './oclib_fifo_test.sv', 'oclib_fifo_test_COPY.sv', ';'], f'{d=}'
76
76
 
77
77
  def test_parse_deps_shell_str__echo():
78
78
  line = ' shell@echo "hello world"'
79
79
  d = deps_helpers.parse_deps_shell_str(line, '', '')
80
- assert d is not None, f'{d=}'
80
+ assert d, f'{d=}'
81
81
  assert d['exec_list'] == ['echo', '"hello', 'world"'], f'{d=}'
82
82
 
83
- def test_parse_deps_shell_str__path_replacement():
83
+ def test_parse_deps_shell_str__enable_filepath_replacement():
84
84
  # Dealing w/ relative paths, change the current working directory to the module directory
85
+ # Default is enabled.
85
86
  module_dir = os.path.dirname(os.path.abspath(__file__))
86
87
  os.chdir(module_dir)
87
88
  line = 'shell@cp ../deps_helpers.py foo.py'
88
89
  d = deps_helpers.parse_deps_shell_str(line, target_path='./', target_node='foo_target')
89
- assert d is not None, f'{d=}'
90
+ assert d, f'{d=}'
90
91
  assert d['exec_list'] == ['cp', os.path.abspath('../deps_helpers.py'), 'foo.py'], f'{d=}'
91
92
  assert d['target_node'] == 'foo_target'
92
93
  assert d['target_path'] == os.path.abspath('./')
93
94
 
95
+ def test_parse_deps_shell_str__disable_filepath_replacement():
96
+ # Dealing w/ relative paths, change the current working directory to the module directory
97
+ module_dir = os.path.dirname(os.path.abspath(__file__))
98
+ os.chdir(module_dir)
99
+ line = 'shell@cp ../deps_helpers.py foo.py'
100
+ d = deps_helpers.parse_deps_shell_str(line, target_path='./', target_node='foo_target',
101
+ enable_filepath_subst_target_dir=False)
102
+ assert d, f'{d=}'
103
+ assert d['exec_list'] == ['cp', '../deps_helpers.py', 'foo.py'], f'{d=}'
104
+ assert d['target_node'] == 'foo_target'
105
+ assert d['target_path'] == os.path.abspath('./')
106
+
107
+ def test_parse_deps_shell_str__enable_dirpath_replacement():
108
+ # Dealing w/ relative paths, change the current working directory to the module directory
109
+ module_dir = os.path.dirname(os.path.abspath(__file__))
110
+ os.chdir(module_dir)
111
+ line = 'shell@ls -ltr ./'
112
+ d = deps_helpers.parse_deps_shell_str(line, target_path='./', target_node='foo_target',
113
+ enable_dirpath_subst_target_dir=True)
114
+ assert d, f'{d=}'
115
+ assert d['exec_list'] == ['ls', '-ltr', os.path.abspath('./')], f'{d=}'
116
+ assert d['target_node'] == 'foo_target'
117
+ assert d['target_path'] == os.path.abspath('./')
118
+
119
+ def test_parse_deps_shell_str__disable_dirpath_replacement():
120
+ # Dealing w/ relative paths, change the current working directory to the module directory
121
+ # Default is disabled.
122
+ module_dir = os.path.dirname(os.path.abspath(__file__))
123
+ os.chdir(module_dir)
124
+ line = 'shell@ls -ltr ./'
125
+ d = deps_helpers.parse_deps_shell_str(line, target_path='./', target_node='foo_target')
126
+ assert d, f'{d=}'
127
+ assert d['exec_list'] == ['ls', '-ltr', './'], f'{d=}'
128
+ assert d['target_node'] == 'foo_target'
129
+ assert d['target_path'] == os.path.abspath('./')
130
+
94
131
 
95
132
  def test_parse_deps_work_dir_add_srcs__no_parse():
96
133
  line = 'some_file.sv'
97
134
  d = deps_helpers.parse_deps_work_dir_add_srcs(line, '', '')
98
- assert d is None, f'{d=}'
135
+ assert not d, f'{d=}'
99
136
 
100
137
  line = 'some_target:'
101
138
  d = deps_helpers.parse_deps_work_dir_add_srcs(line, '', '')
102
- assert d is None, f'{d=}'
139
+ assert not d, f'{d=}'
103
140
 
104
141
  line = ' csr@some_file.sv'
105
142
  d = deps_helpers.parse_deps_work_dir_add_srcs(line, '', '')
106
- assert d is None, f'{d=}'
143
+ assert not d, f'{d=}'
107
144
 
108
145
  def test_parse_deps_work_dir_add_srcs__single_file():
109
146
  line = ' work_dir_add_srcs@ single_file.txt'
110
147
  d = deps_helpers.parse_deps_work_dir_add_srcs(line, '', '')
111
- assert d is not None, f'{d=}'
148
+ assert d, f'{d=}'
112
149
  assert d['file_list'] == ['single_file.txt']
113
150
 
114
151
  def test_parse_deps_work_dir_add_srcs__several_file():
115
152
  line = ' work_dir_add_srcs@ single_file.txt another.sv gen-verilog/mine.v ./gen-vhdl/wordy.vhdl'
116
153
  d = deps_helpers.parse_deps_work_dir_add_srcs(line, '', '')
117
- assert d is not None, f'{d=}'
154
+ assert d, f'{d=}'
118
155
  assert d['file_list'] == [
119
156
  'single_file.txt', 'another.sv', 'gen-verilog/mine.v', './gen-vhdl/wordy.vhdl'
120
157
  ]
@@ -123,33 +160,33 @@ def test_parse_deps_work_dir_add_srcs__several_file():
123
160
  def test_parse_deps_peakrdl__no_parse():
124
161
  line = 'some_file.sv'
125
162
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
126
- assert d is None, f'{d=}'
163
+ assert not d, f'{d=}'
127
164
 
128
165
  line = 'some_target:'
129
166
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
130
- assert d is None, f'{d=}'
167
+ assert not d, f'{d=}'
131
168
 
132
169
  line = ' csr@some_file.sv'
133
170
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
134
- assert d is None, f'{d=}'
171
+ assert not d, f'{d=}'
135
172
 
136
173
  def test_parse_deps_peakrdl__with_top():
137
174
  line = ' peakrdl@ --cpuif axi4-lite-flat --top my_fancy_csrs ./my_csrs.rdl'
138
175
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
139
- assert d is not None, f'{d=}'
176
+ assert d, f'{d=}'
140
177
  assert len(d['shell_commands_list']) > 0
141
178
  assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv', 'peakrdl/my_fancy_csrs.sv']
142
179
 
143
180
  def test_parse_deps_peakrdl__with_top2():
144
181
  line = ' peakrdl@ --cpuif axi4-lite-flat --top=my_fancy_csrs ./my_csrs.rdl'
145
182
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
146
- assert d is not None, f'{d=}'
183
+ assert d, f'{d=}'
147
184
  assert len(d['shell_commands_list']) > 0
148
185
  assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_fancy_csrs_pkg.sv', 'peakrdl/my_fancy_csrs.sv']
149
186
 
150
187
  def test_parse_deps_peakrdl__infer_top():
151
188
  line = ' peakrdl@ --cpuif axi4-lite-flat ./my_csrs.rdl'
152
189
  d = deps_helpers.parse_deps_peakrdl(line, '', '')
153
- assert d is not None, f'{d=}'
190
+ assert d, f'{d=}'
154
191
  assert len(d['shell_commands_list']) > 0
155
192
  assert d['work_dir_add_srcs']['file_list'] == ['peakrdl/my_csrs_pkg.sv', 'peakrdl/my_csrs.sv']
opencos/tests/test_eda.py CHANGED
@@ -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
opencos/tools/vivado.py CHANGED
@@ -136,7 +136,7 @@ class CommandSimVivado(CommandSim, ToolVivado):
136
136
  def __init__(self, config: dict):
137
137
  CommandSim.__init__(self, config)
138
138
  ToolVivado.__init__(self, config=self.config)
139
- # add args specific to this simulator
139
+ # add args specific to this tool
140
140
  self.args.update({
141
141
  'gui': False,
142
142
  'tcl-file': 'sim.tcl',
@@ -349,7 +349,7 @@ class CommandElabVivado(CommandSimVivado):
349
349
  '''CommandElabVivado is a command handler for: eda elab --tool=vivado, uses xvlog, xelab'''
350
350
  def __init__(self, config: dict):
351
351
  CommandSimVivado.__init__(self, config)
352
- # add args specific to this simulator
352
+ # add args specific to this tool
353
353
  self.args['stop-after-elaborate'] = True
354
354
 
355
355
 
@@ -358,7 +358,7 @@ class CommandSynthVivado(CommandSynth, ToolVivado):
358
358
  def __init__(self, config: dict):
359
359
  CommandSynth.__init__(self, config)
360
360
  ToolVivado.__init__(self, config=self.config)
361
- # add args specific to this simulator
361
+ # add args specific to this tool
362
362
  self.args['gui'] = False
363
363
  self.args['tcl-file'] = "synth.tcl"
364
364
  self.args['xdc'] = ""
@@ -401,9 +401,7 @@ class CommandSynthVivado(CommandSynth, ToolVivado):
401
401
  defines = ""
402
402
  for key, value in self.defines.items():
403
403
  defines += (f"-verilog_define {key}" + (" " if value is None else f"={value} "))
404
- incdirs = ""
405
- if self.incdirs:
406
- incdirs = " -include_dirs " + ";".join(self.incdirs)
404
+ incdirs = ' '.join([f'-include_dirs {x}' for x in self.incdirs])
407
405
  flatten = ""
408
406
  if self.args['flatten-all']:
409
407
  flatten = "-flatten_hierarchy full"
@@ -547,7 +545,7 @@ class CommandProjVivado(CommandProj, ToolVivado):
547
545
  def __init__(self, config: dict):
548
546
  CommandProj.__init__(self, config)
549
547
  ToolVivado.__init__(self, config=self.config)
550
- # add args specific to this simulator
548
+ # add args specific to this tool
551
549
  self.args['gui'] = True
552
550
  self.args['oc-vivado-tcl'] = True
553
551
  self.args['tcl-file'] = "proj.tcl"
@@ -630,10 +628,11 @@ class CommandBuildVivado(CommandBuild, ToolVivado):
630
628
  def __init__(self, config: dict):
631
629
  CommandBuild.__init__(self, config)
632
630
  ToolVivado.__init__(self, config=self.config)
633
- # add args specific to this simulator
631
+ # add args specific to this tool
634
632
  self.args['gui'] = False
635
633
  self.args['fpga'] = ""
636
634
  self.args['proj'] = False
635
+ self.args['resynth'] = False
637
636
  self.args['reset'] = False
638
637
  self.args['all-sv'] = False
639
638
 
@@ -705,6 +704,8 @@ class CommandBuildVivado(CommandBuild, ToolVivado):
705
704
  ]
706
705
  if self.args['proj']:
707
706
  command_list += ['--proj']
707
+ if self.args['resynth']:
708
+ command_list += ['--resynth']
708
709
  if self.args['reset']:
709
710
  command_list += ['--reset']
710
711
 
@@ -732,7 +733,7 @@ class CommandUploadVivado(CommandUpload, ToolVivado):
732
733
  def __init__(self, config: dict):
733
734
  CommandUpload.__init__(self, config)
734
735
  ToolVivado.__init__(self, config=self.config)
735
- # add args specific to this simulator
736
+ # add args specific to this tool
736
737
  self.args.update({
737
738
  'gui': False,
738
739
  'bitfile': "",
@@ -743,8 +744,10 @@ class CommandUploadVivado(CommandUpload, ToolVivado):
743
744
  'device': -1,
744
745
  'host': "localhost",
745
746
  'port': 3121,
746
- 'tcl-file': "upload.tcl",
747
747
  'all-sv': False,
748
+ 'tcl-file': "eda_upload.tcl",
749
+ 'log-file': "eda_upload.log",
750
+ 'test-mode': False,
748
751
  })
749
752
  # TODO(drew): Add self.args_help.update({...})
750
753
 
@@ -825,7 +828,7 @@ class CommandUploadVivado(CommandUpload, ToolVivado):
825
828
 
826
829
  # ── Generate TCL script ───────────────────────────────────────────────────
827
830
  script_file = Path(self.args['tcl-file'])
828
- log_file = Path("eda_upload.log")
831
+ log_file = Path(self.args['log-file'])
829
832
 
830
833
  try:
831
834
  with script_file.open("w", encoding="utf-8") as fout:
@@ -924,6 +927,9 @@ class CommandUploadVivado(CommandUpload, ToolVivado):
924
927
  command_list.append('-notrace')
925
928
  self.exec(Path(util.getcwd()), command_list)
926
929
 
930
+ if not self.args['keep']:
931
+ os.unlink(self.args['tcl-file'])
932
+
927
933
  util.info("Upload done")
928
934
 
929
935
 
@@ -935,7 +941,7 @@ class CommandOpenVivado(CommandOpen, ToolVivado):
935
941
  def __init__(self, config: dict):
936
942
  CommandOpen.__init__(self, config)
937
943
  ToolVivado.__init__(self, config=self.config)
938
- # add args specific to this simulator
944
+ # add args specific to this tool
939
945
  self.args['gui'] = True
940
946
  self.args['file'] = False
941
947
  self.args['all-sv'] = False
opencos/util.py CHANGED
@@ -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.46
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
@@ -1,10 +1,10 @@
1
1
  opencos/__init__.py,sha256=ZQ7aOCbP0jkIMYGdVrb-mkZX3rnvaK8epBkmp506tXU,85
2
2
  opencos/_version.py,sha256=qN7iBoOv-v4tEZz-Pu9sVUJwefshJOsgdaddn8HcHio,510
3
3
  opencos/_waves_pkg.sv,sha256=1lbhQOVGc3t_R8czYjP40hssP0I3FlZOpHTkI7yKFbI,1251
4
- opencos/deps_helpers.py,sha256=Q49Su7cJdkcjC9PufE2EtveHYAuENe2F-Prpmlz7tcI,57245
5
- opencos/deps_schema.py,sha256=-zXzU_l0mv8abaOgBcxmRiyIelZ9jOUBXI8ahf3ymBU,14860
6
- opencos/eda.py,sha256=Ky_Uc47TU1N6lRs06eatdpAvtgoSh0iXO0BxhSfEuk0,19420
7
- opencos/eda_base.py,sha256=utUTieQMv0RC2Mz5n8b7rtneKjSFI3YcgCiJON0T7Ws,84410
4
+ opencos/deps_helpers.py,sha256=iFUMnrh8hiNflRgZWp0hid7x6BKk1P2PegUn_gJS8Z0,59452
5
+ opencos/deps_schema.py,sha256=ECysiiagTGXAtAVp2GnW3N_Iw2ZakYEuSEg-WUWtUKo,15469
6
+ opencos/eda.py,sha256=_gGZmXIsjACImBprg_Zx6oitem0xKVrboNBk6ignDg0,19757
7
+ opencos/eda_base.py,sha256=vOxZrIJs4Y8OZjRbEC4UkYUjznLl96R7AVh5vJOC15Q,85962
8
8
  opencos/eda_config.py,sha256=8wwX4PTZ5rmxWogrVxxqAY6adQFPxTsFkkTYbo6H4vU,8853
9
9
  opencos/eda_config_defaults.yml,sha256=AteZbwRJ6wviinks47-e5TX0DoU0culJfBw6L3xYOOA,11535
10
10
  opencos/eda_config_max_verilator_waivers.yml,sha256=lTAU4IOEbUWVlPzuer1YYhIyxpPINeA4EJqcRIT-Ymk,840
@@ -20,30 +20,30 @@ opencos/oc_cli.py,sha256=kj2OXvgxli2WPj4MQ4zTBb36eFtsP2gsqDdeNeWGL4E,132108
20
20
  opencos/pcie.py,sha256=VUJljaZJYgScAAx5yn7F6GoA8K9eTcw24otYZbkMpYs,3035
21
21
  opencos/peakrdl_cleanup.py,sha256=vLhSOVs6cEzsi_PwAP4pSXu5_ZMZjDvfK_WmHDLbDac,486
22
22
  opencos/seed.py,sha256=8TA2uXhBuT_lOaQdAKqdReYvfBWi_KuyQCFJzA2rOVM,549
23
- opencos/util.py,sha256=IH6yeazG5VI95I7lGDnn2U58QEYM5ozm3SOHh6rVD_s,29395
23
+ opencos/util.py,sha256=zG2b22EdgR5G2xFUGFjq3sv9DthKtidhPHURnymQ5dk,31242
24
24
  opencos/commands/__init__.py,sha256=DtOA56oWJu68l-_1_7Gdv0N-gtXVB3-p9IhGzAYex8U,1014
25
25
  opencos/commands/build.py,sha256=jI5ul53qfwn6X-yfSdSQIcLBhGtzZUk7r_wKBBmKJI0,1425
26
26
  opencos/commands/elab.py,sha256=m6Gk03wSzX8UkcmReooK7turF7LpqO0IcdOZwJ8XiyI,1596
27
27
  opencos/commands/export.py,sha256=juzxJL5-RpEnU5DmwF0fiG5pUrB2BbUbvCp2OasEd88,3494
28
28
  opencos/commands/flist.py,sha256=XO0JzNF4cEYlqoO6fJFEH-SjsqQtmIvDO-xRQL7wllY,8531
29
29
  opencos/commands/lec.py,sha256=gN6nQ4GURhPC8nu8Zuj08s1fmNzuiuaS9vJgtNZyX28,3647
30
- opencos/commands/multi.py,sha256=1IH5Bk5xnKsNvtEsYy8a6bFmcyHSfrMVK-lLNdj6vlg,26449
30
+ opencos/commands/multi.py,sha256=Aie9-g5m8yyMAjs-Bzu6ioE2Vm6QFrRAhtEbp-ndeaQ,27271
31
31
  opencos/commands/open.py,sha256=unrpGckzg0FE5W3oARq8x0jX7hhV_uM9Oh5FgISHFAg,724
32
32
  opencos/commands/proj.py,sha256=MdHTOtQYG93_gT97dWuSyAgUxX2vi9FRhL0dtc-rM98,1096
33
33
  opencos/commands/shell.py,sha256=senuqSGOc5nVGU5voZNJO4_hzVAK0ELtu0wmRZgwv3k,7463
34
34
  opencos/commands/sim.py,sha256=oZyc4thCWZAk4668bbehRBzbOOVQDhyoDyemlDwYPFg,14059
35
- opencos/commands/sweep.py,sha256=O0VSpzHHlE_1hM-Edp4d80PAnh2P2HCIwsGLnhkRHWM,9107
35
+ opencos/commands/sweep.py,sha256=EnTLCTEA_3bXc9z9Jb2xT5SN-zwhltHjj_NN1O3vcW8,9296
36
36
  opencos/commands/synth.py,sha256=quB-HWS4LKYTiFBHiYarQi4pMnRmt12wQTZpi14VvlE,4355
37
37
  opencos/commands/targets.py,sha256=_jRNhm2Fqj0fmMvTw6Ba39DCsRHf_r_uZCy_R064kpA,1472
38
38
  opencos/commands/upload.py,sha256=nlb4nlxrDCQPcabEmH3nP19g4PFILDqFDab4LwJ95Z4,796
39
- opencos/commands/waves.py,sha256=SRfjfsqhuszXHylQrgqYiUT3a5CQs9doxJQzuV4Ae0w,7055
39
+ opencos/commands/waves.py,sha256=dsWwtjpDgH-YsiIjJgqTvteY3OZ48UnEAWc3blV2Fog,7055
40
40
  opencos/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  opencos/tests/custom_config.yml,sha256=TRoVM9ZFKPOA_8JmlpzaMhnGO1txmaD14N_8P1oqzew,257
42
- opencos/tests/helpers.py,sha256=r4UYMckYzHY2i2RbSWvysOjerPhty_7i7RB-Z767M-c,8294
42
+ opencos/tests/helpers.py,sha256=MQatotvBIYSOSsA3cZcIDmEc49khXrS9wA5h9wmNSjo,8876
43
43
  opencos/tests/test_build.py,sha256=FQAxOpLVQShAHD_L5rqJctPeSAoqoOCNFI0RXflLuY0,387
44
- opencos/tests/test_deps_helpers.py,sha256=_nJSgLN6WVlMKqu6sCr29gjQyN3Jj-dVk8Ac64ygpJs,5928
44
+ opencos/tests/test_deps_helpers.py,sha256=_C5qH1YuScCCvG3E-Lr8aS_n6C6YKgEl-wIycJEFLRY,7697
45
45
  opencos/tests/test_deps_schema.py,sha256=T3P9KjaMyKsk8b7snNVvNSsom2hIJcg6Z9apYiXoH9Y,941
46
- opencos/tests/test_eda.py,sha256=tplHcx4FiEn8Jmw1mJMlD6tGjpUJ6cxdBGiVRKo7Ykw,38809
46
+ opencos/tests/test_eda.py,sha256=SC008-wibXbnKkmdW9LUhXg0HhPJ1WpYbzi9aPAyqVA,38963
47
47
  opencos/tests/test_eda_elab.py,sha256=75bJpOaoO8rn1FXFxiE4KSu5FdjZP1IbW6SyTCjM_ao,2553
48
48
  opencos/tests/test_eda_synth.py,sha256=kYfceUB0qQwQmN_lIJvXkHzwoILL3Yb59h3nImg0hLU,5235
49
49
  opencos/tests/test_oc_cli.py,sha256=-ZmwVX_CPBXCGT9hXIBEr_XUSIGG2eky89YpSJIbRAg,731
@@ -68,12 +68,12 @@ opencos/tools/slang_yosys.py,sha256=3fyLRRdTXhSppNtUhhUl00oG-cT9TyyPTH6JvasS9ZE,
68
68
  opencos/tools/surelog.py,sha256=XhxJCGt8hyligL0LNT1fCWkHF5pkt4WSp3eqVJlQ4uA,4998
69
69
  opencos/tools/tabbycad_yosys.py,sha256=2LePPgYXBVdsy7YcffPIWN-I0B7queLQ_f_pme2SCGw,7803
70
70
  opencos/tools/verilator.py,sha256=dWnoO8FUvjdMyFmuTjHvC_XYI_zwjApJApYy7iYubf0,18398
71
- opencos/tools/vivado.py,sha256=_GVqKNIZt9CZeiXS5yZCWTFrhD3BKjcQfzhhxR1qLQM,40017
71
+ opencos/tools/vivado.py,sha256=D0we45_2M5dr7Dp6FBjSaXxYmRpgJWm4rq3dc23Uv1A,40219
72
72
  opencos/tools/yosys.py,sha256=FV43RcejyFir4B24WRShnzUoppJMK0sDDNkSlIX8Vew,25579
73
- opencos_eda-0.2.46.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
74
- opencos_eda-0.2.46.dist-info/licenses/LICENSE.spdx,sha256=8gn1610RMP6eFgT3Hm6q9VKXt0RvdTItL_oxMo72jII,189
75
- opencos_eda-0.2.46.dist-info/METADATA,sha256=po0uwX8zxK4LmDW80yfBySMF1T2VwUifZPRgcsHB9f4,604
76
- opencos_eda-0.2.46.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- opencos_eda-0.2.46.dist-info/entry_points.txt,sha256=V8OE1lySAFcFQpDNJuVxVZteeSmDH-joLMhGvrxrvmg,164
78
- opencos_eda-0.2.46.dist-info/top_level.txt,sha256=J4JDP-LpRyJqPNeh9bSjx6yrLz2Mk0h6un6YLmtqql4,8
79
- opencos_eda-0.2.46.dist-info/RECORD,,
73
+ opencos_eda-0.2.48.dist-info/licenses/LICENSE,sha256=HyVuytGSiAUQ6ErWBHTqt1iSGHhLmlC8fO7jTCuR8dU,16725
74
+ opencos_eda-0.2.48.dist-info/licenses/LICENSE.spdx,sha256=8gn1610RMP6eFgT3Hm6q9VKXt0RvdTItL_oxMo72jII,189
75
+ opencos_eda-0.2.48.dist-info/METADATA,sha256=CGrTq0YGaOZ0CEe-x3Lm-BJcoD8S1QyiJsM35P09_qk,604
76
+ opencos_eda-0.2.48.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
+ opencos_eda-0.2.48.dist-info/entry_points.txt,sha256=V8OE1lySAFcFQpDNJuVxVZteeSmDH-joLMhGvrxrvmg,164
78
+ opencos_eda-0.2.48.dist-info/top_level.txt,sha256=J4JDP-LpRyJqPNeh9bSjx6yrLz2Mk0h6un6YLmtqql4,8
79
+ opencos_eda-0.2.48.dist-info/RECORD,,