opencos-eda 0.2.51__py3-none-any.whl → 0.2.53__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 (43) hide show
  1. opencos/commands/__init__.py +2 -0
  2. opencos/commands/build.py +1 -1
  3. opencos/commands/deps_help.py +259 -0
  4. opencos/commands/export.py +1 -1
  5. opencos/commands/flist.py +3 -0
  6. opencos/commands/lec.py +1 -1
  7. opencos/commands/open.py +2 -0
  8. opencos/commands/proj.py +1 -1
  9. opencos/commands/shell.py +1 -1
  10. opencos/commands/sim.py +32 -8
  11. opencos/commands/synth.py +1 -1
  12. opencos/commands/upload.py +3 -0
  13. opencos/commands/waves.py +13 -2
  14. opencos/deps/defaults.py +1 -0
  15. opencos/deps/deps_file.py +30 -4
  16. opencos/deps/deps_processor.py +72 -2
  17. opencos/deps_schema.py +3 -0
  18. opencos/eda.py +66 -29
  19. opencos/eda_base.py +159 -20
  20. opencos/eda_config.py +2 -2
  21. opencos/eda_config_defaults.yml +83 -3
  22. opencos/eda_extract_targets.py +1 -58
  23. opencos/tests/helpers.py +16 -0
  24. opencos/tests/test_eda.py +13 -2
  25. opencos/tests/test_tools.py +227 -6
  26. opencos/tools/cocotb.py +492 -0
  27. opencos/tools/modelsim_ase.py +67 -51
  28. opencos/tools/quartus.py +638 -0
  29. opencos/tools/questa.py +167 -88
  30. opencos/tools/questa_fse.py +10 -0
  31. opencos/tools/riviera.py +1 -0
  32. opencos/tools/verilator.py +31 -0
  33. opencos/tools/vivado.py +3 -3
  34. opencos/util.py +22 -3
  35. opencos/utils/str_helpers.py +85 -0
  36. opencos/utils/vscode_helper.py +47 -0
  37. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/METADATA +2 -1
  38. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/RECORD +43 -39
  39. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/WHEEL +0 -0
  40. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/entry_points.txt +0 -0
  41. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE +0 -0
  42. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/licenses/LICENSE.spdx +0 -0
  43. {opencos_eda-0.2.51.dist-info → opencos_eda-0.2.53.dist-info}/top_level.txt +0 -0
opencos/tools/questa.py CHANGED
@@ -14,7 +14,7 @@ import shutil
14
14
 
15
15
  from opencos import util
16
16
  from opencos.eda_base import Tool
17
- from opencos.commands import CommandSim
17
+ from opencos.commands import CommandSim, CommandFList
18
18
 
19
19
  class ToolQuesta(Tool):
20
20
  '''Base class for CommandSimQuesta, collects version information about qrun'''
@@ -23,6 +23,7 @@ class ToolQuesta(Tool):
23
23
  _EXE = 'qrun'
24
24
 
25
25
  starter_edition = False # Aka, modelsim_ase
26
+ use_vopt = shutil.which('vopt') # vopt exists in qrun/vsim framework, and we'll use it.
26
27
  sim_exe = '' # vsim or qrun
27
28
  sim_exe_base_path = ''
28
29
  questa_major = None
@@ -74,126 +75,204 @@ class CommandSimQuesta(CommandSim, ToolQuesta):
74
75
  CommandSim.__init__(self, config)
75
76
  ToolQuesta.__init__(self, config=self.config)
76
77
  # add args specific to this simulator
77
- self.args['gui'] = False
78
- self.args['tcl-file'] = "sim.tcl"
78
+ self.args.update({
79
+ 'gui': False,
80
+ 'tcl-file': 'sim.tcl',
81
+ 'work-lib': 'work',
82
+ })
83
+ self.args_help.update({
84
+ 'gui': 'Run Questa in GUI mode',
85
+ 'tcl-file': 'name of TCL file to be created for Questa simulation',
86
+ 'work-lib': 'Questa work library name',
87
+ })
88
+
79
89
  self.shell_command = self.sim_exe # set by ToolQuesta.get_versions(self)
90
+ self.vlog_commands = []
91
+ self.vopt_commands = []
92
+ self.vsim_commands = []
80
93
 
81
94
  def set_tool_defines(self):
82
95
  ToolQuesta.set_tool_defines(self)
83
96
 
84
- def do_it(self):
85
- # add defines for this job
97
+ # We do not override CommandSim.do_it(), CommandSim.check_logs_for_errors(...)
98
+
99
+ def prepare_compile(self):
86
100
  self.set_tool_defines()
101
+ self.vlog_commands = self.get_compile_command_lists()
102
+ self.vopt_commands = self.get_elaborate_command_lists()
103
+ self.vsim_commands = self.get_simulate_command_lists()
104
+
105
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='compile.sh',
106
+ command_lists=self.vlog_commands)
107
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='elaborate.sh',
108
+ command_lists=self.vopt_commands)
109
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='simulate.sh',
110
+ command_lists=self.vsim_commands)
111
+ util.write_shell_command_file(dirpath=self.args['work-dir'], filename='all.sh',
112
+ command_lists = [
113
+ ['./pre_compile_dep_shell_commands.sh'],
114
+ ['./compile.sh'],
115
+ ['./elaborate.sh'],
116
+ ['./simulate.sh'],
117
+ ])
118
+
87
119
  self.write_eda_config_and_args()
88
120
 
89
- # it all gets done with one command
90
- command_list = [ self.shell_command, "-64", "-sv" ]
91
-
92
- # incdirs
93
- for value in self.incdirs:
94
- command_list += [ f"+incdir+{value}" ]
95
-
96
- # defines
97
- for key, value in self.defines.items():
98
- if value is None:
99
- command_list += [ f"+define+{key}" ]
100
- elif isinstance(value, str) and "\'" in value:
101
- command_list += [ f"\"+define+{key}={value}\"" ]
102
- else:
103
- command_list += [ f"\'+define+{key}={value}\'" ]
104
-
105
- # compile verilog
106
- for f in self.files_v:
107
- command_list += [ f ]
108
-
109
- # compile systemverilog
110
- for f in self.files_sv:
111
- command_list += [ f ]
112
-
113
- # TODO(drew): We don't natively support Xilinx Ultrascale+ glbl.v in questa yet.
114
- # To do so, would need to do something along the lines of:
115
- # if using_queta_and_xilinx_libs:
116
- # vivado_base_path = getattr(self, 'vivado_base_path', '')
117
- # glbl_v = vivado_base_path.replace('bin', 'data/verilog/src/glbl.v')
118
- # if not os.path.exists(glbl_v):
119
- # self.error(f"Vivado is not setup, could not find file {glbl_v=}")
120
- # command_list.append(glbl_v)
121
-
122
- # misc options
123
- command_list += [ '-top', self.args['top'], '-timescale', '1ns/1ps', '-work', 'work.lib']
124
- # TODO(drew): most of these should move to eda_config_defaults.yml
125
- command_list += [
126
- # avoid warnings about defaulting to "var" which isn't LRM behavior
127
- '-svinputport=net',
128
- # Existing package 'xxxx_pkg' at line 9 will be overwritten.
129
- '-suppress', 'vlog-2275',
130
- # Extra checking for conflict in always_comb and always_latch variables at vopt time
131
- '-suppress', 'vlog-2583',
132
- # Missing connection for port 'xxxx' (The default port value will be used)
121
+ def compile(self):
122
+ if self.args['stop-before-compile']:
123
+ return
124
+ self.run_commands_check_logs(
125
+ self.vlog_commands, check_logs=True, log_filename='vlog.log'
126
+ )
127
+
128
+ def elaborate(self):
129
+ if self.args['stop-before-compile'] or self.args['stop-after-compile']:
130
+ return
131
+ self.run_commands_check_logs(
132
+ self.vopt_commands, check_logs=True, log_filename='vopt.log'
133
+ )
134
+
135
+ def simulate(self):
136
+ if self.args['stop-before-compile'] or self.args['stop-after-compile'] or \
137
+ self.args['stop-after-elaborate']:
138
+ return
139
+ self.run_commands_check_logs(
140
+ self.vsim_commands, check_logs=True, log_filename='vsim.log'
141
+ )
142
+
143
+ def get_compile_command_lists(self, **kwargs) -> list:
144
+ self.set_tool_defines()
145
+ ret = []
146
+
147
+ # Create work library
148
+ vlib_cmd = [os.path.join(self.sim_exe_base_path, 'vlib'), self.args['work-lib']]
149
+ ret.append(vlib_cmd)
150
+
151
+ # Map work library
152
+ vmap_cmd = [os.path.join(self.sim_exe_base_path, 'vmap'), 'work', self.args['work-lib']]
153
+ ret.append(vmap_cmd)
154
+
155
+ # Compile files
156
+ if self.files_v or self.files_sv:
157
+ vlog_cmd = [os.path.join(self.sim_exe_base_path, 'vlog'), '-64', '-sv']
158
+
159
+ # Add include directories
160
+ for incdir in self.incdirs:
161
+ vlog_cmd += [f'+incdir+{incdir}']
162
+
163
+ # Add defines
164
+ for key, value in self.defines.items():
165
+ if value is None:
166
+ vlog_cmd += [f'+define+{key}']
167
+ else:
168
+ vlog_cmd += [f'+define+{key}={value}']
169
+
170
+ # Add suppression flags
171
+ vlog_cmd += [
172
+ '-svinputport=net',
173
+ '-suppress', 'vlog-2275',
174
+ '-suppress', 'vlog-2583',
175
+ ]
176
+
177
+ # Add source files
178
+ vlog_cmd += self.files_v + self.files_sv
179
+
180
+ ret.append(vlog_cmd)
181
+
182
+ # Compile VHDL files if any
183
+ if self.files_vhd:
184
+ vcom_cmd = [os.path.join(self.sim_exe_base_path, 'vcom'), '-64']
185
+ vcom_cmd += self.files_vhd
186
+ ret.append(vcom_cmd)
187
+
188
+ return ret
189
+
190
+ def get_elaborate_command_lists(self, **kwargs) -> list:
191
+ if self.args['stop-after-compile']:
192
+ return []
193
+
194
+ vopt_cmd = [os.path.join(self.sim_exe_base_path, 'vopt'), '-64']
195
+
196
+ # Add optimization flags
197
+ vopt_cmd += [
133
198
  '-suppress', 'vopt-13159',
134
- # Too few port connections for 'uAW_FIFO'. Expected 10, found 8
135
199
  '-suppress', 'vopt-2685',
136
- # Missing connection for port 'almostEmpty' ... same message for inputs and outputs.
137
200
  '-note', 'vopt-2718',
138
201
  ]
139
- if self.args['gui']:
140
- command_list += ['-gui=interactive', '+acc', '-i']
141
- elif self.args['waves']:
142
- command_list += ['+acc', '-c']
143
- else:
144
- command_list += ['-c']
145
202
 
146
- if util.args['verbose']:
147
- command_list += ['-verbose']
203
+ if self.args['gui'] or self.args['waves']:
204
+ vopt_cmd += ['+acc']
148
205
 
149
- # TODO(drew): We don't natively support Xilinx Ultrascale+ libraries in --tool=questa
150
- # with an easy hook (like --xilinx) but we could?
151
- # To get this to work, need to add this, among other items:
152
- # --> command_list += '''-L xil_defaultlib -L unisims_ver -L unimacro_ver
153
- # -L xpm -L secureip -L xilinx_vip'''.split(" ")
206
+ # Add top module and output
207
+ vopt_cmd += [self.args['top'], '-o', 'opt_design']
154
208
 
155
- # check if we're bailing out early
156
- if self.args['stop-after-elaborate']:
157
- command_list += ['-elab', 'elab.output', '-do', '"quit"' ]
209
+ return [vopt_cmd]
158
210
 
159
- # create TCL
211
+ def get_simulate_command_lists(self, **kwargs) -> list:
212
+ # Create TCL file
160
213
  tcl_name = os.path.abspath(os.path.join(self.args['work-dir'], self.args['tcl-file']))
161
- with open( tcl_name, 'w', encoding='utf-8' ) as fo:
214
+
215
+ if self.args['waves']:
216
+ util.artifacts.add_extension(
217
+ search_paths=self.args['work-dir'], file_extension='wlf',
218
+ typ='waveform', description='Questa Waveform WLF file'
219
+ )
220
+
221
+ with open(tcl_name, 'w', encoding='utf-8') as fo:
162
222
  if self.args['waves']:
163
223
  if self.args['waves-start']:
164
224
  print(f"run {self.args['waves-start']} ns", file=fo)
165
225
  print("add wave -r /*", file=fo)
166
226
  print("run -all", file=fo)
167
- if not self.args['gui']:
227
+ if self.run_in_batch_mode():
168
228
  print("quit", file=fo)
169
- command_list += ['-do', tcl_name ]
170
229
 
171
- # execute snapshot
172
- self.exec(self.args['work-dir'], command_list)
230
+ # Create vsim command
231
+ vsim_cmd = [os.path.join(self.sim_exe_base_path, 'vsim'), '-64']
173
232
 
174
- # Note that CommandSimQuesta doesn't yet follow the framework from CommandSim.do_it()
175
- # so we have to define all helper methods for pylint:
233
+ if not self.run_in_batch_mode():
234
+ vsim_cmd += ['-gui']
235
+ else:
236
+ vsim_cmd += ['-c']
176
237
 
177
- def compile(self) -> None:
178
- pass
238
+ if util.args['verbose']:
239
+ vsim_cmd += ['-verbose']
179
240
 
180
- def elaborate(self) -> None:
181
- pass
241
+ # Add simulation arguments
242
+ vsim_cmd += ['-do', tcl_name, 'opt_design']
182
243
 
183
- def simulate(self) -> None:
184
- pass
244
+ return [vsim_cmd]
185
245
 
186
- def get_compile_command_lists(self, **kwargs) -> list:
246
+ def get_post_simulate_command_lists(self, **kwargs) -> list:
187
247
  return []
188
248
 
189
- def get_elaborate_command_lists(self, **kwargs) -> list:
190
- return []
249
+ def run_in_batch_mode(self) -> bool:
250
+ '''Returns bool if we should run in batch mode (-c) from command line'''
251
+ if self.args['test-mode']:
252
+ return True
253
+ if self.args['gui']:
254
+ return False
255
+ return True
191
256
 
192
- def get_simulate_command_lists(self, **kwargs) -> list:
193
- return []
257
+ def artifacts_add(self, name: str, typ: str, description: str) -> None:
258
+ '''Override from Command.artifacts_add for better descriptions'''
259
+ _, leafname = os.path.split(name)
260
+ if leafname == 'vsim.log':
261
+ description = 'Questa simulation step (3/3) log from stdout/stderr'
262
+ elif leafname == 'vopt.log':
263
+ description = 'Questa elaboration step (2/3) log from stdout/stderr'
264
+ elif leafname == 'vlog.log':
265
+ description = 'Questa compile step (1/3) log from stdout/stderr'
194
266
 
195
- def get_post_simulate_command_lists(self, **kwargs) -> list:
196
- return []
267
+ super().artifacts_add(name=name, typ=typ, description=description)
268
+
269
+
270
+ class CommandFListQuesta(CommandFList, ToolQuesta):
271
+ '''CommandFListQuesta is a command handler for: eda flist --tool=questa'''
272
+
273
+ def __init__(self, config: dict):
274
+ CommandFList.__init__(self, config=config)
275
+ ToolQuesta.__init__(self, config=self.config)
197
276
 
198
277
 
199
278
  class CommandElabQuesta(CommandSimQuesta):
@@ -11,6 +11,7 @@ For: Questa Intel Starter FPGA Edition-64 vsim 20XX.X Simulator
11
11
  import os
12
12
 
13
13
  from opencos.tools.modelsim_ase import CommandSimModelsimAse
14
+ from opencos.tools.questa import CommandFListQuesta
14
15
 
15
16
 
16
17
  class CommandSimQuestaFse(CommandSimModelsimAse):
@@ -20,6 +21,7 @@ class CommandSimQuestaFse(CommandSimModelsimAse):
20
21
  '''
21
22
  _TOOL = 'questa_fse'
22
23
  _EXE = 'vsim'
24
+ use_vopt = True
23
25
 
24
26
  def __init__(self, config: dict):
25
27
  # this will setup with self._TOOL = modelsim_ase, which is not ideal so
@@ -57,3 +59,11 @@ class CommandElabQuestaFse(CommandSimQuestaFse):
57
59
  def __init__(self, config:dict):
58
60
  super().__init__(config)
59
61
  self.args['stop-after-elaborate'] = True
62
+
63
+
64
+ class CommandFListQuestaFse(CommandFListQuesta):
65
+ '''CommandFListQuestaFse is a command handler for: eda flist --tool=questa_fse'''
66
+
67
+ def __init__(self, config: dict):
68
+ CommandFListQuesta.__init__(self, config=config)
69
+ self._TOOL = 'questa_fse'
opencos/tools/riviera.py CHANGED
@@ -19,6 +19,7 @@ class ToolRiviera(ToolModelsimAse):
19
19
 
20
20
  _TOOL = 'riviera'
21
21
  _EXE = 'vsim'
22
+ use_vopt = False
22
23
 
23
24
  def get_versions(self) -> str:
24
25
  if self._VERSION:
@@ -79,6 +79,8 @@ class VerilatorSim(CommandSim, ToolVerilator):
79
79
  'lint-only': False,
80
80
  'cc-mode': False,
81
81
  'verilator-coverage-args': [],
82
+ 'x-assign': '',
83
+ 'x-initial': '',
82
84
  })
83
85
 
84
86
  self.args_help.update({
@@ -92,6 +94,14 @@ class VerilatorSim(CommandSim, ToolVerilator):
92
94
  'gui': 'Not supported for Verilator',
93
95
  'cc-mode': 'Run verilator with --cc, requires a sim_main.cpp or similar sources',
94
96
  'optimize': 'Run verilator with: -CLAGS -O3, if no other CFLAGS args are presented',
97
+ 'x-assign': ('String value to added to verilator call: --x-assign <string>;'
98
+ ' where valid string values are: 0, 1, unique, fast.'
99
+ ' Also conditinally adds to verilated exe call:'
100
+ ' +verilator+rand+reset+[0,1,2] for arg values 0, 1, unique|fast'),
101
+ 'x-initial': ('String value to added to verilator call: --x-initial <string>;'
102
+ ' where valid string values are: 0, unique, fast.'
103
+ ' Also conditinally adds to verilated exe call:'
104
+ ' +verilator+rand+reset+[0,2] for arg values 0, unique|fast'),
95
105
  })
96
106
 
97
107
  self.verilate_command_lists = []
@@ -219,6 +229,14 @@ class VerilatorSim(CommandSim, ToolVerilator):
219
229
  '-o', 'sim.exe',
220
230
  ]
221
231
 
232
+ for arg in ('x-assign', 'x-initial'):
233
+ if self.args[arg] and f'--{arg}' not in verilate_command_list:
234
+ # Only add this if arg is set, and not present in verilator call
235
+ # this takes care of it being in our self.tool_config for compile-args.
236
+ verilate_command_list += [
237
+ f'--{arg}', self.args[arg]
238
+ ]
239
+
222
240
  # incdirs
223
241
  for value in self.incdirs:
224
242
  verilate_command_list += [ f"+incdir+{value}" ]
@@ -295,6 +313,19 @@ class VerilatorSim(CommandSim, ToolVerilator):
295
313
  if not any(x.startswith('+verilator+seed+') for x in verilated_exec_command_list):
296
314
  verilated_exec_command_list.append(f'+verilator+seed+{verilator_seed}')
297
315
 
316
+ if any(self.args[arg] in ('unique', 'fast') for arg in ('x-assign', 'x-initial')) and \
317
+ not any(x.startswith('+verilator+rand+reset') for x in verilated_exec_command_list):
318
+ # Only add this if arg is one of x-assign/x-initial is set to "unique" or "fast",
319
+ # we use the encoded value "2" for +verilator+rand+reset+2
320
+ verilated_exec_command_list.append('+verilator+rand+reset+2')
321
+
322
+ if self.args['x-assign'] == '1' and \
323
+ not any(x.startswith('+verilator+rand+reset') for x in verilated_exec_command_list):
324
+ # Only add this if --x-assign=1 (not valid for --x-initial),
325
+ # we use the encoded value "1" for +verilator+rand+reset+1
326
+ verilated_exec_command_list.append('+verilator+rand+reset+1')
327
+
328
+
298
329
  return [
299
330
  util.ShellCommandList(verilated_exec_command_list, tee_fpath='sim.log')
300
331
  ] # single entry list
opencos/tools/vivado.py CHANGED
@@ -259,7 +259,7 @@ class CommandSimVivado(CommandSim, ToolVivado):
259
259
  print(f"run {self.args['waves-start']} ns", file=fo)
260
260
  print("log_wave -recursive *", file=fo)
261
261
  print("run -all", file=fo)
262
- if not self.args['gui']:
262
+ if not self.args['gui'] or self.args['test-mode']:
263
263
  print("exit", file=fo)
264
264
 
265
265
  sv_seed = str(self.args['seed'])
@@ -280,7 +280,7 @@ class CommandSimVivado(CommandSim, ToolVivado):
280
280
  if sys.platform == "win32":
281
281
  command_list[0] += ".bat"
282
282
  command_list += self.tool_config.get('simulate-args', 'snapshot --stats').split()
283
- if self.args['gui']:
283
+ if self.args['gui'] and not self.args['test-mode']:
284
284
  command_list += ['-gui']
285
285
  command_list += [
286
286
  '--tclbatch', tcl_name.replace('\\','\\\\'), # needed for windows paths
@@ -701,7 +701,7 @@ class CommandBuildVivado(CommandBuild, ToolVivado):
701
701
  command_list = [self.vivado_exe]
702
702
  command_list += [
703
703
  '-mode',
704
- 'gui' if self.args['gui'] else 'batch',
704
+ 'gui' if self.args['gui'] and not self.args['test-mode'] else 'batch',
705
705
  '-log', os.path.join(self.args['work-dir'], self.args['top'] + '.build.log')
706
706
  ]
707
707
  if not util.args['verbose']:
opencos/util.py CHANGED
@@ -224,16 +224,17 @@ class UtilLogger:
224
224
  # util's argparser: --no-default-log, --logfile=<name>, or --force-logfile=<name>
225
225
  default_log_enabled = False
226
226
  default_log_filepath = os.path.join('eda.work', 'eda.log')
227
+ default_log_disable_with_args = ['-h', '--help'] # common argparse help
227
228
  enable = True
228
229
 
229
230
  def clear(self) -> None:
230
- '''Resets internals'''
231
+ '''Resets some internals for logging, but preserve others'''
231
232
  self.file = None
232
233
  self.filepath = ''
233
234
  self.time_last = 0
234
235
 
235
236
  def stop(self) -> None:
236
- '''Closes open log, resets internals'''
237
+ '''Closes open log, resets some internals'''
237
238
  if self.file:
238
239
  self.write_timestamp(f'stop - {self.filepath}')
239
240
  info(f"Closing logfile: {self.filepath}")
@@ -367,6 +368,21 @@ def get_argparser() -> argparse.ArgumentParser:
367
368
  return parser
368
369
 
369
370
 
371
+ def get_argparser_args_list(parser: object = None) -> list:
372
+ '''Returns list of all args, all items will include the -- prefix (--help, etc)'''
373
+ if not isinstance(parser, argparse.ArgumentParser):
374
+ return []
375
+ return list(vars(parser).get('_option_string_actions', {}).keys())
376
+
377
+
378
+ def get_argparsers_args_list(parsers: list) -> list:
379
+ '''Returns list of all args from list of parsers, all items have -- prefix (--help, etc)'''
380
+ _args_list = []
381
+ for parser in parsers:
382
+ _args_list.extend(get_argparser_args_list(parser))
383
+ return list(dict.fromkeys(_args_list)) # uniqify, preserve order
384
+
385
+
370
386
  def get_argparser_short_help(parser: object = None) -> str:
371
387
  '''Returns short help for our ArgumentParser'''
372
388
  if not parser:
@@ -420,15 +436,18 @@ def process_tokens(tokens:list) -> (argparse.Namespace, list):
420
436
  # set artifacts.enabled based on args['artifacts-json']
421
437
  artifacts.reset(enable=parsed.artifacts_json)
422
438
 
439
+
423
440
  if parsed.force_logfile:
424
441
  start_log(parsed.force_logfile, force=True)
425
442
  elif parsed.logfile:
426
443
  start_log(parsed.logfile, force=False)
427
444
  elif parsed.default_log and \
428
- not any(x in unparsed for x in ('help', '-h', '--help')) and \
445
+ not parsed.version and \
446
+ not any(x in unparsed for x in global_log.default_log_disable_with_args) and \
429
447
  (parsed.force_logfile is None and parsed.logfile is None):
430
448
  # Use a forced logfile in the eda.work/eda.log:
431
449
  # avoid this if someone has --help arg not yet parsed.
450
+ # avoid this if someone called --version, b/c that will print and exit.
432
451
  start_log(global_log.default_log_filepath, force=True)
433
452
 
434
453
  parsed_as_dict = vars(parsed)
@@ -1,10 +1,21 @@
1
1
  '''opencos.utils.str_helpers -- Various str helpers for printing, indenting'''
2
2
 
3
3
  import fnmatch
4
+ import os
4
5
  import re
5
6
  import shlex
6
7
  import textwrap
7
8
 
9
+ VALID_TARGET_INFO_STR = (
10
+ "should start with an underscore/letter, rest should be alpha-numeric, dashes, or underscores."
11
+ )
12
+
13
+ def is_valid_target_name(s: str) -> bool:
14
+ '''Returns True if str starts with underscore/letter, rest alphanum, dash, or underscores'''
15
+ if not s or not (s[0].isalpha() or s[0] == '_'):
16
+ return False
17
+ return s.replace('_', '').replace('-', '').isalnum()
18
+
8
19
  def strip_all_quotes(s: str) -> str:
9
20
  '''Returns str with all ' and " removed'''
10
21
  return s.replace("'", '').replace('"', '')
@@ -109,3 +120,77 @@ def fnmatch_or_re(pattern: str, string: str) -> bool:
109
120
  # could have been an illegal/unsupported regex, so don't match.
110
121
  pass
111
122
  return any(matches)
123
+
124
+
125
+ def get_terminal_columns():
126
+ """
127
+ Retrieves the number of columns (width) of the terminal window.
128
+
129
+ Returns:
130
+ int: The number of columns in the terminal, or a default value (e.g., 80)
131
+ if the terminal size cannot be determined.
132
+ """
133
+ try:
134
+ size = os.get_terminal_size()
135
+ return size.columns
136
+ except OSError:
137
+ # Handle cases where the terminal size cannot be determined (e.g., not in a TTY)
138
+ return 80 # Default to 80 columns
139
+
140
+ return 80 # else default to 80.
141
+
142
+
143
+ def pretty_list_columns_manual(data: list, num_columns: int = 4, auto_columns: bool = True) -> list:
144
+ """Returns list, from list of str, organized into columns, manually aligning them."""
145
+
146
+ ret_lines = []
147
+ if not data:
148
+ return ret_lines
149
+
150
+ _spacing = 2
151
+
152
+ # Calculate maximum width for each column
153
+ max_lengths = [0] * num_columns
154
+ max_item_len = 0
155
+ for i, item in enumerate(data):
156
+ col_index = i % num_columns
157
+ max_lengths[col_index] = max(max_lengths[col_index], len(item))
158
+ max_item_len = max(max_item_len, len(item))
159
+
160
+ if auto_columns and num_columns > 1:
161
+ window_cols = get_terminal_columns()
162
+ max_line_len = 0
163
+ for x in max_lengths:
164
+ max_line_len += x + _spacing
165
+ if max_line_len > window_cols:
166
+ # subtract a column (already >= 2):
167
+ ret_lines.extend(
168
+ pretty_list_columns_manual(data=data, num_columns=num_columns-1, auto_columns=True)
169
+ )
170
+ return ret_lines
171
+ if max_line_len + max_item_len + _spacing < window_cols:
172
+ # add 1 more column if we're guaranteed to have room.
173
+ ret_lines.extend(
174
+ pretty_list_columns_manual(data=data, num_columns=num_columns+1, auto_columns=True)
175
+ )
176
+ return ret_lines
177
+ # else continue
178
+
179
+ # Print data in columns
180
+ ret_lines.append('')
181
+ for i, item in enumerate(data):
182
+ col_index = i % num_columns
183
+ ret_lines[-1] += str(item).ljust(max_lengths[col_index] + _spacing)
184
+ if col_index == num_columns - 1 or i == len(data) - 1:
185
+ ret_lines.append('')
186
+
187
+ return ret_lines
188
+
189
+
190
+ def print_columns_manual(data: list, num_columns: int = 4, auto_columns: bool = True) -> None:
191
+ """Prints a list of strings in columns, manually aligning them."""
192
+
193
+ lines = pretty_list_columns_manual(
194
+ data=data, num_columns=num_columns, auto_columns=auto_columns
195
+ )
196
+ print('\n'.join(lines))
@@ -0,0 +1,47 @@
1
+ '''Because > 1 tools use the exe 'code', I don't want to run
2
+
3
+ `code --list-extensions --show-versionsvsim -version` for every tool that needs
4
+ to check if they exist in VScode land.
5
+
6
+ Instead, eda.py can call this once, and then query if the VScode extension exists when
7
+ running opencos.eda.auto_tool_setup(..)
8
+ '''
9
+
10
+ import shutil
11
+ import subprocess
12
+
13
+ from opencos.util import debug
14
+
15
+ vscode_path = shutil.which('code')
16
+
17
+ INIT_HAS_RUN = False
18
+ EXTENSIONS = {} # dict of {name: version} for VScode extensions of name
19
+
20
+
21
+ def init() -> None:
22
+ '''Sets INIT_HAS_RUN=True (only runs once) and one of TOOL_IS[tool] = True'''
23
+ global INIT_HAS_RUN # pylint: disable=global-statement
24
+
25
+ if INIT_HAS_RUN:
26
+ return
27
+
28
+ INIT_HAS_RUN = True
29
+
30
+ if not vscode_path:
31
+ return
32
+
33
+ proc = None
34
+ try:
35
+ proc = subprocess.run([vscode_path, '--list-extensions', '--show-versions'],
36
+ capture_output=True, check=False)
37
+ except Exception as e:
38
+ debug(f'vscode --list-extensions --show-versions: exception {e}')
39
+
40
+ if proc is None or proc.returncode != 0:
41
+ return
42
+
43
+ for line in proc.stdout.decode('utf-8', errors='replace').split('\n'):
44
+ if '@' in line:
45
+ parts = line.split('@')
46
+ if parts[0] and parts[1]:
47
+ EXTENSIONS[parts[0]] = parts[1]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: opencos-eda
3
- Version: 0.2.51
3
+ Version: 0.2.53
4
4
  Summary: A simple Python package for wrapping RTL simuliatons and synthesis
5
5
  Author-email: Simon Sabato <simon@cognichip.ai>, Drew Ranck <drew@cognichip.ai>
6
6
  Project-URL: Homepage, https://github.com/cognichip/opencos
@@ -15,4 +15,5 @@ Requires-Dist: schema>=0.7.7
15
15
  Requires-Dist: toml>=0.10.2
16
16
  Requires-Dist: yamllint>=1.35.1
17
17
  Requires-Dist: PySerial>=3.5
18
+ Requires-Dist: cocotb>=2.0.0b1
18
19
  Dynamic: license-file