opencos-eda 0.2.36__py3-none-any.whl → 0.2.39__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.
@@ -17,6 +17,7 @@ from .sweep import CommandSweep
17
17
  from .synth import CommandSynth
18
18
  from .upload import CommandUpload
19
19
  from .waves import CommandWaves
20
+ from .targets import CommandTargets
20
21
 
21
22
  __all__ = [
22
23
  'CommandBuild',
@@ -32,4 +33,5 @@ __all__ = [
32
33
  'CommandToolsMulti',
33
34
  'CommandUpload',
34
35
  'CommandWaves',
36
+ 'CommandTargets',
35
37
  ]
opencos/commands/multi.py CHANGED
@@ -392,7 +392,9 @@ class CommandMulti(CommandParallel):
392
392
  # Special case for 'multi' --export-jsonl, run reach child with --export-json
393
393
  command_list.append('--export-json')
394
394
  if tool and len(all_multi_tools) > 1:
395
- command_list.append(f'--sub-work-dir={short_target}.{command}.{tool}')
395
+ command_list.append(f'--job-name={short_target}.{command}.{tool}')
396
+ else:
397
+ command_list.append(f'--job-name={short_target}.{command}')
396
398
 
397
399
  def append_jobs_from_targets(self, args:list):
398
400
  '''Helper method in CommandMulti to apply 'args' (list) to all self.targets,
@@ -430,13 +432,15 @@ class CommandMulti(CommandParallel):
430
432
  type(self.args['single-timeout']) in [int, str]:
431
433
  command_list = ['timeout', str(self.args['single-timeout'])] + command_list
432
434
 
433
- name = target
435
+ name = self.get_name_from_target(target)
434
436
  if tool and (len(all_multi_tools) > 1 or self.command_name == 'tools-multi'):
435
- name = f'{short_target} ({tool})'
437
+ name += f' ({tool})'
436
438
 
437
439
  this_job_dict = {
438
440
  'name' : name,
439
441
  'index' : len(self.jobs),
442
+ 'command': command,
443
+ 'target': target,
440
444
  'command_list' : command_list
441
445
  }
442
446
  if tool:
opencos/commands/sweep.py CHANGED
@@ -113,6 +113,9 @@ class CommandSweep(CommandDesign, CommandParallel):
113
113
  util.debug(f"Sweep: arg_tokens: '{arg_tokens}'")
114
114
  util.debug(f"Sweep: target: '{self.sweep_target}'")
115
115
 
116
+ if not self.sweep_target:
117
+ self.error(f"Sweep can only take one target, found none, args: {unparsed}")
118
+
116
119
  # now create the list of jobs, support one axis
117
120
  self.jobs = []
118
121
 
@@ -197,15 +200,20 @@ class CommandSweep(CommandDesign, CommandParallel):
197
200
  f"target={self.sweep_target}, arg_tokens={arg_tokens},",
198
201
  f"sweep_axis_list={sweep_axis_list}")
199
202
  if not sweep_axis_list:
200
- # we aren't sweeping anything, create one job
201
- snapshot_name = self.sweep_target.replace('../','').replace('/','_') + sweep_string
203
+ # we aren't sweeping anything, create one job:
204
+ # name {target}.{command}[.sweep_value,.sweep_value,...]
205
+ snapshot_name = self.get_name_from_target(self.sweep_target)
206
+ snapshot_name = snapshot_name.replace(os.sep, '_') \
207
+ + f'.{self.single_command}{sweep_string}'
202
208
  eda_path = get_eda_exec('sweep')
203
209
  self.jobs.append({
204
210
  'name' : snapshot_name,
205
211
  'index' : len(self.jobs),
212
+ 'command': self.single_command,
213
+ 'target': self.sweep_target,
206
214
  'command_list' : (
207
215
  [eda_path, self.single_command, self.sweep_target,
208
- '--job_name', snapshot_name] + arg_tokens
216
+ f'--job-name={snapshot_name}'] + arg_tokens
209
217
  )
210
218
  })
211
219
  return
@@ -221,7 +229,7 @@ class CommandSweep(CommandDesign, CommandParallel):
221
229
  this_arg_tokens.append(f'{lhs}{operator}{v}')
222
230
 
223
231
  v_string = f"{v}".replace('.','p')
224
- this_sweep_string = sweep_string + f"_{lhs_trimmed}_{v_string}"
232
+ this_sweep_string = sweep_string + f".{lhs_trimmed}_{v_string}"
225
233
 
226
234
  self.expand_sweep_axis(
227
235
  arg_tokens=this_arg_tokens,
@@ -0,0 +1,49 @@
1
+ '''opencos.commands.targets - command handler for: eda targets [args]
2
+
3
+ Note this command is handled differently than others (such as CommandSim),
4
+ it is generally run as simply
5
+
6
+ > eda targets
7
+ > eda targets <directory>
8
+ > eda targets [directory/]<pattern> [directory2/]<pattern2> ...
9
+
10
+ uses no tools and will print a pretty list of targets to stdout.
11
+ '''
12
+
13
+ # Note - similar code waiver, tricky to eliminate it with inheritance when
14
+ # calling reusable methods.
15
+ # pylint: disable=R0801
16
+
17
+ import os
18
+
19
+ from opencos import eda_extract_targets
20
+ from opencos.eda_base import Command
21
+
22
+
23
+ class CommandTargets:
24
+ '''command handler for: eda targets'''
25
+
26
+ command_name = 'targets'
27
+
28
+ def __init__(self, config: dict):
29
+ # We don't inherit opencos.eda_base.Command, so we have to set a few
30
+ # member vars for Command.help to work.
31
+ self.args = {}
32
+ self.args_help = {}
33
+ self.config = config
34
+ self.status = 0
35
+
36
+ def process_tokens( # pylint: disable=unused-argument
37
+ self, tokens: list, process_all: bool = True,
38
+ pwd: str = os.getcwd()
39
+ ) -> list:
40
+ '''This is effectively our 'run' method, entrypoint from opencos.eda.main'''
41
+
42
+ eda_extract_targets.run(partial_paths=tokens, base_path=pwd)
43
+ return []
44
+
45
+ def help(self, tokens: list) -> None:
46
+ '''Since we don't inherit from opencos.eda_base.Command, need our own help
47
+ method
48
+ '''
49
+ Command.help(self, tokens=tokens)
opencos/eda.py CHANGED
@@ -10,6 +10,7 @@ import re
10
10
  import signal
11
11
  import argparse
12
12
  import shlex
13
+ import importlib.util
13
14
 
14
15
  import opencos
15
16
  from opencos import util, files
@@ -19,7 +20,6 @@ from opencos.eda_base import Tool, which_tool
19
20
 
20
21
  # Globals
21
22
 
22
- debug_respawn = False
23
23
  util.progname = "EDA"
24
24
 
25
25
 
@@ -30,37 +30,25 @@ util.progname = "EDA"
30
30
  # eda command (such as, command: eda sim) is handled by which class (such as class: CommandSim)
31
31
  # These are also overriden depending on the tool, for example --tool verilator sets
32
32
  # "sim": CommandSimVerilator.
33
- def init_config(config: dict, quiet=False, tool=None) -> dict:
33
+ def init_config(
34
+ config: dict, quiet: bool = False, tool=None, run_auto_tool_setup: bool = True
35
+ ) -> dict:
34
36
  '''Sets or clears entries in config (dict) so tools can be re-loaded.'''
35
37
 
36
- from opencos.commands import CommandSim, CommandElab, CommandSynth, \
37
- CommandFList, CommandProj, CommandMulti, CommandToolsMulti, \
38
- CommandSweep, CommandBuild, CommandWaves, CommandUpload, CommandOpen, \
39
- CommandExport
40
-
41
- # If config didn't set the auto_tools_order, then use a fully populated default
42
- # dict:
43
- config = eda_config.get_config_merged_with_defaults(config)
44
-
45
- config['command_handler'] = {
46
- "sim" : CommandSim,
47
- "elab" : CommandElab,
48
- "synth" : CommandSynth,
49
- "flist" : CommandFList,
50
- "proj" : CommandProj,
51
- "multi" : CommandMulti,
52
- "tools-multi" : CommandToolsMulti,
53
- "sweep" : CommandSweep,
54
- "build" : CommandBuild,
55
- "waves" : CommandWaves,
56
- "upload" : CommandUpload,
57
- "open" : CommandOpen,
58
- "export" : CommandExport,
59
- }
38
+ # For key DEFAULT_HANDLERS, we'll update config['command_handler'] with
39
+ # the actual class using importlib (via opencos.util)
40
+ config['command_handler'] = {}
41
+ for command, str_class in config['DEFAULT_HANDLERS'].items():
42
+ cls = util.import_class_from_string(str_class)
43
+ if not cls:
44
+ util.error(f"config DEFAULT_HANDLERS {command=} {str_class=} could not import")
45
+ else:
46
+ config['command_handler'][command] = cls
60
47
 
61
48
  config['auto_tools_found'] = dict()
62
49
  config['tools_loaded'] = set()
63
- config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
50
+ if run_auto_tool_setup:
51
+ config = auto_tool_setup(config=config, quiet=quiet, tool=tool)
64
52
  return config
65
53
 
66
54
 
@@ -154,7 +142,6 @@ def auto_tool_setup(warnings:bool=True, config=None, quiet=False, tool=None) ->
154
142
  tool='verlator', tool='verilator=/path/to/verilator.exe'
155
143
  If so, updates config['auto_tools_order'][tool]['exe']
156
144
  '''
157
- import importlib.util
158
145
 
159
146
  tool = eda_config.update_config_auto_tool_order_for_tool(
160
147
  tool=tool, config=config
@@ -237,7 +224,6 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
237
224
  tool='verlator', tool='verilator=/path/to/verilator.exe'
238
225
 
239
226
  '''
240
- import importlib
241
227
 
242
228
  tool = eda_config.update_config_auto_tool_order_for_tool(
243
229
  tool=tool, config=config
@@ -281,10 +267,7 @@ def tool_setup(tool: str, config: dict, quiet: bool = False, auto_setup: bool =
281
267
  # skip, already has a tool associated with it, and we're in auto_setup=True
282
268
  continue
283
269
 
284
- if str_class_name not in globals():
285
- cls = util.import_class_from_string(str_class_name)
286
- else:
287
- cls = globals().get(str_class_name, None)
270
+ cls = util.import_class_from_string(str_class_name)
288
271
 
289
272
  assert issubclass(cls, Tool), f'{str_class_name=} is does not have Tool class associated with it'
290
273
  util.debug(f'Setting {cls=} for {command=} in config.command_handler')
@@ -302,6 +285,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
302
285
 
303
286
  deferred_tokens = []
304
287
  command = ""
288
+ run_auto_tool_setup = True
305
289
 
306
290
  parser = eda_base.get_argparser()
307
291
  try:
@@ -324,22 +308,29 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
324
308
  if parsed.eda_safe:
325
309
  eda_config.update_config_for_eda_safe(config)
326
310
 
311
+ util.debug(f'eda process_tokens: {parsed=} {unparsed=}')
312
+
313
+ # Attempt to get the 'command' in the unparsed args before we've even
314
+ # set the command handlers (some commands don't use tools).
315
+ for value in unparsed:
316
+ if value in config['DEFAULT_HANDLERS'].keys():
317
+ command = value
318
+ if value in config['command_uses_no_tools']:
319
+ run_auto_tool_setup = False
320
+ unparsed.remove(value)
321
+ break
322
+
327
323
  if not interactive:
328
324
  # Run init_config() now, we deferred it in main(), but only run it
329
325
  # for this tool (or tool=None to figure it out)
330
- config = init_config(config, tool=parsed.tool)
326
+ config = init_config(
327
+ config, tool=parsed.tool,
328
+ run_auto_tool_setup=run_auto_tool_setup
329
+ )
331
330
  if not config:
332
331
  util.error(f'eda.py main: problem loading config, {args=}')
333
332
  return 3
334
333
 
335
-
336
- util.debug(f'eda process_tokens: {parsed=} {unparsed=}')
337
- for value in unparsed:
338
- if value in config['command_handler'].keys():
339
- command = value
340
- unparsed.remove(value)
341
- break
342
-
343
334
  # Deal with help, now that we have the command (if it was set).
344
335
  if parsed.help:
345
336
  if not command:
@@ -363,7 +354,9 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
363
354
  util.debug(f'{command=}')
364
355
  util.debug(f'{sco.config=}')
365
356
  util.debug(f'{type(sco)=}')
366
- if not parsed.tool and command not in config.get('command_determines_tool', []):
357
+ if not parsed.tool and \
358
+ command not in config.get('command_determines_tool', []) and \
359
+ command not in config.get('command_uses_no_tools', []):
367
360
  use_tool = which_tool(command, config)
368
361
  util.info(f"--tool not specified, using default for {command=}: {use_tool}")
369
362
 
@@ -377,7 +370,7 @@ def process_tokens(tokens: list, original_args: list, config: dict, interactive=
377
370
  sco.config['eda_original_args'] = original_args
378
371
 
379
372
  setattr(sco, 'command_name', command) # as a safeguard, b/c 'command' is not always passed to 'sco'
380
- unparsed = sco.process_tokens(deferred_tokens)
373
+ unparsed = sco.process_tokens(tokens=deferred_tokens, pwd=os.getcwd())
381
374
 
382
375
  # query the status from the Command object (0 is pass, > 0 is fail)
383
376
  rc = getattr(sco, 'status', 1)
@@ -439,7 +432,6 @@ def main(*args):
439
432
  # Handle --config-yml= arg
440
433
  config, unparsed = eda_config.get_eda_config(unparsed)
441
434
 
442
-
443
435
  # Note - we used to call: config = init_config(config=config)
444
436
  # However, we now defer calling init_config(..) until eda.process_tokens(..)
445
437
 
@@ -459,22 +451,8 @@ def main(*args):
459
451
  config=config)
460
452
 
461
453
 
462
- def main_cli(support_respawn=False):
454
+ def main_cli() -> None:
463
455
  ''' Returns None, will exit with return code. Entry point for package script or __main__.'''
464
-
465
- if support_respawn and '--no-respawn' not in sys.argv:
466
- # If someone called eda.py directly (aka, __name__ == '__main__'),
467
- # then we still support a legacy mode of operation - where we check
468
- # for OC_ROOT (in env, or git repo) to make sure this is the right
469
- # location of eda.py by calling main_cli(support_respawn=True).
470
- # Otherwise, we do not respawn $OC_ROOT/bin/eda.py
471
- # Can also be avoided with --no-respawn.
472
-
473
- # Note - respawn will never work if calling as a package executable script,
474
- # which is why our package entrypoint will be main_cli() w/out support_respawn.
475
- main_maybe_respawn()
476
-
477
-
478
456
  signal.signal(signal.SIGINT, signal_handler)
479
457
  util.global_exit_allowed = True
480
458
  # Strip eda or eda.py from sys.argv, we know who we are if called from __main__:
@@ -482,46 +460,8 @@ def main_cli(support_respawn=False):
482
460
  util.exit(rc)
483
461
 
484
462
 
485
- def main_maybe_respawn():
486
- ''' Returns None, will respawn - run - exit, or will return and the command
487
-
488
- is expected to run in main_cli()'''
489
-
490
- # First we check if we are respawning
491
- this_path = os.path.realpath(__file__)
492
- if debug_respawn: util.info(f"RESPAWN: this_path : '{this_path}'")
493
- oc_root = util.get_oc_root()
494
- if debug_respawn: util.info(f"RESPAWN: oc_root : '{oc_root}'")
495
- cwd = util.getcwd()
496
- if debug_respawn: util.info(f"RESPAWN: cwd : '{cwd}'")
497
- if oc_root:
498
- new_paths = [
499
- os.path.join(oc_root, 'opencos', 'eda.py'),
500
- os.path.join(oc_root, 'bin', 'eda'),
501
- ]
502
- if debug_respawn: util.info(f"RESPAWN: {new_paths=} {this_path=}")
503
- if this_path not in new_paths and os.path.exists(new_paths[0]):
504
- # we are not the correct version of EDA for this Git repo, we should respawn
505
- util.info(f"{this_path} respawning {new_paths[0]} in {cwd} with --no-respawn")
506
- sys.argv[0] = new_paths[0]
507
- sys.argv.insert(1, '--no-respawn')
508
- proc = subprocess.Popen(sys.argv, shell=0, cwd=cwd, universal_newlines=True)
509
- while True:
510
- try:
511
- proc.communicate()
512
- break
513
- except KeyboardInterrupt:
514
- continue
515
- # get exit status from proc and return it
516
- util.exit(proc.returncode, quiet=True)
517
- else:
518
- if debug_respawn: util.info(f"RESPAWN: {oc_root=} respawn not necessary")
519
- else:
520
- if debug_respawn: util.info("RESPAWN: respawn not necessary")
521
-
522
-
523
463
  if __name__ == '__main__':
524
- main_cli(support_respawn=True)
464
+ main_cli()
525
465
 
526
466
  # IDEAS:
527
467
  # * options with no default (i.e. if user doesn't override, THEN we set it, like "seed" or "work-dir") can be given a
opencos/eda_base.py CHANGED
@@ -11,6 +11,7 @@ import signal
11
11
  import sys
12
12
  import threading
13
13
  import time
14
+ from pathlib import Path
14
15
 
15
16
  import opencos
16
17
  from opencos import seed, deps_helpers, util, files
@@ -184,20 +185,24 @@ class Command:
184
185
  'export-json': False, # generates an export.json suitable for a testrunner, if possible for self.command.
185
186
  'enable-tags': [],
186
187
  'disable-tags': [],
188
+ 'test-mode': False,
187
189
  })
188
190
  self.args_help.update({
189
- 'stop-before-compile': 'stop this run before any compile (if possible for tool) and' \
190
- + ' save .sh scripts in eda-dir/',
191
+ 'stop-before-compile': ('stop this run before any compile (if possible for tool) and'
192
+ ' save .sh scripts in eda-dir/'),
191
193
  'eda-dir': 'relative directory where eda logs are saved',
192
194
  'export': 'export results for these targets in eda-dir',
193
195
  'export-run': 'export, and run, results for these targets in eda-dir',
194
196
  'export-json': 'export, and save a JSON file per target',
195
- 'work-dir': 'Optional override for working directory, often defaults to ./eda.work/<top>.<command>',
196
- 'enable-tags': 'DEPS markup tag names to be force enabled for this' \
197
- + ' command (mulitple appends to list).',
198
- 'diable-tags': 'DEPS markup tag names to be disabled (even if they' \
199
- + ' match the criteria) for this command (mulitple appends to list).' \
200
- + ' --disable-tags has higher precedence than --enable-tags.'
197
+ 'work-dir': ('Optional override for working directory, often defaults to'
198
+ ' ./eda.work/<top>.<command>'),
199
+ 'enable-tags': ('DEPS markup tag names to be force enabled for this'
200
+ ' command (mulitple appends to list).'),
201
+ 'diable-tags': ('DEPS markup tag names to be disabled (even if they'
202
+ ' match the criteria) for this command (mulitple appends to list).'
203
+ ' --disable-tags has higher precedence than --enable-tags.'),
204
+ 'test-mode': ('command and tool dependent, usually stops the command early without'
205
+ ' executing.'),
201
206
  })
202
207
  self.modified_args = {}
203
208
  self.config = copy.deepcopy(config) # avoid external modifications.
@@ -223,8 +228,8 @@ class Command:
223
228
 
224
229
  def create_work_dir(self):
225
230
  util.debug(f"create_work_dir: {self.args['eda-dir']=} {self.args['work-dir']=}")
226
- if (not os.path.exists(self.args['eda-dir'])): # use os.path.isfile / isdir also
227
- os.mkdir(self.args['eda-dir'])
231
+ if not os.path.exists(self.args['eda-dir']):
232
+ util.safe_mkdir(self.args['eda-dir'])
228
233
  util.info(f"create_work_dir: created {self.args['eda-dir']}")
229
234
  if self.args['design'] == "":
230
235
  if ('top' in self.args) and (self.args['top'] != ""):
@@ -250,14 +255,35 @@ class Command:
250
255
  if (os.path.exists(self.args['work-dir'])):
251
256
  if os.path.exists(keep_file) and not self.args['force']:
252
257
  self.error(f"Cannot remove old work dir due to '{keep_file}'")
253
- util.info(f"Removing previous '{self.args['work-dir']}'")
254
- shutil.rmtree(self.args['work-dir'])
255
- os.mkdir(self.args['work-dir'])
256
- util.debug(f'create_work_dir: created {self.args["work-dir"]}')
258
+ elif os.path.abspath(self.args['work-dir']) in os.getcwd():
259
+ # This effectively checks if
260
+ # --work-dir=.
261
+ # --work-dir=$PWD
262
+ # --work-dir=/some/path/almost/here
263
+ # Allow it, but preserve the existing directory, we don't want to blow away
264
+ # files up-heir from us.
265
+ # Enables support for --work-dir=.
266
+ util.info(f"Not removing existing work-dir: '{self.args['work-dir']}' is within {os.getcwd()=}")
267
+ elif str(Path(self.args['work-dir'])).startswith(str(Path('/'))):
268
+ # Do not allow other absolute path work dirs if it already exists.
269
+ # This prevents you from --work-dir=~ and eda wipes out your home dir.
270
+ self.error(f'Cannot use work-dir={self.args["work-dir"]} starting with absolute path "/"')
271
+ elif str(Path('..')) in str(Path(self.args['work-dir'])):
272
+ # Do not allow other ../ work dirs if it already exists.
273
+ self.error(f'Cannot use work-dir={self.args["work-dir"]} with up-hierarchy ../ paths')
274
+ else:
275
+ # If we made it this far, on a directory that exists, that appears safe
276
+ # to delete and re-create:
277
+ util.info(f"Removing previous '{self.args['work-dir']}'")
278
+ shutil.rmtree(self.args['work-dir'])
279
+ util.safe_mkdir(self.args['work-dir'])
280
+ util.debug(f'create_work_dir: created {self.args["work-dir"]}')
281
+ else:
282
+ util.safe_mkdir(self.args['work-dir'])
283
+ util.debug(f'create_work_dir: created {self.args["work-dir"]}')
257
284
  if (self.args['keep']):
258
285
  open(keep_file, 'w').close()
259
- util.debug(f'create_work_dir: created {keep_file}')
260
- util.info(f'create_work_dir: created {self.args["work-dir"]}')
286
+ util.debug(f'create_work_dir: created {keep_file=}')
261
287
  return self.args['work-dir']
262
288
 
263
289
  def exec(self, work_dir, command_list, background=False, stop_on_error=True,
@@ -550,7 +576,7 @@ class Command:
550
576
  self.set_tool_defines()
551
577
 
552
578
 
553
- def help(self, tokens: list = []):
579
+ def help(self, tokens: list = []) -> None:
554
580
  '''Since we don't quite follow standard argparger help()/usage(), we'll format our own
555
581
 
556
582
  if self.args_help has additional help information.
@@ -569,8 +595,13 @@ class Command:
569
595
 
570
596
  print_base_help()
571
597
  lines = []
598
+ if not self.args:
599
+ print(f'Unparsed args: {tokens}')
600
+ return
601
+
572
602
  if self.command_name:
573
- lines.append(f"Generic help for command='{self.command_name}' (using '{self.__class__.__name__}')")
603
+ lines.append(f"Generic help for command='{self.command_name}'"
604
+ f" (using '{self.__class__.__name__}')")
574
605
  else:
575
606
  lines.append(f"Generic help (from class Command):")
576
607
 
@@ -752,7 +783,7 @@ class CommandDesign(Command):
752
783
  # token = '\'+define+OC_ROOT="/foo/bar/opencos"\''
753
784
  # So we strip all outer ' or " on the plusarg:
754
785
  plusarg = util.strip_outer_quotes(plusarg)
755
- if pwd is None:
786
+ if not pwd:
756
787
  pwd = ''
757
788
 
758
789
  if plusarg.startswith('+define+'):
@@ -1356,6 +1387,8 @@ class CommandParallel(Command):
1356
1387
  fancy_mode = util.args['fancy'] and (num_parallel > 1) and (num_parallel <= (lines-6))
1357
1388
  multi_cwd = util.getcwd() + os.sep
1358
1389
 
1390
+ self.patch_jobs_for_duplicate_target_names()
1391
+
1359
1392
  if run_parallel:
1360
1393
  # we are doing this multi-threaded
1361
1394
  util.info(f"Parallel: Running multi-threaded, starting {num_parallel} workers")
@@ -1514,6 +1547,10 @@ class CommandParallel(Command):
1514
1547
  self.status = 0 if len(self.jobs_status) == 0 else max(self.jobs_status)
1515
1548
  util.fancy_stop()
1516
1549
 
1550
+ @staticmethod
1551
+ def get_name_from_target(target: str) -> str:
1552
+ return target.replace('../', '').lstrip('./')
1553
+
1517
1554
 
1518
1555
  def update_args_list(self, args: list, tool: str) -> None:
1519
1556
  '''Modfies list args, using allow-listed known top-level args:
@@ -1567,3 +1604,58 @@ class CommandParallel(Command):
1567
1604
  # Remove unparsed args starting with '+', since those are commonly sent downstream to
1568
1605
  # single job (example, CommandSim plusargs).
1569
1606
  return [x for x in single_cmd_unparsed if not x.startswith('+')]
1607
+
1608
+
1609
+ def patch_jobs_for_duplicate_target_names(self) -> None:
1610
+ '''Examines list self.jobs, and if leaf target names are duplicate will
1611
+ patch each command's job-name to:
1612
+ --job-name=path.leaf.command[.tool]
1613
+ '''
1614
+
1615
+ def get_job_name(job_dict: dict) -> str:
1616
+ '''Fishes the job-name out of an entry in self.jobs'''
1617
+ for i, item in enumerate(job_dict['command_list']):
1618
+ if item.startswith('--job-name='):
1619
+ _, name = item.split('--job-name=')
1620
+ return name
1621
+ elif item == '--job-name':
1622
+ return job_dict['command_list'][i + 1]
1623
+ return ''
1624
+
1625
+ def replace_job_name(job_dict: dict, new_job_name: str) -> dict:
1626
+ '''Replaces the job-name in an entry in self.jobs'''
1627
+ for i, item in enumerate(job_dict['command_list']):
1628
+ if item.startswith('--job-name='):
1629
+ job_dict['command_list'][i] = '--job-name=' + new_job_name
1630
+ return job_dict
1631
+ elif item == '--job-name':
1632
+ job_dict['command_list'][i + 1] = new_job_name
1633
+ return job_dict
1634
+ return job_dict
1635
+
1636
+
1637
+ job_names_count_dict = {}
1638
+ for job_dict in self.jobs:
1639
+
1640
+ key = get_job_name(job_dict)
1641
+ if not key:
1642
+ self.error(f'{job_dict=} needs to have a --job-name= arg attached')
1643
+ if key not in job_names_count_dict:
1644
+ job_names_count_dict[key] = 1
1645
+ else:
1646
+ job_names_count_dict[key] += 1
1647
+
1648
+ for i, job_dict in enumerate(self.jobs):
1649
+ key = get_job_name(job_dict)
1650
+ if job_names_count_dict[key] < 2:
1651
+ continue
1652
+
1653
+ tpath, _ = os.path.split(job_dict['target'])
1654
+
1655
+ # prepend path information to job-name:
1656
+ patched_sub_work_dir = False
1657
+ patched_target_path = os.path.relpath(tpath).replace(os.sep, '_')
1658
+ new_job_name = f'{patched_target_path}.{key}'
1659
+ new_job_dict = replace_job_name(job_dict, new_job_name)
1660
+ self.jobs[i] = new_job_dict
1661
+ util.debug(f'Patched job {job_dict["name"]}: --job-name={new_job_name}')
opencos/eda_config.py CHANGED
@@ -15,9 +15,11 @@ class Defaults:
15
15
  home_override_config_yml = os.path.join(
16
16
  os.environ.get('HOME', ''), '.opencos-eda', 'EDA_CONFIG.yml'
17
17
  )
18
- config_yml = 'eda_config_defaults.yml'
18
+ opencos_config_yml = 'eda_config_defaults.yml'
19
+ config_yml = ''
19
20
 
20
21
  supported_config_keys = set([
22
+ 'DEFAULT_HANDLERS',
21
23
  'defines',
22
24
  'dep_command_enables',
23
25
  'dep_tags_enables',
@@ -26,10 +28,11 @@ class Defaults:
26
28
  'bare_plusarg_supported',
27
29
  'dep_sub',
28
30
  'vars',
29
- 'tools',
30
- 'auto_tools_order',
31
31
  'file_extensions',
32
32
  'command_determines_tool',
33
+ 'command_uses_no_tools',
34
+ 'tools',
35
+ 'auto_tools_order',
33
36
  ])
34
37
  supported_config_auto_tools_order_keys = set([
35
38
  'exe', 'handlers', 'requires_env', 'requires_py', 'requires_cmd',
@@ -53,6 +56,14 @@ class Defaults:
53
56
  ])
54
57
 
55
58
 
59
+ if os.path.exists(Defaults.environ_override_config_yml):
60
+ Defaults.config_yml = Defaults.environ_override_config_yml
61
+ elif os.path.exists(Defaults.home_override_config_yml):
62
+ Defaults.config_yml = Defaults.home_override_config_yml
63
+ else:
64
+ Defaults.config_yml = Defaults.opencos_config_yml
65
+
66
+
56
67
  def find_eda_config_yml_fpath(filename:str, package_search_only=False, package_search_enabled=True) -> str:
57
68
  '''Locates the filename (.yml) either from fullpath provided or from the sys.path
58
69
  opencos package paths.'''
@@ -192,15 +203,9 @@ def get_config_merged_with_defaults(config:dict) -> dict:
192
203
  return default_config
193
204
 
194
205
  def get_argparser() -> argparse.ArgumentParser:
195
- if os.path.exists(Defaults.environ_override_config_yml):
196
- default_config_yml = Defaults.environ_override_config_yml
197
- elif os.path.exists(Defaults.home_override_config_yml):
198
- default_config_yml = Defaults.home_override_config_yml
199
- else:
200
- default_config_yml = Defaults.config_yml
201
206
  parser = argparse.ArgumentParser(prog='opencos eda config options', add_help=False, allow_abbrev=False)
202
- parser.add_argument('--config-yml', type=str, default=default_config_yml,
203
- help=f'YAML filename to use for configuration (default {default_config_yml})')
207
+ parser.add_argument('--config-yml', type=str, default=Defaults.config_yml,
208
+ help=f'YAML filename to use for configuration (default {Defaults.config_yml})')
204
209
  return parser
205
210
 
206
211
  def get_argparser_short_help() -> str:
@@ -213,6 +218,8 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
213
218
 
214
219
  Handles args for:
215
220
  --config-yml=<YAMLFILE>
221
+
222
+ This will merge the result with the default config (if overriden)
216
223
  '''
217
224
 
218
225
  parser = get_argparser()
@@ -237,5 +244,7 @@ def get_eda_config(args:list, quiet=False) -> (dict, list):
237
244
  else:
238
245
  config = None
239
246
 
247
+ if parsed.config_yml != Defaults.config_yml:
248
+ config = get_config_merged_with_defaults(config)
240
249
 
241
250
  return config, unparsed