labview-fpga-hdl-tools 2.0.0.dev2__tar.gz → 2.0.0.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/PKG-INFO +1 -1
  2. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/check_syntax.py +17 -24
  3. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/common.py +67 -22
  4. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/compile_modelsim_lib.py +19 -23
  5. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/compile_project.py +21 -28
  6. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/create_lvbitx.py +3 -2
  7. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/create_modelsim_project.py +3 -5
  8. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/create_vivado_project.py +35 -37
  9. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/gen_labview_target_plugin.py +19 -33
  10. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/generate_vhdl.py +15 -13
  11. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/get_window_netlist.py +15 -13
  12. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/install_dependencies.py +13 -3
  13. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/install_labview_target_plugin.py +7 -12
  14. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/launch_modelsim.py +9 -8
  15. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/launch_vivado.py +14 -9
  16. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/migrate_clip.py +20 -21
  17. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/sim_modelsim.py +3 -4
  18. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/pyproject.toml +1 -1
  19. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/LICENSE +0 -0
  20. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/README.md +0 -0
  21. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/.gitignore +0 -0
  22. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/__init__.py +0 -0
  23. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/__main__.py +0 -0
  24. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/command_config.py +0 -0
  25. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/command_hooks.py +0 -0
  26. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/nihdlsettings_default.py +0 -0
  27. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/process_constraints.py +0 -0
  28. {labview_fpga_hdl_tools-2.0.0.dev2 → labview_fpga_hdl_tools-2.0.0.dev3}/labview_fpga_hdl_tools/reporting.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: labview-fpga-hdl-tools
3
- Version: 2.0.0.dev2
3
+ Version: 2.0.0.dev3
4
4
  Summary: LabVIEW FPGA HDL Tools
5
5
  License: MIT
6
6
  Author: Salvador Santolucito
@@ -83,12 +83,9 @@ def _validate_ini(config):
83
83
  if invalid_path:
84
84
  invalid_paths.append(invalid_path)
85
85
 
86
- error_msg = common.get_missing_settings_error(missing_settings)
87
- error_msg += common.get_invalid_paths_error(invalid_paths)
88
-
89
- if missing_settings or invalid_paths:
90
- error_msg += "\nPlease update your configuration file and try again."
91
- raise ValueError(error_msg)
86
+ error = common.build_settings_error(missing_settings, invalid_paths)
87
+ if error:
88
+ raise ValueError(error)
92
89
 
93
90
 
94
91
  def _get_check_syntax_status_from_log(log_contents):
@@ -109,16 +106,7 @@ def _get_check_syntax_status_from_log(log_contents):
109
106
 
110
107
  def _run_check_syntax(config, generated_tcl_path):
111
108
  """Run the generated check-syntax TCL script in Vivado batch mode."""
112
- vivado_executable = common.get_vivado_executable(config.vivado_tools_folder)
113
- if not vivado_executable:
114
- raise ValueError("VivadoToolsFolder setting is missing from configuration")
115
-
116
- vivado_abs = os.path.abspath(vivado_executable)
117
- if not os.path.exists(vivado_abs):
118
- raise FileNotFoundError(
119
- f"Vivado executable not found at: {vivado_abs}\n"
120
- f"Please check your --vivado argument or VivadoToolsFolder setting in nihdlsettings.py"
121
- )
109
+ vivado_abs = common.resolve_vivado_executable_abs(config)
122
110
 
123
111
  vivado_project_dir = os.path.join(os.getcwd(), config.vivado_project_folder)
124
112
  log_path = os.path.join(vivado_project_dir, "check_syntax.log")
@@ -128,19 +116,24 @@ def _run_check_syntax(config, generated_tcl_path):
128
116
  if os.path.exists(path):
129
117
  os.remove(path)
130
118
 
131
- command = (
132
- f'"{vivado_abs}" -mode batch '
133
- f'-source "{generated_tcl_path}" '
134
- f'-log "{log_path}" '
135
- f'-journal "{journal_path}"'
136
- )
119
+ command = [
120
+ vivado_abs,
121
+ "-mode",
122
+ "batch",
123
+ "-source",
124
+ generated_tcl_path,
125
+ "-log",
126
+ log_path,
127
+ "-journal",
128
+ journal_path,
129
+ ]
137
130
 
138
131
  reporter.detail(f"Generated TCL script: {generated_tcl_path}")
139
132
  reporter.detail(f"Vivado executable: {vivado_abs}")
140
133
  reporter.detail(f"Working directory: {vivado_project_dir}")
141
- reporter.detail(f"Running command: {command}")
134
+ reporter.detail(f"Running command: {' '.join(command)}")
142
135
 
143
- result = subprocess.run(command, cwd=vivado_project_dir, shell=True, check=False)
136
+ result = subprocess.run(command, cwd=vivado_project_dir, check=False)
144
137
 
145
138
  if not os.path.exists(log_path):
146
139
  raise RuntimeError("Vivado check-syntax log file was not created.")
@@ -237,7 +237,8 @@ def _parse_vhdl_entity(vhdl_path):
237
237
 
238
238
  except Exception as e:
239
239
  reporter.error(f"Error parsing VHDL file: {str(e)}")
240
- traceback.print_exc()
240
+ if reporter.verbose:
241
+ traceback.print_exc()
241
242
  return None, []
242
243
 
243
244
 
@@ -406,10 +407,16 @@ def apply_hdl_excludes(file_list, excluded_abspaths):
406
407
 
407
408
 
408
409
  def run_command(cmd, cwd=None, capture_output=True, check=False):
409
- """Run a shell command and return its output.
410
+ """Run a command and return its output.
411
+
412
+ The command is given as a pre-tokenized argument list (for example
413
+ ``[vivado_abs, "-mode", "batch", ...]``) and is executed directly without a
414
+ shell. Passing an argument list means arguments containing spaces or
415
+ shell-significant characters are passed through verbatim (no quoting bugs)
416
+ and there is no shell-injection surface.
410
417
 
411
418
  Args:
412
- cmd: Command string to run through the shell.
419
+ cmd: The command to run, as a list of arguments.
413
420
  cwd: Working directory to run the command in, if any.
414
421
  capture_output: When True, capture stdout and return it as a string.
415
422
  When False, let the command's output stream to the console and
@@ -421,7 +428,7 @@ def run_command(cmd, cwd=None, capture_output=True, check=False):
421
428
  The captured stdout (stripped) when ``capture_output`` is True, otherwise
422
429
  an empty string.
423
430
  """
424
- reporter.detail(f"Running command: {cmd}")
431
+ reporter.detail(f"Running command: {' '.join(str(arg) for arg in cmd)}")
425
432
 
426
433
  kwargs = {}
427
434
  if cwd:
@@ -429,14 +436,12 @@ def run_command(cmd, cwd=None, capture_output=True, check=False):
429
436
 
430
437
  if capture_output:
431
438
  # Capture and return output
432
- result = subprocess.run(
433
- cmd, shell=True, text=True, capture_output=True, check=check, **kwargs
434
- )
439
+ result = subprocess.run(cmd, text=True, capture_output=True, check=check, **kwargs)
435
440
  # Check if stdout is None before calling strip()
436
441
  return result.stdout.strip() if result.stdout is not None else ""
437
442
  else:
438
443
  # Don't capture output (let it go to console)
439
- subprocess.run(cmd, shell=True, check=check, **kwargs)
444
+ subprocess.run(cmd, check=check, **kwargs)
440
445
  return "" # Return empty string instead of None
441
446
 
442
447
 
@@ -495,6 +500,48 @@ def get_invalid_paths_error(invalid_paths):
495
500
  return error_msg
496
501
 
497
502
 
503
+ def build_settings_error(missing_settings, invalid_paths):
504
+ """Return the standard missing-settings / invalid-paths message, or "" if none.
505
+
506
+ Combines the missing-settings and invalid-path messages with the shared hint
507
+ used by every ``_validate_ini``. Returns an empty string when there is
508
+ nothing to report, so callers can do ``if error: raise ValueError(error)``
509
+ (or report-and-return) without repeating the concatenation and hint text.
510
+ """
511
+ if not (missing_settings or invalid_paths):
512
+ return ""
513
+ return (
514
+ get_missing_settings_error(missing_settings)
515
+ + get_invalid_paths_error(invalid_paths)
516
+ + "\nPlease update your configuration file and try again."
517
+ )
518
+
519
+
520
+ def resolve_vivado_executable_abs(config):
521
+ """Resolve the Vivado executable to an absolute path.
522
+
523
+ Args:
524
+ config: CommandConfiguration providing ``vivado_tools_folder``.
525
+
526
+ Returns:
527
+ str: Absolute path to the Vivado executable.
528
+
529
+ Raises:
530
+ ValueError: if the executable cannot be resolved from the configuration.
531
+ FileNotFoundError: if the resolved executable does not exist on disk.
532
+ """
533
+ vivado_executable = get_vivado_executable(config.vivado_tools_folder)
534
+ if not vivado_executable:
535
+ raise ValueError("VivadoToolsFolder setting is missing from configuration")
536
+ vivado_abs = os.path.abspath(vivado_executable)
537
+ if not os.path.exists(vivado_abs):
538
+ raise FileNotFoundError(
539
+ f"Vivado executable not found at: {vivado_abs}\n"
540
+ "Please check your VivadoToolsFolder setting in nihdlsettings.py"
541
+ )
542
+ return vivado_abs
543
+
544
+
498
545
  def collect_missing_settings(config, required):
499
546
  """Return the labels of required settings that are unset (falsy) on config.
500
547
 
@@ -517,23 +564,21 @@ def collect_missing_settings(config, required):
517
564
  def raise_for_missing_settings(missing_settings, invalid_paths):
518
565
  """Raise ``ValueError`` for missing settings or invalid paths, if any.
519
566
 
520
- Missing settings take precedence over invalid paths. Produces the standard
521
- launch-style messages shared by the launch commands. Does nothing when both
522
- lists are empty.
567
+ Missing settings take precedence over invalid paths (only one message is
568
+ raised). Uses the shared ``get_missing_settings_error`` /
569
+ ``get_invalid_paths_error`` builders so the wording matches the
570
+ ``_validate_ini`` validators. Does nothing when both lists are empty.
523
571
  """
524
572
  if missing_settings:
525
- error_msg = "Missing required configuration settings:\n"
526
- for setting in missing_settings:
527
- error_msg += f" - {setting}\n"
528
- error_msg += "\nCheck your nihdlsettings.py file and try again."
529
- raise ValueError(error_msg)
530
-
573
+ raise ValueError(
574
+ get_missing_settings_error(missing_settings)
575
+ + "\nPlease update your configuration file and try again."
576
+ )
531
577
  if invalid_paths:
532
- error_msg = "The following settings have invalid paths:\n"
533
- for path in invalid_paths:
534
- error_msg += f" - {path}\n"
535
- error_msg += "\nCheck your configuration and try again."
536
- raise ValueError(error_msg)
578
+ raise ValueError(
579
+ get_invalid_paths_error(invalid_paths)
580
+ + "\nPlease update your configuration file and try again."
581
+ )
537
582
 
538
583
 
539
584
  def generate_guid():
@@ -69,11 +69,9 @@ def _validate_ini(config):
69
69
  f"{config.modelsim_tools_folder}"
70
70
  )
71
71
 
72
- error_msg = common.get_missing_settings_error(missing_settings)
73
- error_msg += common.get_invalid_paths_error(invalid_paths)
74
- if missing_settings or invalid_paths:
75
- error_msg += "\nPlease update your configuration file and try again."
76
- raise ValueError(error_msg)
72
+ error = common.build_settings_error(missing_settings, invalid_paths)
73
+ if error:
74
+ raise ValueError(error)
77
75
 
78
76
 
79
77
  def _libraries_already_built(out_folder, libraries):
@@ -134,16 +132,7 @@ def _is_progress_line(line):
134
132
 
135
133
  def _run_compile_simlib(config, tcl_path, out_folder, libraries):
136
134
  """Run the generated compile_simlib TCL script in Vivado batch mode."""
137
- vivado_executable = common.get_vivado_executable(config.vivado_tools_folder)
138
- if not vivado_executable:
139
- raise ValueError("VivadoToolsFolder setting is missing from configuration")
140
-
141
- vivado_abs = os.path.abspath(vivado_executable)
142
- if not os.path.exists(vivado_abs):
143
- raise FileNotFoundError(
144
- f"Vivado executable not found at: {vivado_abs}\n"
145
- f"Please check your VivadoToolsFolder setting in nihdlsettings.py"
146
- )
135
+ vivado_abs = common.resolve_vivado_executable_abs(config)
147
136
 
148
137
  os.makedirs(out_folder, exist_ok=True)
149
138
  # Use a distinct name for Vivado's session log/journal. compile_simlib
@@ -165,12 +154,19 @@ def _run_compile_simlib(config, tcl_path, out_folder, libraries):
165
154
  "try again."
166
155
  ) from exc
167
156
 
168
- command = (
169
- f'"{vivado_abs}" -mode batch '
170
- f'-source "{tcl_path}" '
171
- f'-log "{log_path}" '
172
- f'-journal "{journal_path}"'
173
- )
157
+ # Build the argument list explicitly and run without a shell so paths that
158
+ # contain spaces or shell-significant characters are passed through verbatim.
159
+ command = [
160
+ vivado_abs,
161
+ "-mode",
162
+ "batch",
163
+ "-source",
164
+ tcl_path,
165
+ "-log",
166
+ log_path,
167
+ "-journal",
168
+ journal_path,
169
+ ]
174
170
 
175
171
  # Run with the MODELSIM environment variable unset so compile_simlib does
176
172
  # not read or modify a global modelsim.ini; it writes a fresh one into the
@@ -181,7 +177,7 @@ def _run_compile_simlib(config, tcl_path, out_folder, libraries):
181
177
  reporter.detail(f"Vivado executable: {vivado_abs}")
182
178
  reporter.detail(f"Output directory: {out_folder}")
183
179
  reporter.detail(f"Libraries: {', '.join(libraries)}")
184
- reporter.detail(f"Running command: {command}")
180
+ reporter.detail(f"Running command: {' '.join(command)}")
185
181
  reporter.success("")
186
182
  reporter.success("=" * 78)
187
183
  reporter.success("Compiling the Xilinx ModelSim simulation libraries with Vivado.")
@@ -201,7 +197,7 @@ def _run_compile_simlib(config, tcl_path, out_folder, libraries):
201
197
  process = subprocess.Popen(
202
198
  command,
203
199
  cwd=out_folder,
204
- shell=True,
200
+ shell=False,
205
201
  env=env,
206
202
  stdout=subprocess.PIPE,
207
203
  stderr=subprocess.STDOUT,
@@ -25,12 +25,12 @@ def _generate_compile_project_tcl(config, output_path):
25
25
 
26
26
  def _validate_ini(config):
27
27
  """Validate required settings and paths for compile-project."""
28
- missing_settings = []
28
+ missing_settings = common.collect_missing_settings(
29
+ config,
30
+ [("vivado_project_folder", "VivadoProjectSettings.VivadoProjectFolder")],
31
+ )
29
32
  invalid_paths = []
30
33
 
31
- if not config.vivado_project_folder:
32
- missing_settings.append("VivadoProjectSettings.VivadoProjectFolder")
33
-
34
34
  if not config.vivado_tcl_scripts_folder:
35
35
  missing_settings.append("VivadoProjectSettings.VivadoTclScriptsFolder")
36
36
  else:
@@ -72,12 +72,9 @@ def _validate_ini(config):
72
72
  if invalid_path:
73
73
  invalid_paths.append(invalid_path)
74
74
 
75
- error_msg = common.get_missing_settings_error(missing_settings)
76
- error_msg += common.get_invalid_paths_error(invalid_paths)
77
-
78
- if missing_settings or invalid_paths:
79
- error_msg += "\nPlease update your configuration file and try again."
80
- raise ValueError(error_msg)
75
+ error = common.build_settings_error(missing_settings, invalid_paths)
76
+ if error:
77
+ raise ValueError(error)
81
78
 
82
79
 
83
80
  def _get_compile_status_from_log(log_contents):
@@ -103,16 +100,7 @@ def _get_compile_status_from_log(log_contents):
103
100
 
104
101
  def _run_compile_project(config, generated_tcl_path):
105
102
  """Run the generated compile-project TCL script in Vivado batch mode."""
106
- vivado_executable = common.get_vivado_executable(config.vivado_tools_folder)
107
- if not vivado_executable:
108
- raise ValueError("VivadoToolsFolder setting is missing from configuration")
109
-
110
- vivado_abs = os.path.abspath(vivado_executable)
111
- if not os.path.exists(vivado_abs):
112
- raise FileNotFoundError(
113
- f"Vivado executable not found at: {vivado_abs}\n"
114
- f"Please check your --vivado argument or VivadoToolsFolder setting in nihdlsettings.py"
115
- )
103
+ vivado_abs = common.resolve_vivado_executable_abs(config)
116
104
 
117
105
  current_dir = os.getcwd()
118
106
  vivado_project_dir = os.path.join(current_dir, config.vivado_project_folder)
@@ -123,18 +111,23 @@ def _run_compile_project(config, generated_tcl_path):
123
111
  if os.path.exists(path):
124
112
  os.remove(path)
125
113
 
126
- command = (
127
- f'"{vivado_abs}" -mode batch '
128
- f'-source "{generated_tcl_path}" '
129
- f'-log "{log_path}" '
130
- f'-journal "{journal_path}"'
131
- )
114
+ command = [
115
+ vivado_abs,
116
+ "-mode",
117
+ "batch",
118
+ "-source",
119
+ generated_tcl_path,
120
+ "-log",
121
+ log_path,
122
+ "-journal",
123
+ journal_path,
124
+ ]
132
125
 
133
126
  reporter.detail(f"Vivado executable: {vivado_abs}")
134
127
  reporter.detail(f"Working directory: {vivado_project_dir}")
135
- reporter.detail(f"Running command: {command}")
128
+ reporter.detail(f"Running command: {' '.join(command)}")
136
129
 
137
- result = subprocess.run(command, cwd=vivado_project_dir, shell=True, check=False)
130
+ result = subprocess.run(command, cwd=vivado_project_dir, check=False)
138
131
 
139
132
  if not os.path.exists(log_path):
140
133
  raise RuntimeError("Vivado compile-project log file was not created.")
@@ -211,7 +211,8 @@ def create_lv_bitx(config=None):
211
211
  return result # Return the result code
212
212
  except Exception as e:
213
213
  reporter.error(f"Unhandled exception: {str(e)}")
214
- import traceback
214
+ if reporter.verbose:
215
+ import traceback
215
216
 
216
- traceback.print_exc()
217
+ traceback.print_exc()
217
218
  return 1 # Return error code on exception
@@ -61,11 +61,9 @@ def _validate_ini(config):
61
61
  f"{config.modelsim_tools_folder}"
62
62
  )
63
63
 
64
- error_msg = common.get_missing_settings_error(missing_settings)
65
- error_msg += common.get_invalid_paths_error(invalid_paths)
66
- if missing_settings or invalid_paths:
67
- error_msg += "\nPlease update your configuration file and try again."
68
- raise ValueError(error_msg)
64
+ error = common.build_settings_error(missing_settings, invalid_paths)
65
+ if error:
66
+ raise ValueError(error)
69
67
 
70
68
 
71
69
  def _get_vsim_executable(modelsim_path):
@@ -18,6 +18,7 @@ The tool supports:
18
18
  import glob
19
19
  import os
20
20
  import shutil
21
+ import stat
21
22
  import subprocess
22
23
  from collections import defaultdict
23
24
  from enum import Enum
@@ -290,7 +291,7 @@ def _copy_long_path_files(file_list, vivado_project_dir, tcl_base_dir):
290
291
  if vivado_path_len > path_len_limit:
291
292
  target_path = os.path.join(target_folder_long, os.path.basename(file))
292
293
  if os.path.exists(target_path):
293
- os.chmod(target_path, 0o777) # Make the file writable
294
+ os.chmod(target_path, os.stat(target_path).st_mode | stat.S_IWUSR) # writable
294
295
  try:
295
296
  shutil.copy2(file, target_path)
296
297
  new_file_list.append(target_path)
@@ -471,19 +472,16 @@ def _validate_ini(config):
471
472
  Raises:
472
473
  ValueError: If any required settings are missing or invalid
473
474
  """
474
- missing_settings = []
475
+ missing_settings = common.collect_missing_settings(
476
+ config,
477
+ [
478
+ ("vivado_project_folder", "VivadoProjectSettings.VivadoProjectFolder"),
479
+ ("top_level_entity", "VivadoProjectSettings.TopLevelEntity"),
480
+ ("fpga_part", "VivadoProjectSettings.FPGAPart"),
481
+ ],
482
+ )
475
483
  invalid_paths = []
476
484
 
477
- # Check VivadoProjectSettings
478
- if not config.vivado_project_folder:
479
- missing_settings.append("VivadoProjectSettings.VivadoProjectFolder")
480
-
481
- if not config.top_level_entity:
482
- missing_settings.append("VivadoProjectSettings.TopLevelEntity")
483
-
484
- if not config.fpga_part:
485
- missing_settings.append("VivadoProjectSettings.FPGAPart")
486
-
487
485
  # Don't validate Vivado path if skip_vivado is set
488
486
  if not config.skip_vivado:
489
487
  if not config.vivado_tools_folder:
@@ -541,14 +539,9 @@ def _validate_ini(config):
541
539
  if invalid_path:
542
540
  invalid_paths.append(invalid_path)
543
541
 
544
- # Construct error message
545
- error_msg = common.get_missing_settings_error(missing_settings)
546
- error_msg += common.get_invalid_paths_error(invalid_paths)
547
-
548
- # If any issues found, raise an error with the helpful message
549
- if missing_settings or invalid_paths:
550
- error_msg += "\nPlease update your configuration file and try again."
551
- raise ValueError(error_msg)
542
+ error = common.build_settings_error(missing_settings, invalid_paths)
543
+ if error:
544
+ raise ValueError(error)
552
545
 
553
546
 
554
547
  def _validate_constraints_files(config):
@@ -563,13 +556,9 @@ def _validate_constraints_files(config):
563
556
  if invalid_path:
564
557
  invalid_paths.append(invalid_path)
565
558
 
566
- # Construct error message
567
- error_msg = common.get_invalid_paths_error(invalid_paths)
568
-
569
- # If any issues found, raise an error with the helpful message
570
- if invalid_paths:
571
- error_msg += "\nPlease update your configuration file and try again."
572
- raise ValueError(error_msg)
559
+ error = common.build_settings_error([], invalid_paths)
560
+ if error:
561
+ raise ValueError(error)
573
562
 
574
563
 
575
564
  def _create_project(mode: ProjectMode, config):
@@ -703,14 +692,21 @@ def _create_project(mode: ProjectMode, config):
703
692
 
704
693
  if mode == ProjectMode.NEW:
705
694
  # Create a new project
706
- command = f'"{vivado_abs}" -mode batch -source {new_proj_path}'
695
+ command = [vivado_abs, "-mode", "batch", "-source", new_proj_path]
707
696
  elif mode == ProjectMode.UPDATE:
708
697
  # Update the existing project
709
- command = f'"{vivado_abs}" {project_name}.xpr -mode batch -source {update_proj_path}'
698
+ command = [
699
+ vivado_abs,
700
+ f"{project_name}.xpr",
701
+ "-mode",
702
+ "batch",
703
+ "-source",
704
+ update_proj_path,
705
+ ]
710
706
  else:
711
707
  raise ValueError(f"Unsupported mode: {mode}")
712
708
 
713
- reporter.detail(f"Running command: {command}")
709
+ reporter.detail(f"Running command: {' '.join(command)}")
714
710
 
715
711
  # In skip_vivado mode, create a mock project file and skip Vivado execution
716
712
  if config.skip_vivado:
@@ -723,23 +719,25 @@ def _create_project(mode: ProjectMode, config):
723
719
  return 0
724
720
 
725
721
  try:
726
- output = common.run_command(
722
+ # Stream Vivado output live (capture_output=False) so the user sees
723
+ # progress during the slow compile and so the failure message below
724
+ # actually has output "above" to point at.
725
+ common.run_command(
727
726
  command,
728
727
  cwd=os.getcwd(),
728
+ capture_output=False,
729
729
  check=True,
730
730
  )
731
731
  except subprocess.CalledProcessError as e:
732
- os.chdir(current_dir)
733
732
  action = "updating" if mode == ProjectMode.UPDATE else "creating"
734
733
  raise RuntimeError(
735
734
  f"Vivado exited with a non-zero status ({e.returncode}) while {action} the "
736
735
  f"project. Review the Vivado output above and vivado.log for details."
737
736
  ) from e
738
-
739
- # Change back to the original directory
740
- os.chdir(current_dir)
741
-
742
- reporter.detail(output)
737
+ finally:
738
+ # Always restore the working directory, even if Vivado raised, so later
739
+ # relative-path resolution in the same run is not corrupted.
740
+ os.chdir(current_dir)
743
741
 
744
742
 
745
743
  def _create_project_handler(config, overwrite=False, update=False):
@@ -20,6 +20,7 @@ import csv # For reading signal definitions from CSV
20
20
  import os # For file and directory operations
21
21
  import re
22
22
  import shutil # For file copying operations
23
+ import stat
23
24
  import xml.etree.ElementTree as ET # For XML generation and manipulation # noqa: N817
24
25
  from xml.dom.minidom import parseString # For pretty-formatted XML output
25
26
 
@@ -616,7 +617,7 @@ def _copy_fpgafiles(
616
617
  target_path = os.path.join(dest_deps_folder, base_filename)
617
618
 
618
619
  if os.path.exists(target_path):
619
- os.chmod(target_path, 0o777) # Make the file writable
620
+ os.chmod(target_path, os.stat(target_path).st_mode | stat.S_IWUSR) # writable
620
621
  # Check file paths before copy
621
622
  if file and target_path:
622
623
  shutil.copy2(file, target_path)
@@ -646,7 +647,7 @@ def _copy_menu_files(plugin_folder, menus_folder):
646
647
  dst_file = os.path.join(dest_dir, file)
647
648
  # Make destination writable if it exists
648
649
  if os.path.exists(dst_file):
649
- os.chmod(dst_file, 0o777)
650
+ os.chmod(dst_file, os.stat(dst_file).st_mode | stat.S_IWUSR)
650
651
  # Add null check before copy
651
652
  if src_file is not None and dst_file is not None:
652
653
  shutil.copy2(src_file, dst_file)
@@ -688,29 +689,19 @@ def _validate_ini(config):
688
689
  # generation.
689
690
  missing_settings, invalid_paths = generate_vhdl._validate_generated_vhdl_settings(config)
690
691
 
691
- # Required general settings
692
- if not config.target_family:
693
- missing_settings.append("GeneralSettings.TargetFamily")
694
-
695
- if not config.base_target:
696
- missing_settings.append("GeneralSettings.BaseTarget")
697
-
698
- # Required plugin settings
699
- if not config.lv_target_plugin_output_folder:
700
- missing_settings.append("LVFPGATargetSettings.LVTargetPluginOutputFolder")
701
-
702
- if not config.lv_target_name:
703
- missing_settings.append("LVFPGATargetSettings.LVTargetName")
704
-
705
- if not config.lv_target_guid:
706
- missing_settings.append("LVFPGATargetSettings.LVTargetGUID")
707
-
708
- # Validate input files and folders
709
- if not config.boardio_output:
710
- missing_settings.append("LVFPGATargetSettings.BoardIOXML")
711
-
712
- if not config.clock_output:
713
- missing_settings.append("LVFPGATargetSettings.ClockXML")
692
+ # Required presence-only settings (list settings with path validation follow).
693
+ missing_settings += common.collect_missing_settings(
694
+ config,
695
+ [
696
+ ("target_family", "GeneralSettings.TargetFamily"),
697
+ ("base_target", "GeneralSettings.BaseTarget"),
698
+ ("lv_target_plugin_output_folder", "LVFPGATargetSettings.LVTargetPluginOutputFolder"),
699
+ ("lv_target_name", "LVFPGATargetSettings.LVTargetName"),
700
+ ("lv_target_guid", "LVFPGATargetSettings.LVTargetGUID"),
701
+ ("boardio_output", "LVFPGATargetSettings.BoardIOXML"),
702
+ ("clock_output", "LVFPGATargetSettings.ClockXML"),
703
+ ],
704
+ )
714
705
 
715
706
  # Check list settings
716
707
  if not config.hdl_file_lists:
@@ -750,14 +741,9 @@ def _validate_ini(config):
750
741
  if invalid_path:
751
742
  invalid_paths.append(invalid_path)
752
743
 
753
- # Construct error message
754
- error_msg = common.get_missing_settings_error(missing_settings)
755
- error_msg += common.get_invalid_paths_error(invalid_paths)
756
-
757
- # If any issues found, raise an error with the helpful message
758
- if missing_settings or invalid_paths:
759
- error_msg += "\nPlease update your configuration file and try again."
760
- raise ValueError(error_msg)
744
+ error = common.build_settings_error(missing_settings, invalid_paths)
745
+ if error:
746
+ raise ValueError(error)
761
747
 
762
748
 
763
749
  def gen_lv_target_support(config=None):
@@ -40,8 +40,13 @@ def _map_datatype_to_vhdl(data_type):
40
40
  return "std_logic"
41
41
 
42
42
  elif data_type.startswith(("U", "I")):
43
- # Handle U8, U16, U32, U64, I8, I16, I32, I64
44
- bit_width = int(data_type[1:])
43
+ # Handle U8, U16, U32, U64, I8, I16, I32, I64. Degrade gracefully to a
44
+ # sentinel (like the FXP/Array branches) instead of crashing the whole
45
+ # flow when the CSV holds a malformed U*/I* data type.
46
+ try:
47
+ bit_width = int(data_type[1:])
48
+ except ValueError:
49
+ return "INVALID_DATA_TYPE"
45
50
  return f"std_logic_vector({bit_width - 1} downto 0)"
46
51
 
47
52
  elif data_type.startswith("FXP"):
@@ -220,7 +225,7 @@ def _render_generated_vhdl(template_paths, output_folder, context):
220
225
  context (dict): kwargs passed to ``Template.render``.
221
226
 
222
227
  Raises:
223
- SystemExit: If an error occurs during VHDL generation.
228
+ RuntimeError: If an error occurs during VHDL generation.
224
229
  """
225
230
  try:
226
231
  os.makedirs(output_folder, exist_ok=True)
@@ -259,7 +264,10 @@ def _validate_generated_vhdl_settings(config):
259
264
  tuple: ``(missing_settings, invalid_paths)`` lists describing any
260
265
  problems found. Both empty means the VHDL-generation settings are valid.
261
266
  """
262
- missing_settings = []
267
+ missing_settings = common.collect_missing_settings(
268
+ config,
269
+ [("generated_vhdl_output_folder", "LVFPGATargetSettings.GeneratedVhdlOutputFolder")],
270
+ )
263
271
  invalid_paths = []
264
272
 
265
273
  # Validate input CSV if custom IO is included
@@ -277,10 +285,6 @@ def _validate_generated_vhdl_settings(config):
277
285
  if invalid_path:
278
286
  invalid_paths.append(invalid_path)
279
287
 
280
- # Validate output folder setting
281
- if not config.generated_vhdl_output_folder:
282
- missing_settings.append("LVFPGATargetSettings.GeneratedVhdlOutputFolder")
283
-
284
288
  return missing_settings, invalid_paths
285
289
 
286
290
 
@@ -307,11 +311,9 @@ def gen_generated_vhdl(config=None):
307
311
 
308
312
  # Validate that required settings for generation are present
309
313
  missing_settings, invalid_paths = _validate_generated_vhdl_settings(config)
310
- if missing_settings or invalid_paths:
311
- error_msg = common.get_missing_settings_error(missing_settings)
312
- error_msg += common.get_invalid_paths_error(invalid_paths)
313
- error_msg += "\nPlease update your configuration file and try again."
314
- reporter.error(f"Error: {error_msg}")
314
+ error = common.build_settings_error(missing_settings, invalid_paths)
315
+ if error:
316
+ reporter.error(f"Error: {error}")
315
317
  return 1
316
318
 
317
319
  context = _build_generated_vhdl_context(config)
@@ -75,7 +75,14 @@ def _get_window_netlist(config):
75
75
 
76
76
  try:
77
77
  common.run_command(
78
- f'"{vivado_abs}" {project_name}.xpr -mode batch -source {get_netlist_tcl_path}',
78
+ [
79
+ vivado_abs,
80
+ f"{project_name}.xpr",
81
+ "-mode",
82
+ "batch",
83
+ "-source",
84
+ get_netlist_tcl_path,
85
+ ],
79
86
  cwd=os.getcwd(),
80
87
  capture_output=False,
81
88
  check=True,
@@ -248,7 +255,10 @@ def _validate_ini(config):
248
255
  Raises:
249
256
  ValueError: If any required settings are missing or paths are invalid
250
257
  """
251
- missing_settings = []
258
+ missing_settings = common.collect_missing_settings(
259
+ config,
260
+ [("lv_window_netlist_output_folder", "LVWindowNetlistSettings.LVWindowFolder")],
261
+ )
252
262
  invalid_paths = []
253
263
 
254
264
  # Check required paths for window netlist generation
@@ -274,9 +284,6 @@ def _validate_ini(config):
274
284
  f"Vivado has issues with long paths. Please use a shorter path for your project."
275
285
  )
276
286
 
277
- if not config.lv_window_netlist_output_folder:
278
- missing_settings.append("LVWindowNetlistSettings.LVWindowFolder")
279
-
280
287
  # Check for Vivado tools path
281
288
  if not config.vivado_tools_folder:
282
289
  missing_settings.append("VivadoProjectSettings.VivadoToolsFolder")
@@ -289,14 +296,9 @@ def _validate_ini(config):
289
296
  if invalid_path:
290
297
  invalid_paths.append(invalid_path)
291
298
 
292
- # Construct error message using common utility functions
293
- error_msg = common.get_missing_settings_error(missing_settings)
294
- error_msg += common.get_invalid_paths_error(invalid_paths)
295
-
296
- # If any issues found, raise an error with the helpful message
297
- if missing_settings or invalid_paths:
298
- error_msg += "\nPlease update your configuration file and try again."
299
- raise ValueError(error_msg)
299
+ error = common.build_settings_error(missing_settings, invalid_paths)
300
+ if error:
301
+ raise ValueError(error)
300
302
 
301
303
 
302
304
  def get_window(config=None):
@@ -106,8 +106,8 @@ def _write_dep_marker(repo, repo_name, tag, requested, repo_url, repo_path, deps
106
106
  for stale in deps_dir.glob(f"{repo_name}-*{_DEP_INFO_SUFFIX}"):
107
107
  try:
108
108
  stale.unlink()
109
- except OSError:
110
- pass
109
+ except OSError as e:
110
+ reporter.detail(f" [INFO] Could not remove stale marker {stale.name}: {e}")
111
111
 
112
112
  commit = _get_commit_hash(repo_path) or "unknown"
113
113
  installed = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@@ -360,7 +360,17 @@ def _clone_repo_at_tag(repo, tag_or_spec, base_dir, delete_allowed=False, allow_
360
360
  response = "y"
361
361
  reporter.detail(f" Auto-deleting and re-cloning (--delete flag set)")
362
362
  else:
363
- response = input(f" Delete and re-clone? (y/N): ").strip().lower()
363
+ try:
364
+ response = input(f" Delete and re-clone? (y/N): ").strip().lower()
365
+ except EOFError:
366
+ # Non-interactive/CI run: there is no stdin to prompt on. Default
367
+ # to the safe "keep existing clone" choice instead of crashing;
368
+ # pass --delete to force a re-clone in automated runs.
369
+ reporter.detail(
370
+ " No interactive input available; keeping existing clone "
371
+ "(pass --delete to force a re-clone)."
372
+ )
373
+ response = "n"
364
374
 
365
375
  if response in ["y", "yes"]:
366
376
  reporter.detail(f" Deleting {repo_path}...")
@@ -72,7 +72,10 @@ def _validate_ini(config):
72
72
  Raises:
73
73
  ValueError: If any required settings are missing or paths are invalid
74
74
  """
75
- missing_settings = []
75
+ missing_settings = common.collect_missing_settings(
76
+ config,
77
+ [("lv_target_name", "LVFPGATargetSettings.LVTargetName")],
78
+ )
76
79
  invalid_paths = []
77
80
 
78
81
  # Check required settings for installation
@@ -88,9 +91,6 @@ def _validate_ini(config):
88
91
  if invalid_path:
89
92
  invalid_paths.append(invalid_path)
90
93
 
91
- if not config.lv_target_name:
92
- missing_settings.append("LVFPGATargetSettings.LVTargetName")
93
-
94
94
  if not config.lv_target_plugin_output_folder:
95
95
  missing_settings.append("LVFPGATargetSettings.LVTargetPluginOutputFolder")
96
96
  else:
@@ -103,14 +103,9 @@ def _validate_ini(config):
103
103
  if invalid_path:
104
104
  invalid_paths.append(invalid_path)
105
105
 
106
- # Construct error message
107
- error_msg = common.get_missing_settings_error(missing_settings)
108
- error_msg += common.get_invalid_paths_error(invalid_paths)
109
-
110
- # If any issues found, raise an error with the helpful message
111
- if missing_settings or invalid_paths:
112
- error_msg += "\nPlease update your configuration file and try again."
113
- raise ValueError(error_msg)
106
+ error = common.build_settings_error(missing_settings, invalid_paths)
107
+ if error:
108
+ raise ValueError(error)
114
109
 
115
110
 
116
111
  def install_lv_target_support(config=None):
@@ -30,9 +30,7 @@ def _validate_ini(config):
30
30
  )
31
31
 
32
32
  if not common.get_modelsim_entity(config):
33
- missing_settings.append(
34
- "ModelSimSettings.ModelSimEntity (or VivadoProjectSettings.TopLevelEntity)"
35
- )
33
+ missing_settings.append("ModelSimSettings.ModelSimEntity (set via set_modelsim_top_entity)")
36
34
 
37
35
  common.raise_for_missing_settings(missing_settings, invalid_paths)
38
36
 
@@ -95,14 +93,17 @@ def launch_modelsim(batch=False, config=None):
95
93
  return result.returncode
96
94
  reporter.success("Simulation completed successfully")
97
95
  else:
98
- # GUI mode: launch vsim detached
96
+ # GUI mode: launch vsim in its own window and return immediately.
99
97
  if platform.system() == "Windows":
100
- cmd = f'start "" "{vsim_exe}" -do {do_file}'
101
- return_code = subprocess.call(cmd, shell=True, cwd=project_dir)
98
+ # Use cmd's "start" (as an argument list, shell=False) to open vsim
99
+ # in its own window with no shell-injection surface. This also runs
100
+ # vsim launchers that are batch files, which CreateProcess cannot
101
+ # execute directly.
102
+ cmd = ["cmd", "/c", "start", "", vsim_exe, "-do", do_file]
103
+ return_code = subprocess.call(cmd, cwd=project_dir)
102
104
  else:
103
- cmd = [vsim_exe, "-do", do_file]
104
105
  subprocess.Popen(
105
- cmd,
106
+ [vsim_exe, "-do", do_file],
106
107
  cwd=project_dir,
107
108
  stdout=subprocess.DEVNULL,
108
109
  stderr=subprocess.DEVNULL,
@@ -26,7 +26,10 @@ def _validate_ini(config):
26
26
  Raises:
27
27
  ValueError: If any required settings are missing
28
28
  """
29
- missing_settings = []
29
+ missing_settings = common.collect_missing_settings(
30
+ config,
31
+ [("vivado_project_folder", "VivadoProjectSettings.VivadoProjectFolder")],
32
+ )
30
33
  invalid_paths = []
31
34
 
32
35
  # Check required Vivado settings
@@ -40,9 +43,6 @@ def _validate_ini(config):
40
43
  if invalid_path:
41
44
  invalid_paths.append(invalid_path)
42
45
 
43
- if not config.vivado_project_folder:
44
- missing_settings.append("VivadoProjectSettings.VivadoProjectFolder")
45
-
46
46
  common.raise_for_missing_settings(missing_settings, invalid_paths)
47
47
 
48
48
 
@@ -102,13 +102,18 @@ def launch_vivado(config=None):
102
102
  reporter.success("SKIP VIVADO: Validation successful, skipping Vivado launch")
103
103
  return 0
104
104
 
105
- # Launch Vivado
105
+ # Launch Vivado. project_arg (an existing .xpr) is optional.
106
106
  if platform.system() == "Windows":
107
- # On Windows, use start to launch in a new window
108
- cmd = f'start "" "{vivado_abs}" {project_arg}'
109
- return_code = subprocess.call(cmd, shell=True, cwd=vivado_project_dir)
107
+ # Use cmd's "start" to open Vivado in its own window and return
108
+ # immediately. Passed as an argument list (shell=False) so there is no
109
+ # shell-injection surface, and so Vivado's vivado.bat launcher -- a batch
110
+ # file that CreateProcess cannot run directly -- is executed by cmd.
111
+ cmd = ["cmd", "/c", "start", "", vivado_abs]
112
+ if project_arg:
113
+ cmd.append(project_arg)
114
+ return_code = subprocess.call(cmd, cwd=vivado_project_dir)
110
115
  else:
111
- # On Linux/macOS, launch directly
116
+ # On Linux/macOS, launch directly (blocks until Vivado exits).
112
117
  cmd = [vivado_abs]
113
118
  if project_arg:
114
119
  cmd.append(project_arg)
@@ -289,7 +289,8 @@ def _generate_board_io_csv_from_clip_xml(input_xml_path, clip_output_csv):
289
289
 
290
290
  except Exception as e:
291
291
  reporter.error(f"Error processing XML: {str(e)}")
292
- traceback.print_exc()
292
+ if reporter.verbose:
293
+ traceback.print_exc()
293
294
 
294
295
 
295
296
  def _process_constraint_file(input_xml_path, output_folder, instance_path):
@@ -340,7 +341,8 @@ def _process_constraint_file(input_xml_path, output_folder, instance_path):
340
341
 
341
342
  except Exception as e:
342
343
  reporter.error(f"Error processing XDC file {os.path.basename(input_xml_path)}: {str(e)}")
343
- traceback.print_exc()
344
+ if reporter.verbose:
345
+ traceback.print_exc()
344
346
 
345
347
 
346
348
  def _generate_clip_to_window_signals(input_xml_path, output_vhdl_path):
@@ -430,7 +432,8 @@ def _generate_clip_to_window_signals(input_xml_path, output_vhdl_path):
430
432
 
431
433
  except Exception as e:
432
434
  reporter.error(f"Error generating CLIP to Window signals: {e}")
433
- traceback.print_exc()
435
+ if reporter.verbose:
436
+ traceback.print_exc()
434
437
  return False, [f"Critical error: {str(e)}"]
435
438
 
436
439
 
@@ -573,7 +576,17 @@ def _validate_ini(config):
573
576
  Raises:
574
577
  ValueError: If any required settings are missing or paths are invalid
575
578
  """
576
- missing_settings = []
579
+ missing_settings = common.collect_missing_settings(
580
+ config,
581
+ [
582
+ ("clip_output_csv", "CLIPMigrationSettings.LVTargetBoardIO"),
583
+ ("clip_inst_example", "CLIPMigrationSettings.CLIPInstantiationExample"),
584
+ (
585
+ "clip_to_window_signal_definitions",
586
+ "CLIPMigrationSettings.CLIPtoWindowSignalDefinitions",
587
+ ),
588
+ ],
589
+ )
577
590
  invalid_paths = []
578
591
 
579
592
  # Check required paths
@@ -587,9 +600,6 @@ def _validate_ini(config):
587
600
  if invalid_path:
588
601
  invalid_paths.append(invalid_path)
589
602
 
590
- if not config.clip_output_csv:
591
- missing_settings.append("CLIPMigrationSettings.LVTargetBoardIO")
592
-
593
603
  if not config.clip_top_hdl:
594
604
  missing_settings.append("CLIPMigrationSettings.CLIPHDLTop")
595
605
  else:
@@ -600,9 +610,6 @@ def _validate_ini(config):
600
610
  if invalid_path:
601
611
  invalid_paths.append(invalid_path)
602
612
 
603
- if not config.clip_inst_example:
604
- missing_settings.append("CLIPMigrationSettings.CLIPInstantiationExample")
605
-
606
613
  # Note: CLIPXDCIn is optional, so we don't check if it's missing -
607
614
  # but if they are present, validate them
608
615
  # Only check for CLIPEntityPath and XDC output folder if there are XDC paths
@@ -621,17 +628,9 @@ def _validate_ini(config):
621
628
  if invalid_path:
622
629
  invalid_paths.append(invalid_path)
623
630
 
624
- if not config.clip_to_window_signal_definitions:
625
- missing_settings.append("CLIPMigrationSettings.CLIPtoWindowSignalDefinitions")
626
-
627
- # Construct error message
628
- error_msg = common.get_missing_settings_error(missing_settings)
629
- error_msg += common.get_invalid_paths_error(invalid_paths)
630
-
631
- # If any issues found, raise an error with the helpful message
632
- if missing_settings or invalid_paths:
633
- error_msg += "\nPlease update your configuration file and try again."
634
- raise ValueError(error_msg)
631
+ error = common.build_settings_error(missing_settings, invalid_paths)
632
+ if error:
633
+ raise ValueError(error)
635
634
 
636
635
 
637
636
  def migrate_clip(config=None):
@@ -33,10 +33,9 @@ def _validate_ini(config):
33
33
  if not common.get_modelsim_entity(config):
34
34
  missing_settings.append("ModelSimSettings.ModelSimEntity (set via set_modelsim_top_entity)")
35
35
 
36
- error_msg = common.get_missing_settings_error(missing_settings)
37
- error_msg += common.get_invalid_paths_error(invalid_paths)
38
- if error_msg:
39
- raise ValueError(error_msg)
36
+ error = common.build_settings_error(missing_settings, invalid_paths)
37
+ if error:
38
+ raise ValueError(error)
40
39
 
41
40
 
42
41
  def sim_modelsim(do_file=None, config=None):
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "labview-fpga-hdl-tools"
7
- version = "2.0.0.dev2"
7
+ version = "2.0.0.dev3"
8
8
  description = "LabVIEW FPGA HDL Tools"
9
9
  authors = [
10
10
  "Salvador Santolucito <salvador.santolucito@gmail.com>"