siliconcompiler 0.35.3__py3-none-any.whl → 0.35.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. siliconcompiler/_metadata.py +1 -1
  2. siliconcompiler/apps/sc_issue.py +18 -2
  3. siliconcompiler/checklist.py +2 -1
  4. siliconcompiler/constraints/asic_component.py +49 -11
  5. siliconcompiler/constraints/asic_floorplan.py +23 -21
  6. siliconcompiler/constraints/asic_pins.py +55 -17
  7. siliconcompiler/constraints/asic_timing.py +53 -22
  8. siliconcompiler/constraints/fpga_timing.py +5 -6
  9. siliconcompiler/data/templates/replay/replay.sh.j2 +27 -14
  10. siliconcompiler/package/__init__.py +17 -6
  11. siliconcompiler/project.py +9 -1
  12. siliconcompiler/scheduler/docker.py +24 -25
  13. siliconcompiler/scheduler/scheduler.py +82 -68
  14. siliconcompiler/scheduler/schedulernode.py +133 -20
  15. siliconcompiler/scheduler/slurm.py +113 -29
  16. siliconcompiler/scheduler/taskscheduler.py +0 -7
  17. siliconcompiler/schema/editableschema.py +29 -0
  18. siliconcompiler/schema/parametervalue.py +14 -2
  19. siliconcompiler/schema_support/option.py +82 -1
  20. siliconcompiler/schema_support/pathschema.py +7 -13
  21. siliconcompiler/tool.py +47 -25
  22. siliconcompiler/tools/klayout/__init__.py +3 -0
  23. siliconcompiler/tools/klayout/scripts/klayout_convert_drc_db.py +1 -0
  24. siliconcompiler/tools/klayout/scripts/klayout_export.py +1 -0
  25. siliconcompiler/tools/klayout/scripts/klayout_operations.py +1 -0
  26. siliconcompiler/tools/klayout/scripts/klayout_show.py +1 -0
  27. siliconcompiler/tools/klayout/scripts/klayout_utils.py +3 -4
  28. siliconcompiler/tools/openroad/__init__.py +27 -1
  29. siliconcompiler/tools/openroad/_apr.py +81 -4
  30. siliconcompiler/tools/openroad/clock_tree_synthesis.py +1 -0
  31. siliconcompiler/tools/openroad/global_placement.py +1 -0
  32. siliconcompiler/tools/openroad/init_floorplan.py +116 -7
  33. siliconcompiler/tools/openroad/power_grid_analysis.py +174 -0
  34. siliconcompiler/tools/openroad/repair_design.py +1 -0
  35. siliconcompiler/tools/openroad/repair_timing.py +1 -0
  36. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +1 -1
  37. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +42 -4
  38. siliconcompiler/tools/openroad/scripts/apr/sc_irdrop.tcl +146 -0
  39. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +1 -1
  40. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +4 -6
  41. siliconcompiler/tools/openroad/scripts/common/procs.tcl +1 -1
  42. siliconcompiler/tools/openroad/scripts/common/reports.tcl +1 -1
  43. siliconcompiler/tools/openroad/scripts/rcx/sc_rcx_bench.tcl +2 -4
  44. siliconcompiler/tools/opensta/__init__.py +1 -1
  45. siliconcompiler/tools/opensta/scripts/sc_timing.tcl +17 -12
  46. siliconcompiler/tools/vivado/scripts/sc_bitstream.tcl +11 -0
  47. siliconcompiler/tools/vivado/scripts/sc_place.tcl +11 -0
  48. siliconcompiler/tools/vivado/scripts/sc_route.tcl +11 -0
  49. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +10 -0
  50. siliconcompiler/tools/vpr/__init__.py +28 -0
  51. siliconcompiler/tools/yosys/scripts/sc_screenshot.tcl +1 -1
  52. siliconcompiler/tools/yosys/scripts/sc_synth_asic.tcl +40 -4
  53. siliconcompiler/tools/yosys/scripts/sc_synth_fpga.tcl +15 -5
  54. siliconcompiler/tools/yosys/syn_asic.py +42 -0
  55. siliconcompiler/tools/yosys/syn_fpga.py +8 -0
  56. siliconcompiler/toolscripts/_tools.json +6 -6
  57. siliconcompiler/utils/__init__.py +243 -51
  58. siliconcompiler/utils/curation.py +89 -56
  59. siliconcompiler/utils/issue.py +6 -1
  60. siliconcompiler/utils/multiprocessing.py +35 -2
  61. siliconcompiler/utils/paths.py +21 -0
  62. siliconcompiler/utils/settings.py +141 -0
  63. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/METADATA +4 -3
  64. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/RECORD +68 -65
  65. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/WHEEL +0 -0
  66. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/entry_points.txt +0 -0
  67. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/licenses/LICENSE +0 -0
  68. {siliconcompiler-0.35.3.dist-info → siliconcompiler-0.35.4.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "openroad": {
3
3
  "git-url": "https://github.com/The-OpenROAD-Project/OpenROAD.git",
4
- "git-commit": "f22f811d9d338b1dbc6fe854d35b53757596449d",
4
+ "git-commit": "dd56d50c413ecd215117898437c57bec68b59a87",
5
5
  "docker-cmds": [
6
6
  "# Remove OR-Tools files",
7
7
  "RUN rm -f $SC_PREFIX/Makefile $SC_PREFIX/README.md",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "opensta": {
19
19
  "git-url": "https://github.com/parallaxsw/OpenSTA.git",
20
- "git-commit": "cf903f4db688e6f89d732ef28cce7b8185587201",
20
+ "git-commit": "bd3efdc322c0677eb8e3d76f22ab297f7a6048b9",
21
21
  "auto-update": true
22
22
  },
23
23
  "netgen": {
@@ -41,7 +41,7 @@
41
41
  "auto-update": false
42
42
  },
43
43
  "klayout": {
44
- "version": "0.30.4",
44
+ "version": "0.30.5",
45
45
  "git-url": "https://github.com/KLayout/klayout.git",
46
46
  "auto-update": true,
47
47
  "run-version": "source version.sh && echo $KLAYOUT_VERSION",
@@ -55,7 +55,7 @@
55
55
  },
56
56
  "sv2v": {
57
57
  "git-url": "https://github.com/zachjs/sv2v.git",
58
- "git-commit": "c1ce7d067b0a7edf8d1589dcb5731265c854c490",
58
+ "git-commit": "d3812098d8feb2d04cc4cb55e034a1f2338db02f",
59
59
  "auto-update": true
60
60
  },
61
61
  "verilator": {
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "yosys": {
103
103
  "git-url": "https://github.com/YosysHQ/yosys.git",
104
- "git-commit": "v0.58",
104
+ "git-commit": "v0.60",
105
105
  "version-prefix": "",
106
106
  "auto-update": true
107
107
  },
@@ -145,7 +145,7 @@
145
145
  },
146
146
  "yosys-slang": {
147
147
  "git-url": "https://github.com/povik/yosys-slang.git",
148
- "git-commit": "f44908907726e684965ba71959cc147df50c0357",
148
+ "git-commit": "875539b8cae5a2ac7c86fee43b1e38a743ee8659",
149
149
  "docker-depends": "yosys",
150
150
  "auto-update": true
151
151
  },
@@ -28,6 +28,14 @@ if TYPE_CHECKING:
28
28
 
29
29
 
30
30
  def link_symlink_copy(srcfile, dstfile):
31
+ """
32
+ Attempts to link a source file to a destination using hard link, symbolic link,
33
+ or copy, in that order.
34
+
35
+ Args:
36
+ srcfile (str): Path to the source file.
37
+ dstfile (str): Path to the destination file.
38
+ """
31
39
  # first try hard linking, then symbolic linking,
32
40
  # and finally just copy the file
33
41
  for method in [os.link, os.symlink, shutil.copy2]:
@@ -40,6 +48,14 @@ def link_symlink_copy(srcfile, dstfile):
40
48
 
41
49
 
42
50
  def link_copy(srcfile, dstfile):
51
+ """
52
+ Attempts to link a source file to a destination using hard link or copy,
53
+ in that order.
54
+
55
+ Args:
56
+ srcfile (str): Path to the source file.
57
+ dstfile (str): Path to the destination file.
58
+ """
43
59
  # first try hard linking or just copy the file
44
60
  for method in [os.link, shutil.copy2]:
45
61
  try:
@@ -51,7 +67,15 @@ def link_copy(srcfile, dstfile):
51
67
 
52
68
 
53
69
  def get_file_ext(filename: str) -> str:
54
- '''Get base file extension for a given path, disregarding .gz.'''
70
+ """
71
+ Get base file extension for a given path, disregarding .gz.
72
+
73
+ Args:
74
+ filename (str): The filename to extract the extension from.
75
+
76
+ Returns:
77
+ str: The file extension (e.g., 'v', 'py') without the dot.
78
+ """
55
79
  if filename.lower().endswith('.gz'):
56
80
  filename = os.path.splitext(filename)[0]
57
81
  filetype = os.path.splitext(filename)[1].lower().lstrip('.')
@@ -60,7 +84,10 @@ def get_file_ext(filename: str) -> str:
60
84
 
61
85
  def get_default_iomap() -> Dict[str, str]:
62
86
  """
63
- Default input file map for SC with filesets and extensions
87
+ Returns the default input file map for SC with filesets and extensions.
88
+
89
+ Returns:
90
+ Dict[str, str]: A dictionary mapping file extensions to their SiliconCompiler type.
64
91
  """
65
92
 
66
93
  # Record extensions:
@@ -175,26 +202,71 @@ def get_default_iomap() -> Dict[str, str]:
175
202
  return default_iomap
176
203
 
177
204
 
205
+ def default_sc_dir() -> str:
206
+ """
207
+ Returns the default SiliconCompiler configuration directory.
208
+
209
+ Returns:
210
+ str: The absolute path to the .sc directory in the user's home folder.
211
+ """
212
+ return os.path.join(Path.home(), '.sc')
213
+
214
+
215
+ def default_sc_path(path: str) -> str:
216
+ """
217
+ Returns a path relative to the default SiliconCompiler configuration directory.
218
+
219
+ Args:
220
+ path (str): The relative path to append to the default SC directory.
221
+
222
+ Returns:
223
+ str: The absolute path joined with the default SC directory.
224
+ """
225
+ return os.path.join(default_sc_dir(), path)
226
+
227
+
178
228
  def default_credentials_file() -> str:
179
- cfg_file = os.path.join(Path.home(), '.sc', 'credentials')
229
+ """
230
+ Returns the default path for the SiliconCompiler credentials file.
180
231
 
181
- return cfg_file
232
+ Returns:
233
+ str: The absolute path to the credentials file.
234
+ """
235
+ return default_sc_path('credentials')
182
236
 
183
237
 
184
238
  def default_cache_dir() -> str:
185
- cfg_file = os.path.join(Path.home(), '.sc', 'cache')
239
+ """
240
+ Returns the default path for the SiliconCompiler cache directory.
186
241
 
187
- return cfg_file
242
+ Returns:
243
+ str: The absolute path to the cache directory.
244
+ """
245
+ return default_sc_path('cache')
188
246
 
189
247
 
190
248
  def default_email_credentials_file() -> str:
191
- cfg_file = os.path.join(Path.home(), '.sc', 'email.json')
249
+ """
250
+ Returns the default path for the SiliconCompiler email credentials file.
192
251
 
193
- return cfg_file
252
+ Returns:
253
+ str: The absolute path to the email.json file.
254
+ """
255
+ return default_sc_path('email.json')
194
256
 
195
257
 
196
258
  @contextlib.contextmanager
197
259
  def sc_open(path: str, *args, **kwargs):
260
+ """
261
+ A context manager for opening files with default settings convenient for SC.
262
+
263
+ Sets ``errors='ignore'`` and ``newline='\n'`` by default unless overridden.
264
+
265
+ Args:
266
+ path (str): The file path to open.
267
+ *args: Positional arguments passed to the builtin open().
268
+ **kwargs: Keyword arguments passed to the builtin open().
269
+ """
198
270
  if 'errors' not in kwargs:
199
271
  kwargs['errors'] = 'ignore'
200
272
  kwargs["newline"] = "\n"
@@ -212,6 +284,17 @@ def get_file_template(path: str,
212
284
  os.path.dirname(os.path.abspath(__file__))),
213
285
  'data',
214
286
  'templates')) -> Template:
287
+ """
288
+ Retrieves a Jinja2 template object for the specified file.
289
+
290
+ Args:
291
+ path (str): The name of the template file or an absolute path to it.
292
+ root (str, optional): The root directory to search for templates if path
293
+ is relative. Defaults to the SiliconCompiler data/templates directory.
294
+
295
+ Returns:
296
+ Template: The loaded Jinja2 Template object.
297
+ """
215
298
  if os.path.isabs(path):
216
299
  root = os.path.dirname(path)
217
300
  path = os.path.basename(path)
@@ -225,6 +308,20 @@ def get_file_template(path: str,
225
308
 
226
309
  #######################################
227
310
  def safecompare(value: Union[int, float], op: str, goal: Union[int, float]) -> bool:
311
+ """
312
+ Compares a value against a goal using a string operator.
313
+
314
+ Args:
315
+ value (Union[int, float]): The value to compare.
316
+ op (str): The comparison operator ('>', '>=', '<', '<=', '==', '!=').
317
+ goal (Union[int, float]): The target value to compare against.
318
+
319
+ Returns:
320
+ bool: The result of the comparison.
321
+
322
+ Raises:
323
+ ValueError: If the provided operator is not supported.
324
+ """
228
325
  # supported relational operations
229
326
  # >, >=, <=, <. ==, !=
230
327
  if op == ">":
@@ -244,75 +341,127 @@ def safecompare(value: Union[int, float], op: str, goal: Union[int, float]) -> b
244
341
 
245
342
 
246
343
  ###########################################################################
247
- def grep(project: "Project", args: str, line: str) -> Union[None, str]:
344
+ def grep(logger: logging.Logger, args: str, line: str) -> Union[None, str]:
248
345
  """
249
346
  Emulates the Unix grep command on a string.
250
347
 
251
- Emulates the behavior of the Unix grep command that is etched into
252
- our muscle memory. Partially implemented, not all features supported.
253
- The function returns None if no match is found.
254
-
255
348
  Args:
256
- arg (string): Command line arguments for grep command
257
- line (string): Line to process
349
+ logger (logging.Logger): used for logging errors.
350
+ args (str): Command line arguments for grep command.
351
+ line (str): Line to process.
258
352
 
259
353
  Returns:
260
- Result of grep command (string).
261
-
354
+ Union[None, str]: Result of grep command (string) or None if no match.
262
355
  """
263
356
 
264
357
  # Quick return if input is None
265
358
  if line is None:
266
359
  return None
267
360
 
268
- # Partial list of supported grep options
361
+ # --- 1. Initialize Options and Parse Arguments ---
269
362
  options = {
270
363
  '-v': False, # Invert the sense of matching
271
- '-i': False, # Ignore case distinctions in patterns and data
272
- '-E': False, # Interpret PATTERNS as extended regular expressions.
273
- '-e': False, # Safe interpretation of pattern starting with "-"
274
- '-x': False, # Select only matches that exactly match the whole line.
275
- '-o': False, # Print only the match parts of a matching line
276
- '-w': False} # Select only lines containing matches that form whole words.
364
+ '-i': False, # Ignore case distinctions
365
+ '-E': False, # Extended regular expressions (Python's 're' module is ERE by default)
366
+ '-e': False, # Pattern starts with '-' (simplified logic)
367
+ '-x': False, # Exact line match
368
+ '-o': False, # Print only the match
369
+ '-w': False} # Whole word match
370
+
371
+ parts = args.split()
372
+ pattern = ""
373
+ pattern_start_index = -1
374
+
375
+ # Identify switches and the start of the pattern
376
+ for i, part in enumerate(parts):
377
+ # Check for switch starting with '-' and not just '-'
378
+ if part.startswith('-') and len(part) > 1 and part != '-e':
379
+ # Handle concatenated switches (e.g., -vi)
380
+ is_valid_switch_group = True
381
+ for char in part[1:]:
382
+ switch = f"-{char}"
383
+ if switch in options:
384
+ options[switch] = True
385
+ else:
386
+ logger.error(f"Unknown switch: {switch}")
387
+ is_valid_switch_group = False
388
+ break
389
+ if not is_valid_switch_group:
390
+ # If an invalid switch was found, the rest must be the pattern
391
+ pattern_start_index = i
392
+ break
393
+ elif part == '-e':
394
+ # The next part is the pattern, regardless of what it looks like
395
+ options['-e'] = True
396
+ if i + 1 < len(parts):
397
+ pattern_start_index = i + 1
398
+ break
399
+ elif not pattern.strip():
400
+ # First non-switch part is the start of the pattern
401
+ pattern_start_index = i
402
+ break
277
403
 
278
- # Split into repeating switches and everything else
279
- match = re.match(r'\s*((?:\-\w\s)*)(.*)', args)
404
+ # Assemble the pattern from the determined starting index
405
+ if pattern_start_index != -1:
406
+ pattern = " ".join(parts[pattern_start_index:])
280
407
 
281
- if not match:
408
+ if not pattern:
282
409
  return None
283
410
 
284
- pattern = match.group(2)
411
+ # --- 2. Prepare Regex Flags and Pattern Modifiers ---
285
412
 
286
- # Split space separated switch string into list
287
- switches = match.group(1).strip().split(' ')
413
+ regex_flags = 0
414
+ if options['-i']:
415
+ regex_flags |= re.IGNORECASE
288
416
 
289
- # Find special -e switch update the pattern
290
- for i in range(len(switches)):
291
- if switches[i] == "-e":
292
- if i != (len(switches)):
293
- pattern = ' '.join(switches[i + 1:]) + " " + pattern
294
- switches = switches[0:i + 1]
295
- break
296
- options["-e"] = True
297
- elif switches[i] in options.keys():
298
- options[switches[i]] = True
299
- elif switches[i] != '':
300
- project.logger.error(switches[i])
301
-
302
- # REGEX
303
- # TODO: add all the other optinos
304
- match = re.search(rf"({pattern})", line)
305
- if bool(match) == bool(options["-v"]):
417
+ # Apply Whole Word (-w)
418
+ pattern_to_search = pattern
419
+ if options['-w']:
420
+ # Apply word boundaries
421
+ pattern_to_search = rf"\b({pattern})\b"
422
+
423
+ # Apply Whole Line (-x)
424
+ if options['-x']:
425
+ # Exact line match, using the prepared pattern (which may have \b already)
426
+ pattern_to_search = rf"^{pattern_to_search}$"
427
+
428
+ # --- 3. Perform Search ---
429
+ try:
430
+ # re.search is used to find the pattern anywhere in the line
431
+ match = re.search(pattern_to_search, line, regex_flags)
432
+ except re.error as e:
433
+ # Handle cases where the pattern itself is invalid regex
434
+ logger.error(f"Invalid regex pattern '{pattern}': {e}")
306
435
  return None
436
+
437
+ # --- 4. Handle Inversion (-v) and Final Return ---
438
+
439
+ # Check if a result should be returned: (Match found) XOR (Invert is on)
440
+ should_return = bool(match) != options["-v"]
441
+
442
+ if should_return:
443
+ if options['-o'] and match:
444
+ # Print only the match part (which is match.group(0))
445
+ return match.group(0)
446
+ else:
447
+ # Return the whole line (standard behavior)
448
+ return line
307
449
  else:
308
- return line
450
+ # Match found AND inverted, OR Match NOT found AND not inverted
451
+ return None
309
452
 
310
453
 
311
454
  def get_plugins(system: str, name: Optional[str] = None) -> List[Callable]:
312
455
  '''
313
- Search for python modules with a specific function
314
- '''
456
+ Search for python modules with a specific function.
315
457
 
458
+ Args:
459
+ system (str): The system/group to search within (e.g. 'siliconcompiler.metrics').
460
+ name (str, optional): Specific plugin name to filter by.
461
+
462
+ Returns:
463
+ List[Callable]: A list of loaded plugin functions.
464
+ '''
316
465
  plugins = []
317
466
  discovered_plugins = entry_points(group=f'siliconcompiler.{system}')
318
467
  for plugin in discovered_plugins:
@@ -326,6 +475,19 @@ def get_plugins(system: str, name: Optional[str] = None) -> List[Callable]:
326
475
 
327
476
 
328
477
  def truncate_text(text: str, width: int) -> str:
478
+ """
479
+ Truncates text to a specific width, replacing the middle with '...'.
480
+
481
+ Attempts to preserve the end of the string if it appears to be a version number
482
+ or numeric index.
483
+
484
+ Args:
485
+ text (str): The text to truncate.
486
+ width (int): The maximum desired width of the text.
487
+
488
+ Returns:
489
+ str: The truncated text.
490
+ """
329
491
  width = max(width, 5)
330
492
 
331
493
  if len(text) <= width:
@@ -349,7 +511,10 @@ def get_cores(physical: bool = False) -> int:
349
511
  Get max number of cores for this machine.
350
512
 
351
513
  Args:
352
- physical (boolean): if true, only count physical cores
514
+ physical (bool): if true, only count physical cores. Defaults to False.
515
+
516
+ Returns:
517
+ int: The number of available cores. Defaults to 1 if detection fails.
353
518
  '''
354
519
 
355
520
  cores = psutil.cpu_count(logical=not physical)
@@ -367,6 +532,13 @@ def get_cores(physical: bool = False) -> int:
367
532
 
368
533
 
369
534
  def print_traceback(logger: logging.Logger, exception: Exception):
535
+ """
536
+ Prints the full traceback of an exception to the provided logger.
537
+
538
+ Args:
539
+ logger (logging.Logger): The logger instance to write the traceback to.
540
+ exception (Exception): The exception to log.
541
+ """
370
542
  logger.error(f'{exception}')
371
543
  trace = StringIO()
372
544
  traceback.print_tb(exception.__traceback__, file=trace)
@@ -376,6 +548,13 @@ def print_traceback(logger: logging.Logger, exception: Exception):
376
548
 
377
549
 
378
550
  class FilterDirectories:
551
+ """
552
+ A helper class to filter directories and files during file collection.
553
+
554
+ This class prevents collecting files from the home directory, the build directory,
555
+ and hidden files (on Linux, Windows, and macOS). It also enforces a limit on the
556
+ number of files collected.
557
+ """
379
558
  def __init__(self, project: "Project"):
380
559
  self.file_count = 0
381
560
  self.directory_file_limit = None
@@ -391,6 +570,19 @@ class FilterDirectories:
391
570
  return builddir(self.project)
392
571
 
393
572
  def filter(self, path: str, files: List[str]) -> List[str]:
573
+ """
574
+ Filters a list of files based on the current directory path.
575
+
576
+ Args:
577
+ path (str): The current directory path being traversed.
578
+ files (List[str]): The list of filenames in that directory.
579
+
580
+ Returns:
581
+ List[str]: A list of filenames that should be excluded (i.e., hidden files)
582
+ or handled specifically. Note that despite the name returning 'hidden_files',
583
+ the logic is essentially identifying what to *exclude* from the main processing
584
+ loop in some contexts, or identifying hidden files to ignore.
585
+ """
394
586
  if pathlib.Path(path) == pathlib.Path.home():
395
587
  # refuse to collect home directory
396
588
  self.logger.error(f'Cannot collect user home directory: {path}')
@@ -5,9 +5,10 @@ import os.path
5
5
 
6
6
  from typing import List, Optional, TYPE_CHECKING
7
7
 
8
+ from siliconcompiler.schema import BaseSchema, Parameter
8
9
  from siliconcompiler.schema.parametervalue import NodeListValue, NodeSetValue
9
10
  from siliconcompiler.utils import FilterDirectories
10
- from siliconcompiler.utils.paths import collectiondir
11
+ from siliconcompiler.utils.paths import collectiondir, cwdir
11
12
  from siliconcompiler.scheduler import SchedulerNode
12
13
  from siliconcompiler.flowgraph import RuntimeFlowgraph
13
14
 
@@ -43,16 +44,42 @@ def collect(project: "Project",
43
44
 
44
45
  if not directory:
45
46
  directory = collectiondir(project)
47
+ if not directory:
48
+ raise ValueError("unable to determine collection directory")
49
+
46
50
  directory = os.path.abspath(directory)
47
51
 
48
- # Remove existing directory
52
+ # Move existing directory
53
+ prev_dir = None
49
54
  if os.path.exists(directory):
50
- shutil.rmtree(directory)
55
+ prev_dir = os.path.join(os.path.dirname(directory), "sc_previous_collection")
56
+ os.rename(directory, prev_dir)
51
57
  os.makedirs(directory)
52
58
 
53
59
  if verbose:
54
60
  project.logger.info(f'Collecting files to: {directory}')
55
61
 
62
+ cwd = cwdir(project)
63
+
64
+ def find_files(*key, step: Optional[str] = None, index: Optional[str] = None):
65
+ """
66
+ Find the files in the filesystem, otherwise look in previous collection
67
+ """
68
+ e = None
69
+ try:
70
+ return BaseSchema._find_files(project, *key, step=step, index=index,
71
+ cwd=cwd,
72
+ collection_dir=directory)
73
+ except FileNotFoundError as err:
74
+ e = err
75
+ if prev_dir:
76
+ # Try previous location next
77
+ return BaseSchema._find_files(project, *key, step=step, index=index,
78
+ cwd=cwd,
79
+ collection_dir=prev_dir)
80
+ if e:
81
+ raise e from None
82
+
56
83
  dirs = {}
57
84
  files = {}
58
85
 
@@ -75,17 +102,18 @@ def collect(project: "Project",
75
102
  # skip flow files files from builds
76
103
  continue
77
104
 
78
- leaftype = project.get(*key, field='type')
105
+ param: Parameter = project.get(*key, field=None)
106
+ leaftype: str = param.get(field='type')
79
107
  is_dir = "dir" in leaftype
80
108
  is_file = "file" in leaftype
81
109
 
82
110
  if not is_dir and not is_file:
83
111
  continue
84
112
 
85
- if not project.get(*key, field='copy'):
113
+ if not param.get(field='copy'):
86
114
  continue
87
115
 
88
- for values, step, index in project.get(*key, field=None).getvalues(return_values=False):
116
+ for values, step, index in param.getvalues(return_values=False):
89
117
  if not values.has_value:
90
118
  continue
91
119
 
@@ -99,73 +127,78 @@ def collect(project: "Project",
99
127
  else:
100
128
  files[(key, step, index)] = values
101
129
 
102
- path_filter = FilterDirectories(project)
103
- for key, step, index in sorted(dirs.keys()):
104
- abs_paths = project.find_files(*key, step=step, index=index)
130
+ try:
131
+ path_filter = FilterDirectories(project)
132
+ for key, step, index in sorted(dirs.keys()):
133
+ abs_paths = find_files(*key, step=step, index=index)
105
134
 
106
- new_paths = set()
135
+ new_paths = set()
107
136
 
108
- if not isinstance(abs_paths, (list, tuple, set)):
109
- abs_paths = [abs_paths]
137
+ if not isinstance(abs_paths, (list, tuple, set)):
138
+ abs_paths = [abs_paths]
110
139
 
111
- abs_paths = zip(abs_paths, dirs[(key, step, index)])
112
- abs_paths = sorted(abs_paths, key=lambda p: p[0])
140
+ abs_paths = zip(abs_paths, dirs[(key, step, index)])
141
+ abs_paths = sorted(abs_paths, key=lambda p: p[0])
113
142
 
114
- for abs_path, value in abs_paths:
115
- if not abs_path:
116
- raise FileNotFoundError(f"{value.get()} could not be copied")
143
+ for abs_path, value in abs_paths:
144
+ if not abs_path:
145
+ raise FileNotFoundError(f"{value.get()} could not be copied")
117
146
 
118
- if abs_path.startswith(directory):
119
- # File already imported in directory
120
- continue
147
+ if abs_path.startswith(directory):
148
+ # File already imported in directory
149
+ continue
121
150
 
122
- imported = False
123
- for new_path in new_paths:
124
- if abs_path.startwith(new_path):
125
- imported = True
126
- break
127
- if imported:
128
- continue
151
+ imported = False
152
+ for new_path in new_paths:
153
+ if abs_path.startswith(new_path):
154
+ imported = True
155
+ break
156
+ if imported:
157
+ continue
129
158
 
130
- new_paths.add(abs_path)
159
+ new_paths.add(abs_path)
131
160
 
132
- import_path = os.path.join(directory, value.get_hashed_filename())
133
- if os.path.exists(import_path):
134
- continue
161
+ import_path = os.path.join(directory, value.get_hashed_filename())
162
+ if os.path.exists(import_path):
163
+ continue
135
164
 
136
- if whitelist is not None and abs_path not in whitelist:
137
- raise RuntimeError(f'{abs_path} is not on the approved collection list.')
165
+ if whitelist is not None and abs_path not in whitelist:
166
+ raise RuntimeError(f'{abs_path} is not on the approved collection list.')
138
167
 
139
- if verbose:
140
- project.logger.info(f" Collecting directory: {abs_path}")
141
- path_filter.abspath = abs_path
142
- shutil.copytree(abs_path, import_path, ignore=path_filter.filter)
143
- path_filter.abspath = None
168
+ if verbose:
169
+ project.logger.info(f" Collecting directory: {abs_path}")
170
+ path_filter.abspath = abs_path
171
+ shutil.copytree(abs_path, import_path, ignore=path_filter.filter)
172
+ path_filter.abspath = None
144
173
 
145
- for key, step, index in sorted(files.keys()):
146
- abs_paths = project.find_files(*key, step=step, index=index)
174
+ for key, step, index in sorted(files.keys()):
175
+ abs_paths = find_files(*key, step=step, index=index)
147
176
 
148
- if not isinstance(abs_paths, (list, tuple, set)):
149
- abs_paths = [abs_paths]
177
+ if not isinstance(abs_paths, (list, tuple, set)):
178
+ abs_paths = [abs_paths]
150
179
 
151
- abs_paths = zip(abs_paths, files[(key, step, index)])
152
- abs_paths = sorted(abs_paths, key=lambda p: p[0])
180
+ abs_paths = zip(abs_paths, files[(key, step, index)])
181
+ abs_paths = sorted(abs_paths, key=lambda p: p[0])
153
182
 
154
- for abs_path, value in abs_paths:
155
- if not abs_path:
156
- raise FileNotFoundError(f"{value.get()} could not be copied")
183
+ for abs_path, value in abs_paths:
184
+ if not abs_path:
185
+ raise FileNotFoundError(f"{value.get()} could not be copied")
157
186
 
158
- if abs_path.startswith(directory):
159
- # File already imported in directory
160
- continue
187
+ if abs_path.startswith(directory):
188
+ # File already imported in directory
189
+ continue
161
190
 
162
- import_path = os.path.join(directory, value.get_hashed_filename())
163
- if os.path.exists(import_path):
164
- continue
191
+ import_path = os.path.join(directory, value.get_hashed_filename())
192
+ if os.path.exists(import_path):
193
+ continue
165
194
 
166
- if verbose:
167
- project.logger.info(f" Collecting file: {abs_path}")
168
- shutil.copy2(abs_path, import_path)
195
+ if verbose:
196
+ project.logger.info(f" Collecting file: {abs_path}")
197
+ shutil.copy2(abs_path, import_path)
198
+ finally:
199
+ if prev_dir:
200
+ # Delete existing directory
201
+ shutil.rmtree(prev_dir)
169
202
 
170
203
 
171
204
  def archive(project: "Project",