3 # SymbiYosys (sby) -- Front-end for Yosys-based formal verification flows
5 # Copyright (C) 2016 Claire Xenia Wolf <claire@yosyshq.com>
7 # Permission to use, copy, modify, and/or distribute this software for any
8 # purpose with or without fee is hereby granted, provided that the above
9 # copyright notice and this permission notice appear in all copies.
11 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 import argparse
, json
, os
, sys
, shutil
, tempfile
, re
22 from sby_core
import SbyConfig
, SbyTask
, SbyAbort
, process_filename
25 class DictAction(argparse
.Action
):
26 def __call__(self
, parser
, namespace
, values
, option_string
=None):
27 assert isinstance(getattr(namespace
, self
.dest
), dict), f
"Use ArgumentParser.set_defaults() to initialize {self.dest} to dict()"
28 name
= option_string
.lstrip(parser
.prefix_chars
).replace("-", "_")
29 getattr(namespace
, self
.dest
)[name
] = values
31 parser
= argparse
.ArgumentParser(prog
="sby",
32 usage
="%(prog)s [options] [<jobname>.sby [tasknames] | <dirname>]")
33 parser
.set_defaults(exe_paths
=dict())
35 parser
.add_argument("-d", metavar
="<dirname>", dest
="workdir",
36 help="set workdir name. default: <jobname> or <jobname>_<taskname>. When there is more than one task, use --prefix instead")
37 parser
.add_argument("--prefix", metavar
="<dirname>", dest
="workdir_prefix",
38 help="set the workdir name prefix. `_<taskname>` will be appended to the path for each task")
39 parser
.add_argument("-f", action
="store_true", dest
="force",
40 help="remove workdir if it already exists")
41 parser
.add_argument("-b", action
="store_true", dest
="backup",
42 help="backup workdir if it already exists")
43 parser
.add_argument("-t", action
="store_true", dest
="tmpdir",
44 help="run in a temporary workdir (remove when finished)")
45 parser
.add_argument("-T", metavar
="<taskname>", action
="append", dest
="tasknames", default
=list(),
46 help="add taskname (useful when sby file is read from stdin)")
47 parser
.add_argument("-E", action
="store_true", dest
="throw_err",
48 help="throw an exception (incl stack trace) for most errors")
49 parser
.add_argument("--autotune", action
="store_true", dest
="autotune",
50 help="automatically find a well performing engine and engine configuration for each task")
51 parser
.add_argument("--autotune-config", dest
="autotune_config",
52 help="read an autotune configuration file (overrides the sby file's autotune options)")
54 parser
.add_argument("--yosys", metavar
="<path_to_executable>",
55 action
=DictAction
, dest
="exe_paths")
56 parser
.add_argument("--abc", metavar
="<path_to_executable>",
57 action
=DictAction
, dest
="exe_paths")
58 parser
.add_argument("--smtbmc", metavar
="<path_to_executable>",
59 action
=DictAction
, dest
="exe_paths")
60 parser
.add_argument("--suprove", metavar
="<path_to_executable>",
61 action
=DictAction
, dest
="exe_paths")
62 parser
.add_argument("--aigbmc", metavar
="<path_to_executable>",
63 action
=DictAction
, dest
="exe_paths")
64 parser
.add_argument("--avy", metavar
="<path_to_executable>",
65 action
=DictAction
, dest
="exe_paths")
66 parser
.add_argument("--btormc", metavar
="<path_to_executable>",
67 action
=DictAction
, dest
="exe_paths")
68 parser
.add_argument("--pono", metavar
="<path_to_executable>",
69 action
=DictAction
, dest
="exe_paths",
70 help="configure which executable to use for the respective tool")
71 parser
.add_argument("--dumpcfg", action
="store_true", dest
="dump_cfg",
72 help="print the pre-processed configuration file")
73 parser
.add_argument("--dumptags", action
="store_true", dest
="dump_tags",
74 help="print the list of task tags")
75 parser
.add_argument("--dumptasks", action
="store_true", dest
="dump_tasks",
76 help="print the list of tasks")
77 parser
.add_argument("--dumpdefaults", action
="store_true", dest
="dump_defaults",
78 help="print the list of default tasks")
79 parser
.add_argument("--dumptaskinfo", action
="store_true", dest
="dump_taskinfo",
80 help="output a summary of tasks as JSON")
81 parser
.add_argument("--dumpfiles", action
="store_true", dest
="dump_files",
82 help="print the list of source files")
83 parser
.add_argument("--setup", action
="store_true", dest
="setupmode",
84 help="set up the working directory and exit")
86 parser
.add_argument("--init-config-file", dest
="init_config_file",
87 help="create a default .sby config file")
88 parser
.add_argument("sbyfile", metavar
="<jobname>.sby | <dirname>", nargs
="?",
89 help=".sby file OR directory containing config.sby file")
90 parser
.add_argument("arg_tasknames", metavar
="tasknames", nargs
="*",
91 help="tasks to run (only valid when <jobname>.sby is used)")
93 args
= parser
.parse_args()
95 sbyfile
= args
.sbyfile
96 workdir
= args
.workdir
97 workdir_prefix
= args
.workdir_prefix
98 if workdir
is not None and workdir_prefix
is not None:
99 print("ERROR: -d and --prefix are mutually exclusive.", file=sys
.stderr
)
101 tasknames
= args
.arg_tasknames
+ args
.tasknames
102 opt_force
= args
.force
103 opt_backup
= args
.backup
104 opt_tmpdir
= args
.tmpdir
105 exe_paths
= args
.exe_paths
106 throw_err
= args
.throw_err
107 dump_cfg
= args
.dump_cfg
108 dump_tags
= args
.dump_tags
109 dump_tasks
= args
.dump_tasks
110 dump_defaults
= args
.dump_defaults
111 dump_taskinfo
= args
.dump_taskinfo
112 dump_files
= args
.dump_files
114 setupmode
= args
.setupmode
115 autotune
= args
.autotune
116 autotune_config
= args
.autotune_config
117 init_config_file
= args
.init_config_file
119 if sbyfile
is not None:
120 if os
.path
.isdir(sbyfile
):
121 if workdir
is not None:
122 print("ERROR: Can't use -d when running in existing directory.", file=sys
.stderr
)
125 sbyfile
+= "/config.sby"
127 if not opt_force
and os
.path
.exists(workdir
+ "/model"):
128 print("ERROR: Use -f to re-run in existing directory.", file=sys
.stderr
)
131 print("ERROR: Can't use tasks when running in existing directory.", file=sys
.stderr
)
134 print("ERROR: Can't use --setup with existing directory.", file=sys
.stderr
)
137 for f
in "PASS FAIL UNKNOWN ERROR TIMEOUT".split():
138 if os
.path
.exists(workdir
+ "/" + f
):
139 os
.remove(workdir
+ "/" + f
)
140 elif not sbyfile
.endswith(".sby"):
141 print("ERROR: Sby file does not have .sby file extension.", file=sys
.stderr
)
144 elif init_config_file
is not None:
145 sv_file
= init_config_file
+ ".sv"
146 sby_file
= init_config_file
+ ".sby"
147 with
open(sby_file
, 'w') as config
:
148 config
.write(f
"""[options]
155 read -formal {sv_file}
162 print(f
"sby config written to {sby_file}", file=sys
.stderr
)
165 early_logmsgs
= list()
167 def early_log(workdir
, msg
):
168 tm
= time
.localtime()
169 early_logmsgs
.append("SBY {:2d}:{:02d}:{:02d} [{}] {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, workdir
, msg
))
170 print(early_logmsgs
[-1])
172 def read_sbyconfig(sbydata
, taskname
):
179 tasks_section
= False
180 task_tags_active
= set()
181 task_tags_all
= set()
182 task_skip_block
= False
183 task_skiping_blocks
= False
185 def handle_line(line
):
186 nonlocal pycode
, tasks_section
, task_tags_active
, task_tags_all
187 nonlocal task_skip_block
, task_skiping_blocks
, task_matched
190 line
= line
.rstrip("\n")
191 line
= line
.rstrip("\r")
193 if pycode
is not None:
194 if line
== "--pycode-end--":
195 gdict
= globals().copy()
196 gdict
["task"] = taskname
197 gdict
["tags"] = set(task_tags_active
)
198 gdict
["output_lines"] = list()
199 exec("def output(line):\n output_lines.append(line)\n" + pycode
, gdict
)
201 for line
in gdict
["output_lines"]:
204 pycode
+= line
+ "\n"
207 if line
== "--pycode-begin--":
211 if tasks_section
and line
.startswith("["):
212 tasks_section
= False
214 if task_skiping_blocks
:
216 task_skip_block
= False
217 task_skiping_blocks
= False
220 if not tasks_section
:
221 found_task_tag
= False
222 task_skip_line
= False
224 for t
in task_tags_all
:
225 if line
.startswith(t
+":"):
226 line
= line
[len(t
)+1:].lstrip()
227 match
= t
in task_tags_active
228 elif line
.startswith("~"+t
+":"):
229 line
= line
[len(t
)+2:].lstrip()
230 match
= t
not in task_tags_active
235 task_skiping_blocks
= True
236 task_skip_block
= not match
237 task_skip_line
= True
239 task_skip_line
= not match
241 found_task_tag
= True
244 if len(task_tags_all
) and not found_task_tag
:
245 tokens
= line
.split()
246 if len(tokens
) > 0 and tokens
[0][0] == line
[0] and tokens
[0].endswith(":"):
247 print(f
"ERROR: Invalid task specifier \"{tokens[0]}\".", file=sys
.stderr
)
250 if task_skip_line
or task_skip_block
:
256 if line
.startswith("#"):
259 line
= line
.split(":")
261 line
= line
[0].split()
263 lhs
, rhs
= line
[:1], line
[1:]
267 lhs
, rhs
= line
[0].split(), line
[1].split()
269 print("ERROR: Syntax error in tasks block.", file=sys
.stderr
)
273 if tagname
== "default":
275 if all(map(lambda c
: c
not in "(?*.[]|)", tagname
)):
276 task_tags_all
.add(tagname
)
279 if all(map(lambda c
: c
not in "(?*.[]|)", tname
)):
280 if tname
not in tasklist
:
281 tasklist
.append(tname
)
283 if defaultlist
is None:
285 if tname
not in defaultlist
:
286 defaultlist
.append(tname
)
287 task_tags_all
.add(tname
)
289 if taskname
is not None and re
.fullmatch(tname
, taskname
):
291 task_tags_active
.add(tname
)
293 if tagname
== "default":
295 if all(map(lambda c
: c
not in "(?*.[]|)", tagname
)):
296 task_tags_active
.add(tagname
)
298 for t
in task_tags_all
:
299 if re
.fullmatch(tagname
, t
):
300 task_tags_active
.add(t
)
302 elif line
== "[tasks]":
313 if taskname
is not None and not task_matched
:
314 print(f
"ERROR: Task name '{taskname}' didn't match any lines in [tasks].", file=sys
.stderr
)
317 if defaultlist
is None:
318 defaultlist
= tasklist
320 return cfgdata
, tasklist
, defaultlist
, sorted(list(task_tags_all
))
324 with (open(sbyfile
, "r") if sbyfile
is not None else sys
.stdin
) as f
:
329 assert len(tasknames
) < 2
330 sbyconfig
, _
, _
, _
= read_sbyconfig(sbydata
, tasknames
[0] if len(tasknames
) else None)
331 print("\n".join(sbyconfig
))
337 def find_files(taskname
):
338 sbyconfig
, _
, _
, _
= read_sbyconfig(sbydata
, taskname
)
341 for i
in range(len(sbyconfig
)):
342 if sbyconfig
[i
].strip() == "[files]":
346 if start_index
== -1:
349 for line
in sbyconfig
[start_index
+1:]:
351 if line
.startswith("["):
353 if line
== "" or line
.startswith("#"):
355 filename
= line
.split()[-1]
356 file_set
.add(process_filename(filename
))
359 for taskname
in tasknames
:
363 print("\n".join(file_set
))
367 _
, _
, _
, tagnames
= read_sbyconfig(sbydata
, None)
372 if dump_tasks
or dump_defaults
or dump_tags
:
373 _
, tasks
, dtasks
, tags
= read_sbyconfig(sbydata
, None)
374 for name
in tasks
if dump_tasks
else dtasks
if dump_defaults
else tags
:
380 _
, _
, tasknames
, _
= read_sbyconfig(sbydata
, None)
382 for taskname
in tasknames
or [None]:
383 task_sbyconfig
, _
, _
, _
= read_sbyconfig(sbydata
, taskname
)
384 taskinfo
[taskname
or ""] = info
= {}
386 cfg
.parse_config(task_sbyconfig
)
387 taskinfo
[taskname
or ""] = {
388 "mode": cfg
.options
.get("mode"),
389 "engines": cfg
.engines
,
390 "script": cfg
.script
,
392 print(json
.dumps(taskinfo
, indent
=2))
395 if len(tasknames
) == 0:
396 _
, _
, tasknames
, _
= read_sbyconfig(sbydata
, None)
397 if len(tasknames
) == 0:
400 if (workdir
is not None) and (len(tasknames
) != 1):
401 print("ERROR: Exactly one task is required when workdir is specified. Specify the task or use --prefix instead of -d.", file=sys
.stderr
)
404 def run_task(taskname
):
405 sbyconfig
, _
, _
, _
= read_sbyconfig(sbydata
, taskname
)
407 my_opt_tmpdir
= opt_tmpdir
410 if workdir
is not None:
412 elif workdir_prefix
is not None:
414 my_workdir
= workdir_prefix
416 my_workdir
= workdir_prefix
+ "_" + taskname
418 if my_workdir
is None and sbyfile
is not None and not my_opt_tmpdir
:
419 my_workdir
= sbyfile
[:-4]
420 if taskname
is not None:
421 my_workdir
+= "_" + taskname
423 if my_workdir
is not None:
426 while os
.path
.exists("{}.bak{:03d}".format(my_workdir
, backup_idx
)):
428 early_log(my_workdir
, "Moving directory '{}' to '{}'.".format(my_workdir
, "{}.bak{:03d}".format(my_workdir
, backup_idx
)))
429 shutil
.move(my_workdir
, "{}.bak{:03d}".format(my_workdir
, backup_idx
))
431 if opt_force
and not reusedir
:
432 early_log(my_workdir
, f
"Removing directory '{os.path.abspath(my_workdir)}'.")
434 shutil
.rmtree(my_workdir
, ignore_errors
=True)
438 elif os
.path
.isdir(my_workdir
):
439 print(f
"ERROR: Directory '{my_workdir}' already exists, use -f to overwrite the existing directory.")
442 os
.makedirs(my_workdir
)
446 my_workdir
= tempfile
.mkdtemp()
448 if os
.getenv("SBY_WORKDIR_GITIGNORE"):
449 with
open(f
"{my_workdir}/.gitignore", "w") as gitignore
:
450 print("*", file=gitignore
)
452 junit_ts_name
= os
.path
.basename(sbyfile
[:-4]) if sbyfile
is not None else workdir
if workdir
is not None else "stdin"
453 junit_tc_name
= taskname
if taskname
is not None else "default"
456 junit_filename
= os
.path
.basename(my_workdir
)
457 elif sbyfile
is not None:
458 junit_filename
= os
.path
.basename(sbyfile
[:-4])
459 if taskname
is not None:
460 junit_filename
+= "_" + taskname
461 elif taskname
is not None:
462 junit_filename
= taskname
464 junit_filename
= "junit"
466 task
= SbyTask(sbyconfig
, my_workdir
, early_logmsgs
, reusedir
)
468 for k
, v
in exe_paths
.items():
469 task
.exe_paths
[k
] = v
474 sby_autotune
.SbyAutotune(task
, autotune_config
).run()
482 task
.log(f
"Removing directory '{my_workdir}'.")
483 shutil
.rmtree(my_workdir
, ignore_errors
=True)
486 task
.log(f
"SETUP COMPLETE (rc={task.retcode})")
488 task
.log(f
"DONE ({task.status}, rc={task.retcode})")
491 if not my_opt_tmpdir
and not setupmode
and not autotune
:
492 with
open("{}/{}.xml".format(task
.workdir
, junit_filename
), "w") as f
:
493 task
.print_junit_result(f
, junit_ts_name
, junit_tc_name
, junit_format_strict
=False)
495 with
open(f
"{task.workdir}/status", "w") as f
:
496 print(f
"{task.status} {task.retcode} {task.total_time}", file=f
)
503 for task
in tasknames
:
504 task_retcode
= run_task(task
)
505 retcode |
= task_retcode
509 if failed
and (len(tasknames
) > 1 or tasknames
[0] is not None):
510 tm
= time
.localtime()
511 print("SBY {:2d}:{:02d}:{:02d} The following tasks failed: {}".format(tm
.tm_hour
, tm
.tm_min
, tm
.tm_sec
, failed
))