certora-cli-alpha-master 20241216.22.25.927248__py3-none-macosx_10_9_universal2.whl → 20241217.16.7.546354__py3-none-macosx_10_9_universal2.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,9 @@
1
+ import glob
1
2
  import itertools
2
3
  import json
3
4
  import os
4
5
  import re
6
+ import shutil
5
7
  import uuid
6
8
 
7
9
  import requests
@@ -190,21 +192,41 @@ def parse_json(response: Response) -> Dict[str, Any]:
190
192
  return json_response
191
193
 
192
194
 
193
- def compress_files_wasm(zip_file_path: Path, context: CertoraContext) -> bool:
195
+ def zip_rust_files(zip_file_path: Path, context: CertoraContext, *resource_paths: Path) -> bool:
196
+ patterns = ["*.rs", "*.so", "*.wasm", "Cargo.toml", "Cargo.lock", "justfile"]
197
+ exclude_dirs = [".certora_internal"]
198
+
199
+ root_directory = Path(context.rust_project_directory)
200
+ if not root_directory.is_dir():
201
+ raise ValueError(f"The given directory {root_directory} is not valid.")
202
+
194
203
  with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zip_obj:
195
- cargo_directory = context.cargo_dir
196
- for path in cargo_directory.rglob('*.rs'):
197
- if cargo_directory / 'target' in path.parents or cargo_directory / '.certora_internal' in path.parents:
198
- continue
199
- zip_obj.write(path, Util.CERTORA_SOURCES / path.relative_to(cargo_directory))
200
- for path in [cargo_directory / 'Cargo.toml', cargo_directory / 'Cargo.lock']:
201
- if path.exists():
202
- zip_obj.write(path, path.relative_to(cargo_directory))
203
- if context.wasm_file.exists():
204
- zip_obj.write(context.wasm_file, context.wasm_file.relative_to(context.wasm_file.parent))
204
+ for source in context.rust_sources:
205
+ for file in glob.glob(f'{root_directory.joinpath(source)}', recursive=True):
206
+ file_path = Path(file)
207
+ if any(excluded in file_path.parts for excluded in exclude_dirs):
208
+ continue
209
+ if file_path.is_file() and any(file_path.match(pattern) for pattern in patterns):
210
+ zip_obj.write(file_path, Util.CERTORA_SOURCES / file_path.relative_to(root_directory))
211
+ if context.build_script and Path(context.build_script).exists():
212
+ zip_obj.write(Path(context.build_script),
213
+ Util.CERTORA_SOURCES / Path(context.build_script).absolute().relative_to(root_directory))
214
+ if context.conf_file and Path(context.conf_file).exists():
215
+ zip_obj.write(Path(context.conf_file),
216
+ Util.CERTORA_SOURCES / Path(context.conf_file).absolute().relative_to(root_directory))
217
+ if Util.get_last_conf_file():
218
+ zip_obj.write(Util.get_last_conf_file(),
219
+ Util.CERTORA_SOURCES / Util.get_last_conf_file().relative_to(Util.get_build_dir()))
220
+
221
+ rust_executable = root_directory.joinpath(context.rust_executables)
222
+ zip_obj.write(rust_executable, rust_executable.relative_to(rust_executable.parent))
223
+ for path in resource_paths:
224
+ zip_obj.write(path, os.path.relpath(path, Util.get_build_dir()))
225
+ if path.suffix == '.txt':
226
+ zip_obj.write(path, Util.CERTORA_SOURCES / path.relative_to(Util.get_build_dir()))
205
227
 
206
228
  if zip_file_path.stat().st_size > MAX_FILE_SIZE:
207
- cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded.")
229
+ cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded. File is located at {zip_file_path}")
208
230
  return False
209
231
  return True
210
232
 
@@ -600,8 +622,12 @@ class CloudVerification:
600
622
  # We need to strip "../" path component from all file paths because
601
623
  # unzip will also do that.
602
624
  solana_jar_settings = []
603
- for file in self.context.files:
604
- solana_jar_settings.append(file.split('../')[-1])
625
+ if hasattr(self.context, 'build_script') and self.context.build_script:
626
+ solana_jar_settings.append(Path(self.context.rust_executables).name)
627
+
628
+ else:
629
+ for file in self.context.files:
630
+ solana_jar_settings.append(file.split('../')[-1])
605
631
 
606
632
  is_file = False
607
633
  for arg in jar_settings:
@@ -616,18 +642,19 @@ class CloudVerification:
616
642
  elif arg == '-solanaSummaries':
617
643
  is_file = True
618
644
  auth_data["jarSettings"] = solana_jar_settings
619
- elif Attrs.is_wasm_app():
645
+ elif Attrs.is_soroban_app():
620
646
  # We need to strip "../" path component from all file paths because
621
647
  # unzip will also do that.
622
- wasm_jar_settings = []
623
- if hasattr(self.context, 'wasm_file') and self.context.wasm_file:
624
- wasm_jar_settings.append(self.context.wasm_file.name) # assuming root is cargo_directory
648
+ soroban_jar_settings = []
649
+ # not needed - should be in files
650
+ if hasattr(self.context, 'build_script') and self.context.build_script:
651
+ soroban_jar_settings.append(Path(self.context.rust_executables).name)
625
652
  else:
626
653
  for file in self.context.files:
627
- wasm_jar_settings.append(file.split('../')[-1])
654
+ soroban_jar_settings.append(file.split('../')[-1])
628
655
  for arg in jar_settings:
629
- wasm_jar_settings.append(arg)
630
- auth_data["jarSettings"] = wasm_jar_settings
656
+ soroban_jar_settings.append(arg)
657
+ auth_data["jarSettings"] = soroban_jar_settings
631
658
  else:
632
659
  auth_data["jarSettings"] = jar_settings
633
660
 
@@ -744,34 +771,51 @@ class CloudVerification:
744
771
  paths.append(Path(self.context.bytecode_spec))
745
772
  result = compress_files(self.ZipFilePath, *paths,
746
773
  short_output=Ctx.is_minimal_cli_output(self.context))
747
- elif Attrs.is_solana_app():
748
- # We zip the ELF files and the two configuration files
749
- jar_args = Ctx.collect_jar_args(self.context)
750
- paths = []
751
- for file in self.context.files:
752
- paths.append(Path(file))
753
- is_file = False
754
- for arg in jar_args:
755
- if is_file:
756
- paths.append(Path(arg))
757
- is_file = False
774
+ elif Attrs.is_rust_app():
775
+ files_list = [Util.get_certora_metadata_file()]
776
+
777
+ if hasattr(self.context, 'build_script') and self.context.build_script:
778
+ if self.context.prover_resource_files:
779
+ for value in self.context.prover_resource_files:
780
+ _, file_path = value.split(':')
781
+ cur_path = (Path(self.context.conf_file).parent / Path(file_path)).resolve()
782
+ shutil.copy(cur_path, Util.get_build_dir())
783
+ files_list.append(Util.get_build_dir() / cur_path.name)
784
+
785
+ result = zip_rust_files(self.ZipFilePath, self.context, *files_list)
786
+
787
+ elif Attrs.is_solana_app():
788
+ # We zip the ELF files and the two configuration files
789
+ jar_args = Ctx.collect_jar_args(self.context)
758
790
 
759
- if arg == '-solanaInlining':
760
- is_file = True
761
- elif arg == '-solanaSummaries':
762
- is_file = True
763
- result = compress_files(self.ZipFilePath, *paths,
764
- short_output=Ctx.is_minimal_cli_output(self.context))
765
- elif Attrs.is_wasm_app():
766
- # We zip the wat file
767
- paths = []
768
- if hasattr(self.context, 'wasm_file') and self.context.wasm_file:
769
- result = compress_files_wasm(self.ZipFilePath, self.context)
770
- else:
771
791
  for file in self.context.files:
772
- paths.append(Path(file))
773
- result = compress_files(self.ZipFilePath, *paths,
792
+ files_list.append(Path(file))
793
+ is_file = False
794
+ for arg in jar_args:
795
+ if is_file:
796
+ files_list.append(Path(arg))
797
+ is_file = False
798
+
799
+ if arg == '-solanaInlining':
800
+ is_file = True
801
+ elif arg == '-solanaSummaries':
802
+ is_file = True
803
+ result = compress_files(self.ZipFilePath, *files_list,
774
804
  short_output=Ctx.is_minimal_cli_output(self.context))
805
+
806
+ elif Attrs.is_soroban_app():
807
+ # We zip the wat file
808
+ for file in self.context.files:
809
+ files_list.append(Path(file))
810
+ result = compress_files(self.ZipFilePath, *files_list,
811
+ short_output=Ctx.is_minimal_cli_output(self.context))
812
+ else:
813
+ raise Util.CertoraUserInputError(
814
+ 'When running rust application, context should either have attribute "rust_executables" '
815
+ 'provided by build_script execution, '
816
+ 'or either is_solana_app(), is_soroban_app() should be set to True'
817
+ )
818
+
775
819
  else:
776
820
  # Zip the log file first separately and again with the rest of the files, so it will not be decompressed
777
821
  # on each run in order to save space
@@ -53,7 +53,6 @@ def read_from_conf_file(context: CertoraContext) -> None:
53
53
  context namespace if the key was not set in the command line (command line shadows conf data).
54
54
  @param context: A namespace containing options from the command line
55
55
  """
56
-
57
56
  conf_file_path = Path(context.files[0])
58
57
  assert conf_file_path.suffix == ".conf", f"conf file must be of type .conf, instead got {conf_file_path}"
59
58
 
@@ -86,6 +85,11 @@ def check_conf_content(conf: Dict[str, Any], context: CertoraContext) -> None:
86
85
  f" file ({conf_val})")
87
86
  else:
88
87
  raise Util.CertoraUserInputError(f"{option} appears in the conf file but is not a known attribute. ")
88
+
89
89
  if 'files' not in conf:
90
- raise Util.CertoraUserInputError("Mandatory 'files' attribute is missing from the configuration")
91
- context.files = conf['files'] # Override the current .conf file
90
+ if Attrs.is_rust_app() and hasattr(context, 'build_script') and context.build_script is not None:
91
+ context.files = conf.get('files', None) # Override the current .conf file
92
+ else:
93
+ raise Util.CertoraUserInputError("Mandatory 'files' attribute is missing from the configuration")
94
+ else:
95
+ context.files = conf['files'] # Override the current .conf file
@@ -5,19 +5,13 @@ import logging
5
5
  import os
6
6
  import re
7
7
  import sys
8
- import shutil
9
- import platform
10
8
 
11
- # tomllib was added to python 3.11 this import is for supporting older versions
12
- if sys.version_info >= (3, 11):
13
- import tomllib
14
- else:
15
- import tomli as tomllib
16
9
 
17
10
  from pathlib import Path
18
11
  from typing import Dict, List, Optional, Any
19
12
  from rich.console import Console
20
13
 
14
+ from EVMVerifier.certoraParseBuildScript import run_script_and_parse_json
21
15
 
22
16
  scripts_dir_path = Path(__file__).parent.resolve() # containing directory
23
17
  sys.path.insert(0, str(scripts_dir_path))
@@ -112,15 +106,10 @@ def get_local_run_cmd(context: CertoraContext) -> str:
112
106
  @return: A command for running the prover locally
113
107
  """
114
108
  run_args = []
115
- if context.is_tac or Attrs.is_solana_app():
109
+ if context.is_tac or Attrs.is_rust_app():
110
+ # For Rust app we assume the files holds the executable for the prover, currently we support a single file
116
111
  run_args.append(context.files[0])
117
112
 
118
- if Attrs.is_wasm_app():
119
- if hasattr(context, 'wasm_file') and context.wasm_file:
120
- run_args.append(str(context.wasm_file))
121
- else:
122
- run_args.append(context.files[0])
123
-
124
113
  if Attrs.is_evm_app() and context.cache is not None:
125
114
  run_args.extend(['-cache', context.cache])
126
115
 
@@ -158,7 +147,7 @@ class ProverParser(AttrUtil.ContextAttributeParser):
158
147
  console = Console()
159
148
  console.print("\n\nThe Certora Prover - A formal verification tool for smart contracts")
160
149
  # Using sys.stdout.write() as print() would color some of the strings here
161
- sys.stdout.write("\n\nUsage: certoraRun <Files> <Flags>\n\n")
150
+ sys.stdout.write(f"\n\nUsage: {sys.argv[0]} <Files> <Flags>\n\n")
162
151
  sys.stdout.write("Files are Solidity, Vyper contract files, a shared object Solana contract file, "
163
152
  "or a conf file. Solidity contracts are denoted as file:contract "
164
153
  "e.g. f.sol:A. If the contract name is identical to the file name, "
@@ -171,7 +160,7 @@ class ProverParser(AttrUtil.ContextAttributeParser):
171
160
  console.print("3. list (L): gets multiple strings as a value, flags may also appear multiple times")
172
161
  console.print("4. map (M): collection of key, value pairs\n\n")
173
162
 
174
- Attrs.EvmProverAttributes.print_attr_help()
163
+ Attrs.get_attribute_class().print_attr_help()
175
164
  console.print("\n\nYou can find detailed documentation of the supported flags here: "
176
165
  f"{Util.print_rich_link(CLI_DOCUMENTATION_URL)}\n\n")
177
166
 
@@ -527,52 +516,15 @@ def run_local_spec_check(with_typechecking: bool, context: CertoraContext) -> No
527
516
  raise Util.CertoraUserInputError("Cannot run local checks because of missing a suitable java installation. "
528
517
  "To skip local checks run with the --disable_local_typechecking flag")
529
518
 
530
- def set_cargo_name(context: CertoraContext) -> None:
531
- mode = "r" if platform.system().lower() == 'windows' else "rb"
532
- with open(context.cargo_file, mode) as f:
533
- cargo_data = tomllib.load(f)
534
-
535
- if 'package' in cargo_data and 'name' in cargo_data['package']:
536
- context.cargo_name = cargo_data['package']['name'].replace('-', '_')
537
-
538
- def build_wasm(context: CertoraContext) -> None:
539
- if len(context.files) != 1:
540
- raise Util.CertoraUserInputError(f"expected a single wasm file ({Util.CARGO_FILE} or .wat file)."
541
- f" Got: {context.files}")
542
- file_path = Path(context.files[0])
543
- if file_path.name != Util.CARGO_FILE:
544
- return
545
- context.cargo_dir = file_path.parent
546
- context.cargo_file = file_path
547
- set_cargo_name(context)
548
- run_wasm_compiler(context)
549
-
550
-
551
- def run_wasm_compiler(context: CertoraContext) -> None:
552
- env = os.environ.copy()
553
- env['RUSTFLAGS'] = "-C strip=none --emit=llvm-ir -C debuginfo=2"
554
- try:
555
- context_logger.info("Executing build process...")
556
-
557
- cmd = f"cargo build --target={context.wasm_target} --release --features certora"
558
- Util.run_shell_command(cmd, env=env, cwd=context.cargo_dir)
559
519
 
560
- context.wasm_file = Path(f"{context.cargo_dir}/target/{context.wasm_target}/release/{context.cargo_name}.wasm")
561
- intermediate_wasm = Path("tmpwasm.wasm")
562
-
563
- wat_file = context.wasm_file.with_suffix(".wat")
564
-
565
- cmd = f"wasm2wat {context.wasm_file} --generate-names -o {wat_file}"
566
- Util.run_shell_command(cmd, cwd=context.cargo_dir, env=env)
567
-
568
- cmd = f"wat2wasm {wat_file} --debug-names -o {intermediate_wasm}"
569
- Util.run_shell_command(cmd, cwd=context.cargo_dir, env=env)
570
-
571
- cmd = f"wasm2wat {intermediate_wasm} -o {wat_file}"
572
- Util.run_shell_command(cmd, cwd=context.cargo_dir, env=env)
573
-
574
- intermediate_wasm.unlink()
575
- shutil.copy(context.wasm_file, context.cargo_dir)
576
-
577
- except Exception as e:
578
- raise RuntimeError(f"Failed to build wasm file\n{e}")
520
+ def build_rust_app(context: CertoraContext) -> None:
521
+ if context.build_script:
522
+ run_script_and_parse_json(context)
523
+ if not context.rust_executables:
524
+ raise Util.CertoraUserInputError("failed to get target executable")
525
+ else:
526
+ if not context.files:
527
+ raise Util.CertoraUserInputError("'files' or 'build_script' must be set for Rust projects")
528
+ if len(context.files) > 1:
529
+ raise Util.CertoraUserInputError("Rust projects must specify exactly one executable in 'files'.")
530
+ context.rust_executables = context.files[0]
@@ -1,4 +1,5 @@
1
1
  import logging
2
+
2
3
  import json5
3
4
  import sys
4
5
  from pathlib import Path
@@ -37,6 +38,17 @@ def validate_prover_args(value: str) -> str:
37
38
 
38
39
 
39
40
  class CommonAttributes(AttrUtil.Attributes):
41
+ COMPILATION_STEPS_ONLY = AttrUtil.AttributeDefinition(
42
+ arg_type=AttrUtil.AttrArgType.BOOLEAN,
43
+ help_msg="Compile the spec and the code without sending a verification request to the cloud",
44
+ default_desc="Sends a request after source compilation and spec syntax checking",
45
+ argparse_args={
46
+ 'action': AttrUtil.STORE_TRUE
47
+ },
48
+ affects_build_cache_key=False,
49
+ disables_build_cache=False
50
+ )
51
+
40
52
  MSG = AttrUtil.AttributeDefinition(
41
53
  attr_validation_func=Vf.validate_msg,
42
54
  help_msg="Add a message description to your run",
@@ -149,6 +161,7 @@ class CommonAttributes(AttrUtil.Attributes):
149
161
  disables_build_cache=False
150
162
  )
151
163
 
164
+
152
165
  class DeprecatedAttributes(AttrUtil.Attributes):
153
166
  AUTO_NONDET_DIFFICULT_INTERNAL_FUNCS = AttrUtil.AttributeDefinition(
154
167
  arg_type=AttrUtil.AttrArgType.BOOLEAN,
@@ -418,17 +431,6 @@ class EvmAttributes(AttrUtil.Attributes):
418
431
  disables_build_cache=False
419
432
  )
420
433
 
421
- COMPILATION_STEPS_ONLY = AttrUtil.AttributeDefinition(
422
- arg_type=AttrUtil.AttrArgType.BOOLEAN,
423
- help_msg="Compile the spec and the code without sending a verification request to the cloud",
424
- default_desc="Sends a request after source compilation and spec syntax checking",
425
- argparse_args={
426
- 'action': AttrUtil.STORE_TRUE
427
- },
428
- affects_build_cache_key=False,
429
- disables_build_cache=False
430
- )
431
-
432
434
  NO_MEMORY_SAFE_AUTOFINDERS = AttrUtil.AttributeDefinition(
433
435
  arg_type=AttrUtil.AttrArgType.BOOLEAN,
434
436
  # This is a hidden flag, the following two attributes are left intentionally as comments to help devs
@@ -1384,6 +1386,20 @@ class BackendAttributes(AttrUtil.Attributes):
1384
1386
  disables_build_cache=False
1385
1387
  )
1386
1388
 
1389
+
1390
+ class RustAttributes(AttrUtil.Attributes):
1391
+
1392
+ BUILD_SCRIPT = AttrUtil.AttributeDefinition(
1393
+ help_msg="Python script to build the project",
1394
+ default_desc="Using default building command",
1395
+ argparse_args={
1396
+ 'action': AttrUtil.UniqueStore
1397
+ },
1398
+ affects_build_cache_key=False,
1399
+ disables_build_cache=False
1400
+ )
1401
+
1402
+
1387
1403
  class EvmProverAttributes(CommonAttributes, DeprecatedAttributes, EvmAttributes, InternalUseAttributes,
1388
1404
  BackendAttributes):
1389
1405
  FILES = AttrUtil.AttributeDefinition(
@@ -1399,9 +1415,9 @@ class EvmProverAttributes(CommonAttributes, DeprecatedAttributes, EvmAttributes,
1399
1415
  )
1400
1416
 
1401
1417
 
1402
- class SorobanProverAttributes(CommonAttributes, InternalUseAttributes, BackendAttributes):
1418
+ class SorobanProverAttributes(CommonAttributes, InternalUseAttributes, BackendAttributes, RustAttributes):
1403
1419
  FILES = AttrUtil.AttributeDefinition(
1404
- attr_validation_func=Vf.validate_wasm_file,
1420
+ attr_validation_func=Vf.validate_soroban_extension,
1405
1421
  arg_type=AttrUtil.AttrArgType.LIST,
1406
1422
  help_msg="binary .wat files for the Prover",
1407
1423
  default_desc="",
@@ -1412,19 +1428,10 @@ class SorobanProverAttributes(CommonAttributes, InternalUseAttributes, BackendAt
1412
1428
  disables_build_cache=False
1413
1429
  )
1414
1430
 
1415
- WASM_TARGET = AttrUtil.AttributeDefinition(
1416
- default_desc="",
1417
- argparse_args={
1418
- 'action': AttrUtil.UniqueStore,
1419
- 'default': "wasm32-unknown-unknown"
1420
- },
1421
- affects_build_cache_key=False,
1422
- disables_build_cache=False
1423
- )
1424
1431
 
1425
- class SolanaProverAttributes(CommonAttributes, InternalUseAttributes, BackendAttributes):
1432
+ class SolanaProverAttributes(CommonAttributes, InternalUseAttributes, BackendAttributes, RustAttributes):
1426
1433
  FILES = AttrUtil.AttributeDefinition(
1427
- attr_validation_func=Vf.validate_solana_file,
1434
+ attr_validation_func=Vf.validate_solana_extension,
1428
1435
  arg_type=AttrUtil.AttrArgType.LIST,
1429
1436
  help_msg="contract files for analysis SOLANA_FILE.so or a conf file",
1430
1437
 
@@ -1460,9 +1467,9 @@ def detect_application_class(args: List[str]) -> Type[AttrUtil.Attributes]:
1460
1467
  def application_by_suffix(file: str) -> Type[AttrUtil.Attributes]:
1461
1468
  if file.endswith(Util.EVM_EXTENSIONS):
1462
1469
  return EvmProverAttributes
1463
- elif file.endswith(Util.WASM_EXTENSIONS) or Path(file).name == Util.CARGO_FILE:
1470
+ elif file.endswith(Util.SOROBAN_EXEC_EXTENSION):
1464
1471
  return SorobanProverAttributes
1465
- elif file.endswith(Util.SOLANA_EXTENSIONS):
1472
+ elif file.endswith(Util.SOLANA_EXEC_EXTENSION):
1466
1473
  return SolanaProverAttributes
1467
1474
  elif file.endswith('.conf'):
1468
1475
  raise Util.CertoraUserInputError(f"Cannot use conf files inside a conf file: {file}")
@@ -1472,6 +1479,7 @@ def detect_application_class(args: List[str]) -> Type[AttrUtil.Attributes]:
1472
1479
  cli_files = []
1473
1480
  cli_conf_files = []
1474
1481
  files = []
1482
+ build_script = None
1475
1483
  for arg in args:
1476
1484
  if arg.startswith('-'):
1477
1485
  break # Stop processing when a flag is detected
@@ -1485,6 +1493,10 @@ def detect_application_class(args: List[str]) -> Type[AttrUtil.Attributes]:
1485
1493
  with conf_file_path.open() as conf_file:
1486
1494
  configuration = json5.load(conf_file, allow_duplicate_keys=False)
1487
1495
  files = configuration.get('files', [])
1496
+ build_script = configuration.get('build_script')
1497
+
1498
+ if build_script:
1499
+ return SorobanProverAttributes
1488
1500
 
1489
1501
  if len(cli_conf_files) == 0:
1490
1502
  files = cli_files
@@ -1516,9 +1528,13 @@ def is_solana_app() -> bool:
1516
1528
  return get_attribute_class() == SolanaProverAttributes
1517
1529
 
1518
1530
 
1519
- def is_wasm_app() -> bool:
1531
+ def is_soroban_app() -> bool:
1520
1532
  return get_attribute_class() == SorobanProverAttributes
1521
1533
 
1522
1534
 
1535
+ def is_rust_app() -> bool:
1536
+ return is_soroban_app() or is_solana_app()
1537
+
1538
+
1523
1539
  def is_evm_app() -> bool:
1524
1540
  return get_attribute_class() == EvmProverAttributes
@@ -409,8 +409,7 @@ def check_contract_name_arg_inputs(context: CertoraContext) -> None:
409
409
  def valid_input_file(filename: str) -> bool:
410
410
  pattern_match_sol = bool(re.match(r'([\w.$]+)\.sol:([\w$]+)', Path(filename).name))
411
411
  ends_with_extension = any(filename.endswith(ext) for ext in Util.VALID_FILE_EXTENSIONS)
412
- is_cargo_file = Path(filename).name == Util.CARGO_FILE
413
- return pattern_match_sol or ends_with_extension or is_cargo_file
412
+ return pattern_match_sol or ends_with_extension
414
413
 
415
414
 
416
415
  def check_files_attribute(context: CertoraContext) -> None:
@@ -492,7 +491,7 @@ def check_mode_of_operation(context: CertoraContext) -> None:
492
491
  raise Util.CertoraUserInputError(
493
492
  f"Cannot use conf files inside a conf file: {','.join(conf_files_inside_conf_file)}")
494
493
 
495
- special_file_suffixes = [".tac", ".json"] + list(Util.SOLANA_EXTENSIONS) + list(Util.WASM_EXTENSIONS)
494
+ special_file_suffixes = [".tac", ".json"] + [Util.SOLANA_EXEC_EXTENSION, Util.SOROBAN_EXEC_EXTENSION]
496
495
  for input_file in context.files:
497
496
  special_file_type = next((suffix for suffix in special_file_suffixes if input_file.endswith(suffix)), None)
498
497
 
@@ -710,7 +709,6 @@ def validate_certora_key() -> str:
710
709
  raise Util.CertoraUserInputError(f"environment variable {KEY_ENV_VAR} has an illegal length")
711
710
  return key
712
711
 
713
-
714
712
  def check_files_input(file_list: List[str]) -> None:
715
713
  """
716
714
  Verifies that correct input was inserted as input to files.
@@ -719,8 +717,8 @@ def check_files_input(file_list: List[str]) -> None:
719
717
  The allowed disjoint cases are:
720
718
  1. Use a single .conf file
721
719
  2. Use a single .tac file
722
- 3. Use a single .o or .so file
723
- 4. Use a single .wat or .wasm file or a single Cargo.toml file
720
+ 3. Use a single .so file
721
+ 4. Use a single .wasm file
724
722
  5. Use any number of [contract.sol:nickname ...] (at least one is guaranteed by argparser)
725
723
  @param file_list: A list of strings representing file paths
726
724
  @raise CertoraUserInputError if more than one of the modes above was used
@@ -733,13 +731,10 @@ def check_files_input(file_list: List[str]) -> None:
733
731
  if '.conf' in file:
734
732
  raise Util.CertoraUserInputError(
735
733
  f'The conf file {file} cannot be accompanied with other files')
736
- if file.endswith(Util.SOLANA_EXTENSIONS):
734
+ if file.endswith(Util.SOLANA_EXEC_EXTENSION):
737
735
  raise Util.CertoraUserInputError(f'The Solana file {file} cannot be accompanied with other files')
738
- if file.endswith(Util.WASM_EXTENSIONS) or file == Util.CARGO_FILE:
739
- raise Util.CertoraUserInputError(
740
- f'When using the tool in WASM mode, you can only provide a single Cargo.toml file or .wat file.'
741
- f'{num_files} files were given.')
742
-
736
+ if file.endswith(Util.SOROBAN_EXEC_EXTENSION):
737
+ raise Util.CertoraUserInputError(f'The Soroban file {file} cannot be accompanied with other files')
743
738
 
744
739
  def set_wait_for_results_default(context: CertoraContext) -> None:
745
740
  if context.wait_for_results is None:
@@ -0,0 +1,36 @@
1
+ import subprocess
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from EVMVerifier.certoraContextClass import CertoraContext
6
+ from Shared import certoraUtils as Util
7
+ import os
8
+
9
+
10
+ def run_script_and_parse_json(context: CertoraContext) -> None:
11
+ if not context.build_script:
12
+ return
13
+ try:
14
+ env = os.environ.copy()
15
+ result = subprocess.run(["python3", Path(context.build_script).resolve(), '--json'], capture_output=True, text=True,
16
+ env=env, cwd=Path(context.build_script).resolve().parent)
17
+
18
+ # Check if the script executed successfully
19
+ if result.returncode != 0:
20
+ raise Util.CertoraUserInputError(f"Error running the script {context.build_script}\n{result.stderr}")
21
+
22
+ json_obj = json.loads(result.stdout)
23
+
24
+ if not json_obj or not json_obj.get("success"):
25
+ raise Util.CertoraUserInputError(f"{result.stderr}\nBuild from {context.build_script} failed")
26
+
27
+ context.rust_project_directory = json_obj.get("project_directory")
28
+ context.rust_sources = json_obj.get("sources")
29
+ context.rust_executables = json_obj.get("executables")
30
+
31
+ except FileNotFoundError as e:
32
+ raise Util.CertoraUserInputError(f"File not found: {e}")
33
+ except json.JSONDecodeError as e:
34
+ raise Util.CertoraUserInputError(f"Error decoding JSON: {e}")
35
+ except Exception as e:
36
+ raise Util.CertoraUserInputError(f"An unexpected error occurred: {e}")
@@ -715,11 +715,7 @@ class MutateApp:
715
715
  self.server = self.config_server()
716
716
 
717
717
  def is_soroban_run(self) -> bool:
718
- try:
719
- files = self.prover_context.files
720
- except AttributeError:
721
- return False
722
- return Util.CARGO_FILE in files or any(Path(path).suffix == '.wasm' for path in files)
718
+ return (hasattr(self.prover_context, 'build_script') and self.prover_context is not None)
723
719
 
724
720
  def config_server(self) -> str:
725
721
  """
@@ -873,6 +869,16 @@ class MutateApp:
873
869
  self.backup_paths = []
874
870
 
875
871
  def submit_soroban(self) -> None:
872
+
873
+ try:
874
+ original_run_result = self.run_certora_prover(self.conf, self.mutation_test_id, msg=MConstants.ORIGINAL)
875
+ except Exception as e:
876
+ raise Util.CertoraUserInputError(f"Orig run: {e}")
877
+ if not original_run_result:
878
+ raise Util.CertoraUserInputError("No Orig results")
879
+ result_link = original_run_result.rule_report_link
880
+ assert result_link, "submit_soroban: Null result_link"
881
+
876
882
  mutation_logger.info("Generating mutants and submitting...")
877
883
  generated_mutants: List[Mutant] = self.get_universal_mutator_mutants()
878
884
 
@@ -1341,7 +1347,8 @@ class MutateApp:
1341
1347
  for mutant in self.universal_mutator:
1342
1348
  file_to_mutate = Path(os.path.normpath(mutant[MConstants.FILE_TO_MUTATE]))
1343
1349
  mutants_location = Path(mutant[MConstants.MUTANTS_LOCATION])
1344
- run_universal_mutator(file_to_mutate, mutants_location)
1350
+ num_of_mutants = mutant[MConstants.NUM_MUTANTS]
1351
+ run_universal_mutator(file_to_mutate, self.prover_context.build_script, mutants_location, num_of_mutants)
1345
1352
  self.add_dir_to_mutants(ret_mutants, mutants_location, file_to_mutate)
1346
1353
  return ret_mutants
1347
1354
 
@@ -1784,7 +1791,9 @@ class MutateApp:
1784
1791
 
1785
1792
  # all keys in prover_context must exist as attribute of certoraRun
1786
1793
  def check_prover_context(self) -> None:
1787
- prover_attrs = [attr.get_conf_key() for attr in Attrs.EvmProverAttributes.attribute_list()]
1794
+ attributes = Attrs.SorobanProverAttributes.attribute_list() if self.is_soroban_run() \
1795
+ else Attrs.EvmProverAttributes.attribute_list()
1796
+ prover_attrs = [attr.get_conf_key() for attr in attributes]
1788
1797
  for key in vars(self.prover_context).keys():
1789
1798
  if key not in prover_attrs:
1790
1799
  raise Util.CertoraUserInputError(f"{key} not a valid attribute in a conf file")
@@ -78,7 +78,7 @@ RECENT_JOBS_FILE = Path(".certora_recent_jobs.json")
78
78
  LAST_CONF_FILE = Path("run.conf")
79
79
  EMV_JAR = Path("emv.jar")
80
80
  CERTORA_SOURCES = Path(".certora_sources")
81
-
81
+ SOLANA_DEFAULT_COMMAND = "cargo +solana build-sbf"
82
82
  ALPHA_PACKAGE_NAME = 'certora-cli-alpha-master'
83
83
  # contract names in Solidity consists of alphanums, underscores and dollar signs
84
84
  # https://docs.soliditylang.org/en/latest/grammar.html#a4.SolidityLexer.Identifier
@@ -89,11 +89,10 @@ NEW_LINE = '\n' # for new lines in f strings
89
89
  VY_EXT = '.vy'
90
90
  SOL_EXT = '.sol'
91
91
  YUL_EXT = '.yul'
92
- CARGO_FILE = "Cargo.toml"
93
92
  EVM_EXTENSIONS = ('.sol', VY_EXT, YUL_EXT, '.tac', '.json')
94
- SOLANA_EXTENSIONS = ('.so', '.o')
95
- WASM_EXTENSIONS = ('.wat', '.wasm')
96
- VALID_FILE_EXTENSIONS = ['.conf'] + list(EVM_EXTENSIONS) + list(SOLANA_EXTENSIONS) + list(WASM_EXTENSIONS)
93
+ SOLANA_EXEC_EXTENSION = '.so'
94
+ SOROBAN_EXEC_EXTENSION = '.wasm'
95
+ VALID_FILE_EXTENSIONS = ['.conf'] + list(EVM_EXTENSIONS) + [SOLANA_EXEC_EXTENSION, SOROBAN_EXEC_EXTENSION]
97
96
  # Type alias definition, not a variable
98
97
  CompilerVersion = Tuple[int, int, int]
99
98
 
@@ -1241,7 +1240,7 @@ class TestValue(NoValEnum):
1241
1240
  AFTER_COLLECT = auto()
1242
1241
  AFTER_BUILD_MUTANTS_DIRECTORY = auto()
1243
1242
  AFTER_GENERATE_COLLECT_REPORT = auto()
1244
- AFTER_BUILD_WASM = auto()
1243
+ AFTER_BUILD_RUST = auto()
1245
1244
 
1246
1245
 
1247
1246
  class FeValue(NoValEnum):
@@ -1368,9 +1367,7 @@ def get_ir_flag(solc: str) -> str:
1368
1367
  return ''
1369
1368
 
1370
1369
 
1371
- def run_shell_command(cmd: str, env: Dict[str, str], cwd: Optional[Path]) -> None:
1372
- if not cwd:
1373
- cwd = Path.cwd()
1370
+ def run_shell_command(cmd: Union[str, List[str]], env: Dict[str, str] = os.environ.copy(), cwd: Path = Path.cwd()) -> None:
1374
1371
  try:
1375
1372
  print(f"Executing: {cmd}")
1376
1373
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True, env=env, cwd=cwd)
@@ -244,6 +244,17 @@ def validate_optional_readable_file(filename: str) -> str:
244
244
  def validate_spec_file(filename: str) -> str:
245
245
  return validate_readable_file(filename, (".spec", ".cvl"))
246
246
 
247
+ def validate_soroban_extension(filename: str) -> str:
248
+ if not filename.lower().endswith(Util.SOROBAN_EXEC_EXTENSION):
249
+ raise Util.CertoraUserInputError(f"{filename} does not end with {Util.SOROBAN_EXEC_EXTENSION}")
250
+ return filename
251
+
252
+
253
+ def validate_solana_extension(filename: str) -> str:
254
+ if not filename.lower().endswith(Util.SOLANA_EXEC_EXTENSION):
255
+ raise Util.CertoraUserInputError(f"{filename} does not end with {Util.SOLANA_EXEC_EXTENSION}")
256
+ return filename
257
+
247
258
 
248
259
  def validate_readable_file(filename: str, extensions: Union[str, tuple] = '') -> str:
249
260
  file_path = Path(filename)
@@ -253,8 +264,6 @@ def validate_readable_file(filename: str, extensions: Union[str, tuple] = '') ->
253
264
  raise Util.CertoraUserInputError(f"'{filename}' is a directory and not a file")
254
265
  if not os.access(filename, os.R_OK):
255
266
  raise Util.CertoraUserInputError(f"no read permissions for {filename}")
256
- if Path(filename).name == Util.CARGO_FILE:
257
- return filename
258
267
  if extensions and not filename.lower().endswith(extensions):
259
268
  raise Util.CertoraUserInputError(f"{filename} does not end with {extensions}")
260
269
 
@@ -375,14 +384,12 @@ def validate_exec_file(file_name: str) -> str:
375
384
  return file_name
376
385
 
377
386
 
378
- def validate_wasm_file(file: str) -> str:
379
- if file == Util.CARGO_FILE:
380
- return validate_readable_file(Util.CARGO_FILE)
381
- return validate_readable_file(file, Util.WASM_EXTENSIONS)
387
+ def validate_soroban_file(file: str) -> str:
388
+ return validate_readable_file(file, Util.SOROBAN_EXEC_EXTENSION)
382
389
 
383
390
 
384
391
  def validate_solana_file(file: str) -> str:
385
- return validate_readable_file(file, Util.SOLANA_EXTENSIONS)
392
+ return validate_readable_file(file, Util.SOLANA_EXEC_EXTENSION)
386
393
 
387
394
 
388
395
  def validate_contract_extension_attr(map: Any) -> Dict[str, List[Dict[str, Any]]]:
certora_cli/certoraRun.py CHANGED
@@ -24,7 +24,6 @@ from EVMVerifier import certoraContextValidator as Cv
24
24
  import EVMVerifier.certoraContextAttributes as Attrs
25
25
  from Shared import certoraAttrUtil as AttrUtil
26
26
 
27
-
28
27
  BUILD_SCRIPT_PATH = Path("EVMVerifier/certoraBuild.py")
29
28
  VIOLATIONS_EXIT_CODE = 100
30
29
 
@@ -95,9 +94,9 @@ def run_certora(args: List[str], attrs_class: Optional[Type[AttrUtil.Attributes]
95
94
  raise Util.TestResultsReady(metadata)
96
95
  metadata.dump()
97
96
 
98
- if Attrs.is_solana_app() or Attrs.is_wasm_app():
99
- if Attrs.is_wasm_app():
100
- Ctx.build_wasm(context)
97
+ if Attrs.is_rust_app():
98
+ Ctx.build_rust_app(context)
99
+
101
100
  if context.local:
102
101
  check_cmd = Ctx.get_local_run_cmd(context)
103
102
  print(f"Verifier run command:\n {check_cmd}", flush=True)
@@ -244,7 +243,6 @@ def entry_point() -> None:
244
243
  It is important this function gets no arguments!
245
244
  """
246
245
  try:
247
- Attrs.set_attribute_class(Attrs.EvmProverAttributes)
248
246
  run_certora(sys.argv[1:])
249
247
  sys.exit(0)
250
248
  except KeyboardInterrupt:
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import sys
4
+ import time
5
+ import logging
6
+ from typing import List, Optional, Type
7
+ from pathlib import Path
8
+ from rich.console import Console
9
+
10
+ scripts_dir_path = Path(__file__).parent.resolve() # containing directory
11
+ sys.path.insert(0, str(scripts_dir_path))
12
+
13
+ from Shared import certoraUtils as Util
14
+ from Shared import certoraAttrUtil as AttrUtil
15
+ from Shared.certoraLogging import LoggingManager
16
+
17
+ from EVMVerifier.certoraCloudIO import CloudVerification, validate_version_and_branch
18
+ from EVMVerifier.certoraCollectRunMetadata import collect_run_metadata
19
+ from EVMVerifier import certoraContextValidator as Cv
20
+
21
+ import EVMVerifier.certoraContext as Ctx
22
+ import EVMVerifier.certoraContextAttributes as Attrs
23
+
24
+ from certoraRun import CertoraRunResult, VIOLATIONS_EXIT_CODE, CertoraFoundViolations
25
+
26
+ # logger for issues regarding the general run flow.
27
+ # Also serves as the default logger for errors originating from unexpected places.
28
+ run_logger = logging.getLogger("run")
29
+
30
+
31
+ def run_solana_prover(args: List[str], attrs_class: Optional[Type[AttrUtil.Attributes]] = None) \
32
+ -> Optional[CertoraRunResult]:
33
+ """
34
+ The main function that is responsible for the general flow of the script.
35
+ The general flow is:
36
+ 1. Parse program arguments
37
+ 2. Run the necessary steps (build/ cloud verification/ local verification)
38
+ """
39
+
40
+ Attrs.set_attribute_class(Attrs.SolanaProverAttributes)
41
+ non_str_els = [x for x in args if not isinstance(x, str)]
42
+ if non_str_els:
43
+ print(f"args for run_certora that are not strings: {non_str_els}")
44
+ exit(1)
45
+
46
+ # If we are not in debug mode, we do not want to print the traceback in case of exceptions.
47
+ if '--debug' not in args: # We check manually, because we want no traceback in argument parsing exceptions
48
+ sys.tracebacklimit = 0
49
+
50
+ # creating the default internal dir, files may be copied to user defined build directory after
51
+ # parsing the input
52
+
53
+ if not ('--help' in args or '--version' in args):
54
+ Util.reset_certora_internal_dir()
55
+ Util.safe_create_dir(Util.get_build_dir())
56
+ logging_manager = LoggingManager()
57
+
58
+ Ctx.handle_flags_in_args(args)
59
+ context = Ctx.get_args(args) # Parse arguments
60
+ logging_manager.set_log_level_and_format(is_quiet=Ctx.is_minimal_cli_output(context),
61
+ debug=context.debug,
62
+ debug_topics=context.debug_topics,
63
+ show_debug_topics=context.show_debug_topics)
64
+
65
+ timings = {}
66
+ exit_code = 0 # The exit code of the script. 0 means success, any other number is an error.
67
+ return_value = None
68
+
69
+ metadata = (
70
+ collect_run_metadata(wd=Path.cwd(), raw_args=sys.argv, context=context))
71
+
72
+ if context.test == str(Util.TestValue.CHECK_METADATA):
73
+ raise Util.TestResultsReady(metadata)
74
+ metadata.dump()
75
+
76
+ # Build Solana - If input file is .so or .o file, we skip building part
77
+ run_logger.debug("Build Solana target")
78
+ build_start = time.perf_counter()
79
+
80
+ Ctx.build_rust_app(context)
81
+ build_end = time.perf_counter()
82
+ timings["buildTime"] = round(build_end - build_start, 4)
83
+ if context.test == str(Util.TestValue.AFTER_BUILD):
84
+ raise Util.TestResultsReady(None)
85
+
86
+ if context.local:
87
+ check_cmd = Ctx.get_local_run_cmd(context)
88
+ print(f"Verifier run command:\n {check_cmd}", flush=True)
89
+
90
+ compare_with_tool_output = False
91
+ run_result = Util.run_jar_cmd(check_cmd, compare_with_tool_output, logger_topic="verification",
92
+ print_output=True)
93
+
94
+ if run_result != 0:
95
+ exit_code = 1
96
+ else:
97
+ Util.print_completion_message("Finished running verifier:")
98
+ print(f"\t{check_cmd}")
99
+ else:
100
+ validate_version_and_branch(context)
101
+ context.key = Cv.validate_certora_key()
102
+ cloud_verifier = CloudVerification(context, timings)
103
+ # Wrap strings with space with ' so it can be copied and pasted to shell
104
+ pretty_args = [f"'{arg}'" if ' ' in arg else arg for arg in args]
105
+ cl_args = ' '.join(pretty_args)
106
+ logging_manager.remove_debug_logger()
107
+ result = cloud_verifier.cli_verify_and_report(cl_args, context.wait_for_results)
108
+ if cloud_verifier.statusUrl:
109
+ return_value = CertoraRunResult(cloud_verifier.statusUrl, False,
110
+ Util.get_certora_sources_dir(), cloud_verifier.reportUrl)
111
+ if not result:
112
+ exit_code = 1
113
+
114
+ if exit_code == VIOLATIONS_EXIT_CODE:
115
+ raise CertoraFoundViolations("violations were found", return_value)
116
+ if exit_code != 0:
117
+ raise Util.CertoraUserInputError(f"run_certora failed (code {exit_code})")
118
+ return return_value
119
+
120
+
121
+ def entry_point() -> None:
122
+ """
123
+ This function is the entry point of the certora_cli customer-facing package, as well as this script.
124
+ It is important this function gets no arguments!
125
+ """
126
+ try:
127
+ run_solana_prover(sys.argv[1:])
128
+ sys.exit(0)
129
+ except KeyboardInterrupt:
130
+ Console().print("[bold red]\nInterrupted by user")
131
+ sys.exit(1)
132
+ except CertoraFoundViolations as e:
133
+ try:
134
+ if e.results and e.results.rule_report_link:
135
+ print(f"report url: {e.results.rule_report_link}")
136
+ except Exception:
137
+ pass
138
+ Console().print("[bold red]\nViolations were found\n")
139
+ sys.exit(1)
140
+ except Util.CertoraUserInputError as e:
141
+ if e.orig:
142
+ print(f"\n{str(e.orig).strip()}")
143
+ if e.more_info:
144
+ print(f"\n{e.more_info.strip()}")
145
+ Console().print(f"[bold red]\n{e}\n")
146
+ sys.exit(1)
147
+ except Exception as e:
148
+ Console().print(f"[bold red]{e}")
149
+ sys.exit(1)
150
+
151
+
152
+ if __name__ == '__main__':
153
+ entry_point()
@@ -6,8 +6,8 @@ import argparse
6
6
  from pathlib import Path
7
7
  import logging
8
8
  import re
9
- import os
10
9
  import random
10
+ from tqdm import tqdm
11
11
  from typing import List, Tuple
12
12
 
13
13
  from Shared import certoraUtils as Util
@@ -37,6 +37,13 @@ def parse_args() -> argparse.Namespace:
37
37
  required=True,
38
38
  help="Path to the Rust source file to mutate (e.g., src/lib.rs)"
39
39
  )
40
+ parser.add_argument(
41
+ "--build_script",
42
+ "-b",
43
+ type=str,
44
+ required=True,
45
+ help = "Custom build command to execute for each mutant (e.g., 'cargo build --release')"
46
+ )
40
47
  parser.add_argument(
41
48
  "--mutants_location",
42
49
  "-m",
@@ -82,16 +89,25 @@ def restore_source(backup_file: Path, file_to_mutant: Path) -> None:
82
89
  rust_mutator_logger.warning(f"No backup file '{backup_file}' found. Skipping restoration.")
83
90
 
84
91
 
85
- def clean_temp_files() -> None:
92
+ def clean_temp_files(file_to_mutant: Path) -> None:
86
93
  """
87
94
  Remove temporary mutant output files matching the pattern '.um.mutant_output.*'.
95
+
96
+ Args:
97
+ file_to_mutant (Path): Path to the original source file.
88
98
  """
89
- temp_files = Path().glob(".um.mutant_output.*")
99
+ temp_files = Path().rglob(".um.mutant_output.*")
90
100
 
91
101
  rust_mutator_logger.info("Removing temporary mutant output files...")
92
102
  for temp_file in temp_files:
93
103
  temp_file.unlink()
94
104
  rust_mutator_logger.info(f"Removed: {temp_file}")
105
+
106
+ backup_file = next(Path().rglob(f"{file_to_mutant}.um.backup.*"), None)
107
+ if backup_file:
108
+ backup_file.unlink()
109
+ rust_mutator_logger.info(f"Removed: {backup_file}")
110
+
95
111
  rust_mutator_logger.info("Temporary files removal completed.")
96
112
 
97
113
 
@@ -224,18 +240,6 @@ def validate_mutant_count(file_to_mutant: Path, mutants_location: Path, num_muta
224
240
  rust_mutator_logger.debug(f"Renamed {mutant} to {new_file_name}")
225
241
  break
226
242
 
227
-
228
- def validate_cargo() -> None:
229
- """
230
- Validate that the cargo.toml file is in the current working directory.
231
-
232
- Raises:
233
- Util.CertoraUserInputError: If validation fails.
234
- """
235
- if not Path(Util.CARGO_FILE).exists():
236
- raise Util.CertoraUserInputError(f"'{Util.CARGO_FILE}' not found in the current working directory: {os.getcwd()}")
237
-
238
-
239
243
  def validate_mutate_command() -> None:
240
244
  """
241
245
  Validate that the universalmutator command is available in the PATH.
@@ -247,15 +251,17 @@ def validate_mutate_command() -> None:
247
251
  raise Util.CertoraUserInputError("universalmutator command 'mutate' not found in PATH.")
248
252
 
249
253
 
250
- def run_mutate(file_to_mutant: Path, mutants_location: Path, build_command: str, debug: bool = False) -> None:
254
+ def run_mutate(file_to_mutant: Path, mutants_location: Path, build_command: str) -> None:
251
255
  """
252
- Execute the universalmutator command to generate mutants.
256
+ Execute the universalmutator command to generate mutants, displaying a progress bar.
253
257
 
254
258
  Args:
255
259
  file_to_mutant (Path): Path to the Rust source file.
256
260
  mutants_location (Path): Directory to store generated mutants.
257
261
  build_command (str): Command to execute for each mutant to verify compilation.
258
- debug (bool): Enable debug logging.
262
+
263
+ Raises:
264
+ subprocess.CalledProcessError: If the mutate command fails.
259
265
  """
260
266
  mutate_command = [
261
267
  "mutate",
@@ -266,22 +272,87 @@ def run_mutate(file_to_mutant: Path, mutants_location: Path, build_command: str,
266
272
  "--cmd",
267
273
  build_command
268
274
  ]
269
-
270
275
  rust_mutator_logger.info("Generating mutants...")
271
276
  rust_mutator_logger.debug(f"Running universalmutator with command: {' '.join(mutate_command)}")
272
- if not debug:
273
- subprocess.run(mutate_command, check=True, stdout=subprocess.DEVNULL)
274
- else:
275
- subprocess.run(mutate_command, check=True)
276
- rust_mutator_logger.info("Mutation generation completed successfully.")
277
277
 
278
+ # Initialize the subprocess with real-time output capture
279
+ process = subprocess.Popen(
280
+ mutate_command,
281
+ stdout=subprocess.PIPE,
282
+ stderr=subprocess.STDOUT,
283
+ universal_newlines=True,
284
+ bufsize=1
285
+ )
286
+
287
+ total_mutants = None
288
+ mutant_pattern = re.compile(r"^PROCESSING MUTANT: (\d+):")
289
+ total_pattern = re.compile(r"^(\d+) MUTANTS GENERATED BY RULES")
290
+ valid_mutant_pattern = re.compile(rf"VALID \[written to {mutants_location.stem}/{file_to_mutant.stem}\.mutant\.\d+\.rs\]")
291
+
292
+ progress_bar = None
293
+
294
+ try:
295
+ if not process.stdout:
296
+ rust_mutator_logger.error("Failed to capture output from the mutate command.")
297
+ raise subprocess.CalledProcessError(1, mutate_command)
298
+
299
+ for line in process.stdout:
300
+ line = line.strip()
301
+ rust_mutator_logger.debug(line)
302
+
303
+ # Parse total number of mutants from the initial output
304
+ if total_mutants is None:
305
+ total_match = total_pattern.match(line)
306
+ if total_match:
307
+ total_mutants = int(total_match.group(1))
308
+ progress_bar = tqdm(total=total_mutants, desc="Mutants Generated", unit="mutant")
309
+ rust_mutator_logger.info(f"Total mutants to generate: {total_mutants}")
310
+ continue
311
+
312
+ # Update progress bar for each processed mutant
313
+ mutant_match = mutant_pattern.match(line)
314
+ if mutant_match and progress_bar:
315
+ progress_bar.update(1)
316
+ continue
317
+
318
+ # Optionally, handle validation messages
319
+ if valid_mutant_pattern.search(line):
320
+ rust_mutator_logger.info(f"Valid mutant generated: {line}")
321
+
322
+ process.wait()
278
323
 
279
- def run_universal_mutator(file_to_mutant: Path, mutants_location: Path, num_mutants: int = NUM_MUTANTS, seed: int = SEED, debug: bool = False) -> None:
324
+ if progress_bar:
325
+ progress_bar.close()
326
+
327
+ if process.returncode != 0:
328
+ rust_mutator_logger.error(f"Mutation generation failed with return code {process.returncode}")
329
+ raise subprocess.CalledProcessError(process.returncode, mutate_command)
330
+
331
+ rust_mutator_logger.info("Mutation generation completed successfully.")
332
+
333
+ except Exception as e:
334
+ if progress_bar:
335
+ progress_bar.close()
336
+ rust_mutator_logger.error(f"An error occurred during mutation generation: {e}")
337
+ raise
338
+ finally:
339
+ if process and process.poll() is None:
340
+ process.terminate()
341
+
342
+ def run_universal_mutator(
343
+ file_to_mutant: Path,
344
+ build_script: str,
345
+ mutants_location: Path,
346
+ num_mutants: int = NUM_MUTANTS,
347
+ seed: int = SEED,
348
+ debug: bool = False
349
+ ) -> None:
280
350
  f"""
281
351
  Generate mutants for the specified source file and ensure the original file remains unchanged.
282
352
 
283
353
  Args:
284
354
  file_to_mutant (Path): Path to the Rust source file.
355
+ build_script (str): Command to execute for each mutant to verify compilation.
285
356
  mutants_location (Path): Directory to store generated mutants.
286
357
  num_mutants (int): Upper bound on the number of mutants to generate (default: {NUM_MUTANTS}).
287
358
  seed (int): Seed value for random selection to ensure repeatable testing (default: {SEED}).
@@ -291,14 +362,9 @@ def run_universal_mutator(file_to_mutant: Path, mutants_location: Path, num_muta
291
362
  Util.CertoraUserInputError: If any validation fails.
292
363
  Exception: For unexpected errors.
293
364
  """
294
-
295
- # Define the build command, ensuring it dynamically references the correct source file
296
- build_command = (
297
- f'cp MUTANT {file_to_mutant} && '
298
- 'RUSTFLAGS="-C strip=none --emit=llvm-ir" '
299
- 'cargo build --target=wasm32-unknown-unknown --release --features certora'
300
- )
301
365
  backup_file = None
366
+ build_command = f"cp MUTATE & python3 {build_script}"
367
+
302
368
  try:
303
369
  # Set up the logger
304
370
  setup_logger(debug)
@@ -306,9 +372,6 @@ def run_universal_mutator(file_to_mutant: Path, mutants_location: Path, num_muta
306
372
  # Validate mutate command is available
307
373
  validate_mutate_command()
308
374
 
309
- # Validate cargo.toml is in CWD
310
- validate_cargo()
311
-
312
375
  # Validate source file
313
376
  validate_source_file(file_to_mutant)
314
377
 
@@ -324,7 +387,7 @@ def run_universal_mutator(file_to_mutant: Path, mutants_location: Path, num_muta
324
387
  validate_mutant_dir(mutants_location)
325
388
 
326
389
  # Run the mutation command
327
- run_mutate(file_to_mutant, mutants_location, build_command, debug)
390
+ run_mutate(file_to_mutant, mutants_location, build_command)
328
391
 
329
392
  # Make sure the number of mutants generated is less than or equal to the specified limit
330
393
  validate_mutant_count(file_to_mutant, mutants_location, num_mutants, seed)
@@ -334,10 +397,17 @@ def run_universal_mutator(file_to_mutant: Path, mutants_location: Path, num_muta
334
397
  if backup_file:
335
398
  Util.restore_backup(backup_file)
336
399
  # Clean up temporary files
337
- clean_temp_files()
400
+ clean_temp_files(file_to_mutant)
338
401
 
339
402
 
340
403
  if __name__ == "__main__":
341
404
  # Parse command-line arguments
342
405
  args = parse_args()
343
- run_universal_mutator(args.file_to_mutant, args.mutants_location, args.num_mutants, args.seed, args.debug)
406
+ run_universal_mutator(
407
+ args.file_to_mutant,
408
+ args.build_script,
409
+ args.mutants_location,
410
+ args.num_mutants,
411
+ args.seed,
412
+ args.debug
413
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: certora-cli-alpha-master
3
- Version: 20241216.22.25.927248
3
+ Version: 20241217.16.7.546354
4
4
  Summary: Runner for the Certora Prover
5
5
  Home-page: https://pypi.org/project/certora-cli-alpha-master
6
6
  Author: Certora
@@ -23,4 +23,4 @@ Requires-Dist: StrEnum
23
23
  Requires-Dist: tomli
24
24
  Requires-Dist: universalmutator
25
25
 
26
- Commit 6e2a8f1. Build and Run scripts for executing the Certora Prover on Solidity smart contracts.
26
+ Commit 77317ff. Build and Run scripts for executing the Certora Prover on Solidity smart contracts.
@@ -3,25 +3,27 @@ certora_bins/gambit,sha256=mEtRHvYfT6ShKzPHpLNwqAhDfTwRjoj3wRwGYYkRxUs,3820384
3
3
  certora_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  certora_cli/certoraEqCheck.py,sha256=5LtN9GNBSYR_kYghDBeNNL4yWshPw9PNpe0stLtuIxQ,379
5
5
  certora_cli/certoraMutate.py,sha256=BUK_keaunrcCYgLiwHfIkDSjIHM_KoGwwnIiuojc-ug,2643
6
- certora_cli/certoraRun.py,sha256=IOl3iLVivvnGms26SfHEkfwesWV52iR4wbYbw5GbDT8,12002
7
- certora_cli/rustMutator.py,sha256=JRxQ-20k5iJ7B04WZXWMP-sVEW-DVIYQZCpIYJaX_B8,12113
6
+ certora_cli/certoraRun.py,sha256=rnczNnSoYRuUKOkIN7_0cRxjiVDK0x4NJxOT4KNkYdg,11884
7
+ certora_cli/certoraSolanaProver.py,sha256=3Vb05zd5PqkU5CmCNzYvmYfbC-hsmU6Tt390msjQlOc,5898
8
+ certora_cli/rustMutator.py,sha256=wSE_4qDegDhItxHw7dtcFzDMgYCTsVxW4yFGxYRHlQc,14418
8
9
  certora_cli/EVMVerifier/__init__.py,sha256=AJxj90KAGh1JqAsxKqDBTL2rFbqgtkhDfW_XmxSHft0,159
9
10
  certora_cli/EVMVerifier/certoraBuild.py,sha256=omIs-4cQWR-IT3WTckXMZe9EZMyk2e-hSfRqG7iQJHo,203131
10
11
  certora_cli/EVMVerifier/certoraBuildCacheManager.py,sha256=ZSVCsdTkFFtawuNJ8VK3wJOb01Uyu5e8viPQ8wTziSQ,12563
11
12
  certora_cli/EVMVerifier/certoraBuildDataClasses.py,sha256=4IhvOA_fg-ij5RM9RRlvDDoS7imfp4Zru-AgHhxcHc0,13314
12
- certora_cli/EVMVerifier/certoraCloudIO.py,sha256=bu7s5lK7I8FmxQzGP7Sb5g8Q4dvCk3KXSsBMsNDes14,52460
13
+ certora_cli/EVMVerifier/certoraCloudIO.py,sha256=ZZDjMsSkp0ZN5IA1JczhJa2m16s0nC3DBcHHDv5J2kc,54931
13
14
  certora_cli/EVMVerifier/certoraCollectRunMetadata.py,sha256=selEf-p7UW8_70-AbPfa5659ki6WcGrBwfPkUQWKhFU,10322
14
15
  certora_cli/EVMVerifier/certoraCompilerParameters.py,sha256=PDKW3K_lNeRNPHisW0e5hYtfu6unDRjJUop-yItGHO0,762
15
- certora_cli/EVMVerifier/certoraConfigIO.py,sha256=8BUxRmRfmbRroYOJvjemDPPdvdXqmyMqKnfPnOwA3yA,4409
16
- certora_cli/EVMVerifier/certoraContext.py,sha256=w4k6_O7sT9OuK8MmJrTUZjjCeflOPNpg_gHIieZtKkQ,23640
17
- certora_cli/EVMVerifier/certoraContextAttributes.py,sha256=I-8Ll3aK48EdBegpd-eMzjydImh_aeCtjGx6NqvkbX0,55308
16
+ certora_cli/EVMVerifier/certoraConfigIO.py,sha256=oyNOyBjVWFfB1dmj0jNnEwvokM7UMoGZ91luSh-cFGk,4634
17
+ certora_cli/EVMVerifier/certoraContext.py,sha256=z86n6XbsU9dKfxiIqlPrCHz6EtrBAuYIRE0BS4rxSHQ,21974
18
+ certora_cli/EVMVerifier/certoraContextAttributes.py,sha256=__JqYpti0mDwBvgtunajEotCAKprUyoeDabe1ooSnro,55637
18
19
  certora_cli/EVMVerifier/certoraContextClass.py,sha256=qdHYmrzI4zeQaAaMnonw4C-h1PFrPEhI84trrgvxFj0,210
19
- certora_cli/EVMVerifier/certoraContextValidator.py,sha256=JUOVaHquuEOv5hTo2b_f7G_6lxFB4Wm_IlsfeQmOrx8,37451
20
+ certora_cli/EVMVerifier/certoraContextValidator.py,sha256=YE4_BYRhZhXWEtjodD_Pligh8opdaOjs27mVEoy74ZY,37210
20
21
  certora_cli/EVMVerifier/certoraContractFuncs.py,sha256=W3lAvKotGKK7ZGJLg4yYh0FzTwwSQJbZMpizyGaYXZo,6229
21
22
  certora_cli/EVMVerifier/certoraExtensionInfo.py,sha256=bHjEBKg3qQ1lS419KSU6v6HKbhrSOz2TmEE21YrOfYI,1336
22
23
  certora_cli/EVMVerifier/certoraJobList.py,sha256=JlPOANEzNHP6cDrM5UKfO314pmWYokuhAPTkH3ezNb4,10768
23
24
  certora_cli/EVMVerifier/certoraMiniSpecParser.py,sha256=Thtcr1BLmCz9xOiOkaP04mSnrDaqIUOUSCM81TrqD_s,8970
24
25
  certora_cli/EVMVerifier/certoraNodeFilters.py,sha256=0e8iPdj4cWjwReaAhfJhpdnSUYc8gephmKQzOOP5X_g,2140
26
+ certora_cli/EVMVerifier/certoraParseBuildScript.py,sha256=vX_lhus5VgLQaZp5GrNtJP6R5cqChjv_Rfpk_JZz2uY,1456
25
27
  certora_cli/EVMVerifier/certoraSourceFinders.py,sha256=fZLYGjNgntIySnG76DwsjRZ-UHiy0zEkM6xNP7cYpDE,18342
26
28
  certora_cli/EVMVerifier/certoraType.py,sha256=mmMQ2yrffKmoH_dHhO7cMQfEENZwHw1jLbyUo98xzas,28598
27
29
  certora_cli/EVMVerifier/certoraVerifyGenerator.py,sha256=OT5zrtQMXDoaVdGfQ4_gbAnITin3nlXgXTgw3msqlNQ,9452
@@ -41,7 +43,7 @@ certora_cli/EquivalenceCheck/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
41
43
  certora_cli/EquivalenceCheck/equivCheck.py,sha256=eG6jCa1DmHqPweGpjby-6Bnym91maffH4olE22uB2-c,19828
42
44
  certora_cli/EquivalenceCheck/sanity.spec,sha256=tWmE9z2Sq3_SWaqKDRQaNajRrw94maUrirvoUmX89LE,103
43
45
  certora_cli/Mutate/__init__.py,sha256=AJxj90KAGh1JqAsxKqDBTL2rFbqgtkhDfW_XmxSHft0,159
44
- certora_cli/Mutate/mutateApp.py,sha256=gDYOVhQtwgzOiHJyN2o9nmxwiOjlmwzFOU9-uynMDAU,85302
46
+ certora_cli/Mutate/mutateApp.py,sha256=wDCfkgXgV3fdAGw8SxWt1hlU1W9kl90HOcQIdNpb_aM,85871
45
47
  certora_cli/Mutate/mutateAttributes.py,sha256=l7tpkq3WZumVBuORaM2VLv93Px_X_XueL2IkfThTMNg,9402
46
48
  certora_cli/Mutate/mutateConstants.py,sha256=SHFQ0lBCYwJYH3Uk2ncG-7-4ANQIC4JRczNvWflfNU4,3277
47
49
  certora_cli/Mutate/mutateUtil.py,sha256=O3ub9gJe6LWQTDPbWyDqdfBhupwYl0ZrlfRX4kSn18c,1597
@@ -50,14 +52,14 @@ certora_cli/Shared/ExpectedComparator.py,sha256=rDDY3-FaE3OS_ZjZ-nOyvUTlUp8eQCHw
50
52
  certora_cli/Shared/__init__.py,sha256=QGoFb_Uu87tWp4E4L6C_VtzdG-sfNrzdNtRK79h5_Lw,333
51
53
  certora_cli/Shared/certoraAttrUtil.py,sha256=IbIIvTbtPzmPSSrK7VIWZNitkz1iwvW_e0ggX1e2joY,6751
52
54
  certora_cli/Shared/certoraLogging.py,sha256=5Lx-XWKl7GnwnWi7KlwTLIfsEvUvCTZ8KeyfNyi_6RU,13323
53
- certora_cli/Shared/certoraUtils.py,sha256=nlUljgDaYCIptVHAzCxQc2N9fPZ8A1bV46ItM6jzaLc,53607
54
- certora_cli/Shared/certoraValidateFuncs.py,sha256=3qQ8ikE0UrYBWn0s0zIISlgh2COXp0poJXriCDZO2U4,36525
55
- certora_jars/CERTORA-CLI-VERSION-METADATA.json,sha256=jH-b595iMzQ5nCqrTe2uo2sMKuYFA6sdcTbY9B0v7pE,170
56
- certora_jars/Typechecker.jar,sha256=C3JlkbWVYoftsKM971ALFSiTBiGYDbdvjrh39FRmMls,16680344
55
+ certora_cli/Shared/certoraUtils.py,sha256=8EGXrxfIxu34VTx46QeTx6tTYLdwpTLT9ulVQijmD9Y,53624
56
+ certora_cli/Shared/certoraValidateFuncs.py,sha256=OOjPAkcfrURZDD4oDjOMBFTvY6wwQSXboXzu-47AUbY,36871
57
+ certora_jars/CERTORA-CLI-VERSION-METADATA.json,sha256=CKa-Wwq9Rh28A51DZy0AdvS1UqUA3YxH7iMGm-ELUEY,168
58
+ certora_jars/Typechecker.jar,sha256=no7ucRdariDhJSTuF91i1KdG6vCuRJxWSeLFOkvuylc,16680403
57
59
  certora_jars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/LICENSE,sha256=VeEBJLgfzZqyAUfjLoKUztf7KJBBUjtZ1ap99eQubOo,1065
59
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/METADATA,sha256=11LzZIVTFumsqknzhf6XgkMGhHh-DW5zEpjmX-i9pDg,838
60
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/WHEEL,sha256=N3Zagyg8u7FcZiPCx4UnuNyRRYq7IQpu24eWYyuhGOQ,110
61
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/entry_points.txt,sha256=1hX_iXDKItMNmz3frdg30bXS0auF5FG_5oNXxBi79Q8,195
62
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/top_level.txt,sha256=8C77w3JLanY0-NW45vpJsjRssyCqVP-qmPiN9FjWiX4,38
63
- certora_cli_alpha_master-20241216.22.25.927248.dist-info/RECORD,,
60
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/LICENSE,sha256=VeEBJLgfzZqyAUfjLoKUztf7KJBBUjtZ1ap99eQubOo,1065
61
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/METADATA,sha256=IlmCZaJUG0ha0YrHEfQoy1AtGPGlzeLm131hzHWb9j0,837
62
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/WHEEL,sha256=N3Zagyg8u7FcZiPCx4UnuNyRRYq7IQpu24eWYyuhGOQ,110
63
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/entry_points.txt,sha256=Y90wInBDlKPSU3YnuTn3JqAsSgPRTDHc8lf6BfKILjY,261
64
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/top_level.txt,sha256=8C77w3JLanY0-NW45vpJsjRssyCqVP-qmPiN9FjWiX4,38
65
+ certora_cli_alpha_master-20241217.16.7.546354.dist-info/RECORD,,
@@ -2,3 +2,4 @@
2
2
  certoraEqCheck = certora_cli.certoraEqCheck:equiv_check_entry_point
3
3
  certoraMutate = certora_cli.certoraMutate:mutate_entry_point
4
4
  certoraRun = certora_cli.certoraRun:entry_point
5
+ certoraSolanaProver = certora_cli.certoraSolanaProver:entry_point
@@ -1 +1 @@
1
- {"name": "certora-cli-alpha-master", "tag": "", "branch": "master", "commit": "6e2a8f1", "timestamp": "20241216.22.25.927248", "version": "20241216.22.25.927248+6e2a8f1"}
1
+ {"name": "certora-cli-alpha-master", "tag": "", "branch": "master", "commit": "77317ff", "timestamp": "20241217.16.7.546354", "version": "20241217.16.7.546354+77317ff"}
Binary file