certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 7.29.0__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 (31) hide show
  1. certora_cli/CertoraProver/Compiler/CompilerCollectorVy.py +48 -13
  2. certora_cli/CertoraProver/certoraBuild.py +61 -30
  3. certora_cli/CertoraProver/certoraBuildDataClasses.py +5 -2
  4. certora_cli/CertoraProver/certoraBuildRust.py +77 -41
  5. certora_cli/CertoraProver/certoraCloudIO.py +29 -64
  6. certora_cli/CertoraProver/certoraCollectConfigurationLayout.py +205 -70
  7. certora_cli/CertoraProver/certoraCollectRunMetadata.py +3 -1
  8. certora_cli/CertoraProver/certoraConfigIO.py +14 -15
  9. certora_cli/CertoraProver/certoraContext.py +13 -5
  10. certora_cli/CertoraProver/certoraContextAttributes.py +95 -26
  11. certora_cli/CertoraProver/certoraContextValidator.py +39 -5
  12. certora_cli/CertoraProver/certoraParseBuildScript.py +7 -10
  13. certora_cli/CertoraProver/certoraVerifyGenerator.py +12 -0
  14. certora_cli/CertoraProver/splitRules.py +3 -1
  15. certora_cli/Mutate/mutateApp.py +3 -3
  16. certora_cli/Shared/certoraAttrUtil.py +10 -0
  17. certora_cli/Shared/certoraUtils.py +9 -1
  18. certora_cli/Shared/certoraValidateFuncs.py +7 -0
  19. certora_cli/certoraRanger.py +71 -0
  20. certora_cli/certoraRun.py +11 -13
  21. certora_cli/certoraSolanaProver.py +7 -0
  22. certora_cli/certoraSorobanProver.py +253 -4
  23. certora_cli_beta_mirror-7.29.0.dist-info/LICENSE +15 -0
  24. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/METADATA +18 -4
  25. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/RECORD +30 -29
  26. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/WHEEL +1 -1
  27. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/entry_points.txt +1 -0
  28. certora_jars/CERTORA-CLI-VERSION-METADATA.json +1 -1
  29. certora_jars/Typechecker.jar +0 -0
  30. certora_cli_beta_mirror-7.28.0.dist-info/LICENSE +0 -22
  31. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.0.dist-info}/top_level.txt +0 -0
@@ -47,6 +47,7 @@ import logging
47
47
  cloud_logger = logging.getLogger("cloud")
48
48
 
49
49
  MAX_FILE_SIZE = 25 * 1024 * 1024
50
+ SOLANA_MAX_FILE_SIZE = 2 * MAX_FILE_SIZE
50
51
  NO_OUTPUT_LIMIT_MINUTES = 15
51
52
  MAX_POLLING_TIME_MINUTES = 150
52
53
  LOG_READ_FREQUENCY = 10
@@ -264,7 +265,9 @@ def compress_files(zip_file_path: Path, *resource_paths: Path, short_output: boo
264
265
  cloud_logger.error(f"{GENERAL_ERR_PREFIX}" f"Could not compress {path}")
265
266
  return False
266
267
 
267
- if zip_file_path.stat().st_size > MAX_FILE_SIZE:
268
+ # Solana binary files can become heavy, thus we need to increase size limit.
269
+ max_file_size = MAX_FILE_SIZE if not Attrs.is_rust_app() else SOLANA_MAX_FILE_SIZE
270
+ if zip_file_path.stat().st_size > max_file_size:
268
271
  cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded.")
269
272
  return False
270
273
 
@@ -592,43 +595,25 @@ class CloudVerification:
592
595
 
593
596
  jar_settings = Ctx.collect_jar_args(self.context)
594
597
 
595
- if Attrs.is_solana_app():
596
- # We need to strip "../" path component from all file paths because
597
- # unzip will also do that.
598
- solana_jar_settings = []
599
- if hasattr(self.context, 'build_script') and self.context.build_script:
600
- solana_jar_settings.append(Path(self.context.rust_executables).name)
598
+ if Attrs.is_rust_app():
599
+ rust_jar_settings = [Path(self.context.files[0]).name]
601
600
 
602
- else:
603
- for file in self.context.files:
604
- solana_jar_settings.append(file.split('../')[-1])
601
+ if Attrs.is_solana_app():
602
+ def paths_in_source_dir(attr_values: List[str]) -> str:
603
+ cwd_rel_in_sources = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources
604
+ values: list[str] = \
605
+ [os.path.relpath(cwd_rel_in_sources / value, Util.get_build_dir()) for value in attr_values]
606
+ return ','.join(values)
605
607
 
606
- is_file = False
607
- for arg in jar_settings:
608
- if is_file:
609
- solana_jar_settings.append(arg.split('../')[-1])
610
- is_file = False
611
- else:
612
- solana_jar_settings.append(arg)
613
-
614
- if arg == '-solanaInlining':
615
- is_file = True
616
- elif arg == '-solanaSummaries':
617
- is_file = True
618
- auth_data["jarSettings"] = solana_jar_settings
619
- elif Attrs.is_soroban_app():
620
- # We need to strip "../" path component from all file paths because
621
- # unzip will also do that.
622
- soroban_jar_settings = []
623
- # not needed - should be in files
624
- if hasattr(self.context, 'build_script') and self.context.build_script:
625
- soroban_jar_settings.append(Path(self.context.rust_executables).name)
626
- else:
627
- for file in self.context.files:
628
- soroban_jar_settings.append(file.split('../')[-1])
629
- for arg in jar_settings:
630
- soroban_jar_settings.append(arg)
631
- auth_data["jarSettings"] = soroban_jar_settings
608
+ if self.context.solana_summaries:
609
+ rust_jar_settings.append('-solanaSummaries')
610
+ rust_jar_settings.append(paths_in_source_dir(self.context.solana_summaries))
611
+
612
+ if self.context.solana_inlining:
613
+ rust_jar_settings.append('-solanaInlining')
614
+ rust_jar_settings.append(paths_in_source_dir(self.context.solana_inlining))
615
+
616
+ auth_data["jarSettings"] = rust_jar_settings + jar_settings
632
617
  else:
633
618
  auth_data["jarSettings"] = jar_settings
634
619
 
@@ -747,7 +732,10 @@ class CloudVerification:
747
732
  result = compress_files(self.ZipFilePath, *paths,
748
733
  short_output=Ctx.is_minimal_cli_output(self.context))
749
734
  elif Attrs.is_rust_app():
750
- files_list = [Util.get_certora_metadata_file(), Util.get_configuration_layout_data_file()]
735
+ files_list = [Util.get_certora_metadata_file(),
736
+ Util.get_configuration_layout_data_file(),
737
+ Util.get_build_dir() / Path(self.context.files[0]).name]
738
+
751
739
  if Util.get_certora_sources_dir().exists():
752
740
  files_list.append(Util.get_certora_sources_dir())
753
741
 
@@ -759,18 +747,11 @@ class CloudVerification:
759
747
  return False
760
748
  files_list.append(self.logZipFilePath)
761
749
 
762
- files_list.append(Util.get_build_dir() / Path(self.context.rust_executables).name)
763
-
764
750
  # Create a .RustExecution file to classify zipInput as a rust source code
765
751
  rust_execution_file = Util.get_build_dir() / ".RustExecution"
766
752
  rust_execution_file.touch(exist_ok=True)
767
753
  files_list.append(rust_execution_file)
768
754
 
769
- additional_files = (getattr(self.context, 'solana_inlining', None) or []) + \
770
- (getattr(self.context, 'solana_summaries', None) or [])
771
- for file in additional_files:
772
- files_list.append(Util.get_build_dir() / Path(file).name)
773
-
774
755
  if attr_file := getattr(self.context, 'rust_logs_stdout', None):
775
756
  files_list.append(Util.get_build_dir() / Path(attr_file).name)
776
757
  if attr_file := getattr(self.context, 'rust_logs_stderr', None):
@@ -779,29 +760,10 @@ class CloudVerification:
779
760
  result = compress_files(self.ZipFilePath, *files_list,
780
761
  short_output=Ctx.is_minimal_cli_output(self.context))
781
762
 
782
- elif Attrs.is_solana_app():
783
- # We zip the ELF files and the two configuration files
784
- jar_args = Ctx.collect_jar_args(self.context)
785
-
763
+ elif Attrs.is_solana_app() or Attrs.is_soroban_app():
786
764
  for file in self.context.files:
787
765
  files_list.append(Path(file))
788
- is_file = False
789
- for arg in jar_args:
790
- if is_file:
791
- files_list.append(Path(arg))
792
- is_file = False
793
-
794
- if arg == '-solanaInlining':
795
- is_file = True
796
- elif arg == '-solanaSummaries':
797
- is_file = True
798
- result = compress_files(self.ZipFilePath, *files_list,
799
- short_output=Ctx.is_minimal_cli_output(self.context))
800
766
 
801
- elif Attrs.is_soroban_app():
802
- # We zip the wat file
803
- for file in self.context.files:
804
- files_list.append(Path(file))
805
767
  result = compress_files(self.ZipFilePath, *files_list,
806
768
  short_output=Ctx.is_minimal_cli_output(self.context))
807
769
  else:
@@ -829,6 +791,8 @@ class CloudVerification:
829
791
  Util.flush_stdout()
830
792
  if not result:
831
793
  return False
794
+ if self.context.test == str(Util.TestValue.CHECK_ZIP):
795
+ raise Util.TestResultsReady(self.ZipFilePath)
832
796
 
833
797
  cloud_logger.debug("Uploading files...")
834
798
  if self.upload(self.presigned_url, self.ZipFilePath):
@@ -866,6 +830,7 @@ class CloudVerification:
866
830
  return False
867
831
 
868
832
  file_upload_success = self.__compress_and_upload_zip_files()
833
+
869
834
  if not file_upload_success:
870
835
  return False
871
836
 
@@ -12,10 +12,10 @@
12
12
  #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
-
15
+ import dataclasses
16
16
  import json
17
17
  from enum import Enum
18
- from typing import Optional, Dict, Any
18
+ from typing import Optional, Any
19
19
  from pathlib import Path
20
20
  import sys
21
21
 
@@ -31,21 +31,40 @@ class MainSection(Enum):
31
31
  GENERAL = "GENERAL"
32
32
  SOLIDITY_COMPILER = "SOLIDITY_COMPILER"
33
33
  GIT = "GIT"
34
- FILES = "FILES"
35
- LINKS = "LINKS"
36
- PACKAGES = "PACKAGES"
37
- METADATA = "METADATA"
34
+ NEW_SECTION = "NEW_SECTION"
35
+
36
+
37
+ class ContentType(Enum):
38
+ SIMPLE = "SIMPLE"
39
+ COMPLEX = "COMPLEX"
40
+ FLAG = "FLAG"
41
+
42
+
43
+ @dataclasses.dataclass
44
+ class InnerContent:
45
+ inner_title: str
46
+ content_type: str
47
+ content: Any
48
+ doc_link: str = ''
49
+ tooltip: str = ''
50
+ unsound: str = 'false'
38
51
 
52
+ def __post_init__(self) -> None:
53
+ if isinstance(self.content, bool):
54
+ self.content = 'true' if self.content else 'false'
55
+ if isinstance(self.unsound, bool):
56
+ self.unsound = 'true' if self.unsound else 'false'
39
57
 
40
- class FlagType(Enum):
41
- VALUE_FLAG = "VALUE"
42
- LIST_FLAG = "LIST"
43
- MAP_FLAG = "MAP"
58
+
59
+ @dataclasses.dataclass
60
+ class CardContent:
61
+ card_title: str
62
+ content_type: str
63
+ content: Any
44
64
 
45
65
 
46
66
  DOC_LINK_PREFIX = 'https://docs.certora.com/en/latest/docs/'
47
67
  GIT_ATTRIBUTES = ['origin', 'revision', 'branch', 'dirty']
48
- SPECIAL_MAIN_SECTIONS = ['files', 'links', 'packages']
49
68
 
50
69
 
51
70
  class AttributeJobConfigData:
@@ -87,18 +106,25 @@ class RunConfigurationLayout:
87
106
  configuration_layout : Dict -- An aggregated configuration for a specific run, nested by main section, subsection.
88
107
  Each leaf contains data about attribute value, type, documentation link and UI data.
89
108
  """
90
- def __init__(self, configuration_layout: Dict[str, Any]):
109
+
110
+ configuration_layout: list[Any]
111
+
112
+ def __init__(self, configuration_layout: list[Any]):
91
113
  # Dynamically allocate class attributes from dict
92
- for key, value in configuration_layout.items():
93
- setattr(self, key, value)
114
+ self.configuration_layout = configuration_layout
94
115
 
95
116
  def __repr__(self) -> str:
96
- return "\n".join(f"{key}: {value}" for key, value in self.__dict__.items())
117
+ try:
118
+ return json.dumps(self.configuration_layout, indent=2, sort_keys=True)
119
+ except TypeError:
120
+ # Fallback if something isn't serializable
121
+ return str(self.configuration_layout)
97
122
 
98
123
  @classmethod
99
- def dump_file(cls, data: dict) -> None:
124
+ def dump_file(cls, data: list) -> None:
125
+ sorted_data = sort_configuration_layout(data)
100
126
  with Utils.get_configuration_layout_data_file().open("w+") as f:
101
- json.dump(data, f, indent=4, sort_keys=True, cls=MetadataEncoder)
127
+ json.dump(sorted_data, f, indent=4, cls=MetadataEncoder)
102
128
 
103
129
  @classmethod
104
130
  def load_file(cls) -> dict:
@@ -110,12 +136,11 @@ class RunConfigurationLayout:
110
136
  raise
111
137
 
112
138
  def dump(self) -> None:
113
- if self.__dict__: # dictionary containing all the attributes defined for GitInfo
114
- try:
115
- self.dump_file(self.__dict__)
116
- except Exception as e:
117
- print(f"failed to write configuration layout file {Utils.get_configuration_layout_data_file()}\n{e}")
118
- raise
139
+ try:
140
+ self.dump_file(self.configuration_layout)
141
+ except Exception as e:
142
+ print(f"Failed to write configuration layout file: {Utils.get_configuration_layout_data_file()}\n{e}")
143
+ raise
119
144
 
120
145
 
121
146
  def collect_configuration_layout() -> RunConfigurationLayout:
@@ -127,7 +152,7 @@ def collect_configuration_layout() -> RunConfigurationLayout:
127
152
  metadata = RunMetaData.load_file()
128
153
  except Exception as e:
129
154
  print(f"failed to load job metadata! cannot create a configuration layout file without metadata!\n{e}")
130
- return RunConfigurationLayout(configuration_layout={})
155
+ return RunConfigurationLayout(configuration_layout=[])
131
156
 
132
157
  attributes_configs = collect_attribute_configs(metadata)
133
158
  configuration_layout = collect_run_config_from_metadata(attributes_configs, metadata)
@@ -154,89 +179,199 @@ def get_doc_link(attr) -> str: # type: ignore
154
179
  return doc_link
155
180
 
156
181
 
157
- def collect_attribute_configs(metadata: dict) -> dict:
182
+ def create_or_get_card_content(output: list[CardContent], name: str) -> CardContent:
183
+ """
184
+ Returns an existing CardContent by name or creates and appends a new one if it doesn't exist.
185
+ Card content type will always be complex in this case.
186
+ Args:
187
+ output (list[CardContent]): List of CardContent objects.
188
+ name (str): Title of the card to find or create.
189
+
190
+ Returns:
191
+ CardContent: The found or newly created CardContent.
192
+ """
193
+ main_section = next((section for section in output if section.card_title == name), None)
194
+ if main_section is None:
195
+ main_section = CardContent(
196
+ card_title=name,
197
+ content_type=ContentType.COMPLEX.value,
198
+ content=[]
199
+ )
200
+ output.append(main_section)
201
+ return main_section
202
+
203
+
204
+ def create_inner_content(name: str, content_type: ContentType, value: Any, doc_link: str,
205
+ config_data: AttributeJobConfigData) -> InnerContent:
206
+ return InnerContent(
207
+ inner_title=name,
208
+ content_type=content_type.value,
209
+ content=value,
210
+ doc_link=doc_link,
211
+ tooltip=config_data.tooltip or '',
212
+ unsound='true' if config_data.unsound else 'false'
213
+ )
214
+
215
+
216
+ def collect_attribute_configs(metadata: dict) -> list[CardContent]:
217
+ """
218
+ Collects and organizes attribute configurations into a structured list of CardContent objects.
219
+
220
+ This function iterates through all available attributes defined, checks if relevant metadata is provided
221
+ for each attribute, and organizes the data into sections and subsections based on configuration rules.
222
+
223
+ Attributes are grouped under their respective main sections, with special handling for:
224
+ - Simple value attributes
225
+ - List and dictionary attributes
226
+ - Attributes requiring new sections (e.g., Files, Links, Packages)
227
+
228
+ Args:
229
+ metadata (dict): Metadata dictionary containing attribute values.
230
+
231
+ Returns:
232
+ list: A list of CardContent objects representing the structured configuration view,
233
+ ready for rendering or further processing.
234
+ """
158
235
  attr_list = Attrs.get_attribute_class().attribute_list()
159
- output: Dict[str, Any] = {}
236
+ output: list[CardContent] = []
160
237
 
161
238
  for attr in attr_list:
162
239
  attr_name = attr.name.lower()
163
240
  if attr.config_data is None:
164
241
  continue
165
242
 
166
- if metadata.get(attr_name) is None and metadata.get('conf', {}).get(attr_name) is None:
243
+ attr_value = metadata.get(attr_name) or metadata.get('conf', {}).get(attr_name)
244
+ if attr_value is None:
167
245
  continue
168
246
 
169
- attr_value = metadata.get(attr_name) or metadata.get('conf', {}).get(attr_name)
170
247
  config_data: AttributeJobConfigData = attr.config_data
171
248
  doc_link = config_data.doc_link or get_doc_link(attr)
172
249
 
173
- # Get or create the main section
250
+ # Find or create the main section
174
251
  main_section_key = config_data.main_section.value.lower()
175
- main_section = output.setdefault(main_section_key, {})
252
+ main_section = create_or_get_card_content(output, main_section_key)
253
+
254
+ # Files, Links and Packages are special cases where the main section is the attribute itself
255
+ if main_section_key == MainSection.NEW_SECTION.value.lower():
256
+ main_section.card_title = attr_name
257
+ main_section.content_type = ContentType.SIMPLE.value
258
+ main_section.content.append(
259
+ create_inner_content(attr_name, ContentType.SIMPLE, attr_value, doc_link, config_data)
260
+ )
261
+ continue
176
262
 
177
- # Get or create the subsection (if it exists) and flag_type
263
+ # Find or create the subsection (if it exists)
178
264
  if isinstance(attr_value, list):
179
- # Files, Links and Packages are special cases where the main section is the attribute itself
180
- if main_section_key in SPECIAL_MAIN_SECTIONS:
181
- current_section = output
182
- attr_name = main_section_key
183
- else:
184
- current_section = main_section
185
-
186
- flag_type = FlagType.LIST_FLAG
265
+ current_section: Any = main_section
266
+ content_type = ContentType.SIMPLE
267
+
187
268
  elif isinstance(attr_value, dict):
188
269
  current_section = main_section
189
- flag_type = FlagType.MAP_FLAG
270
+ content_type = ContentType.COMPLEX
271
+ attr_value = [
272
+ create_inner_content(key, ContentType.FLAG, value, doc_link, config_data)
273
+ for key, value in attr_value.items()
274
+ ]
190
275
  else:
276
+ # this attribute is a value attribute without a subsection, it will be placed in flags.
191
277
  subsection_key = config_data.subsection.lower() if config_data.subsection else 'flags'
192
- current_section = main_section.setdefault(subsection_key, {})
193
- flag_type = FlagType.VALUE_FLAG
278
+ current_section = next((section for section in main_section.content
279
+ if section.inner_title == subsection_key), None)
280
+ if current_section is None:
281
+ current_section = InnerContent(
282
+ inner_title=subsection_key,
283
+ content_type=ContentType.COMPLEX.value,
284
+ content=[]
285
+ )
286
+ main_section.content.append(current_section)
287
+
288
+ content_type = ContentType.FLAG
194
289
 
195
290
  # Update the current section with attribute details
196
- current_section[attr_name] = {
197
- 'value': attr_value,
198
- 'flag_type': flag_type.value.lower(),
199
- 'doc_link': doc_link,
200
- 'tooltip': config_data.tooltip,
201
- 'unsound': config_data.unsound
202
- }
291
+ current_section.content.append(
292
+ create_inner_content(attr_name, content_type, attr_value, doc_link, config_data)
293
+ )
203
294
 
204
295
  return output
205
296
 
206
297
 
207
- def collect_run_config_from_metadata(attributes_configs: dict, metadata: dict) -> dict:
298
+ def collect_run_config_from_metadata(attributes_configs: list[CardContent], metadata: dict) -> list[CardContent]:
208
299
  """
209
300
  Adding CLI and Git configuration from metadata
210
301
  """
211
- metadata_section = attributes_configs.setdefault(MainSection.METADATA.value.lower(), {})
212
302
 
213
303
  # Define a mapping of metadata attributes to their keys in general_section
214
304
  metadata_mappings = {
215
- 'cli_version': metadata.get('CLI_version'),
216
- 'main_spec': metadata.get('main_spec'),
217
- 'solc_version': metadata.get('conf', {}).get('solc'),
218
- 'verify': metadata.get('conf', {}).get('verify'),
305
+ 'CLI Version': metadata.get('CLI_version'),
306
+ 'Verify': metadata.get('conf', {}).get('verify'),
219
307
  }
220
308
 
309
+ general_section = create_or_get_card_content(attributes_configs, MainSection.GENERAL.value.lower())
310
+
221
311
  # Add metadata attributes dynamically if they exist
222
312
  for key, value in metadata_mappings.items():
223
313
  if value:
224
- metadata_section[key] = {
225
- 'value': value,
226
- 'flag_type': FlagType.VALUE_FLAG.value,
227
- 'doc_link': '',
228
- 'tooltip': '',
229
- }
230
-
231
- # Adding GIT configuration from metadata
232
- git_section = attributes_configs.setdefault(MainSection.GIT.value.lower(), {})
314
+ general_section.content.append(InnerContent(
315
+ inner_title=key,
316
+ content_type=ContentType.FLAG.value,
317
+ content=value,
318
+ ))
319
+
320
+ # Adding GIT configuration from metadata only if attributes are found
321
+ git_content = []
233
322
  for attr in GIT_ATTRIBUTES:
234
323
  if attr_value := metadata.get(attr):
235
- git_section[attr] = {
236
- 'value': attr_value,
237
- 'flag_type': FlagType.MAP_FLAG.value,
238
- 'doc_link': '',
239
- 'tooltip': ''
240
- }
324
+ git_content.append(InnerContent(
325
+ inner_title=attr,
326
+ content_type=ContentType.FLAG.value,
327
+ content=attr_value,
328
+ ))
329
+
330
+ if git_content:
331
+ git_section = create_or_get_card_content(attributes_configs, MainSection.GIT.value.lower())
332
+ git_section.content = git_content
241
333
 
242
334
  return attributes_configs
335
+
336
+
337
+ def sort_configuration_layout(data: list[CardContent]) -> list[CardContent]:
338
+ """
339
+ Sorts a configuration layout:
340
+ - Top-level sorted by 'card_title'
341
+ - Nested content sorted by 'inner_title', with 'Verify' first
342
+ """
343
+ priority = {
344
+ "Verify": 0,
345
+ "general": 0,
346
+ "solc": 0,
347
+ "CLI Version": 1,
348
+ "flags": 2
349
+ }
350
+
351
+ def inner_sort_key(item: Any) -> Any:
352
+ if isinstance(item, CardContent):
353
+ title = item.card_title
354
+ return priority.get(title, 3), title.lower()
355
+ elif isinstance(item, InnerContent):
356
+ title = item.inner_title
357
+ return priority.get(title, 3), title.lower()
358
+ else:
359
+ return item
360
+
361
+ def sort_content(content: list[InnerContent]) -> list[InnerContent]:
362
+ sorted_content = []
363
+ for item in content:
364
+ if isinstance(item.content, list):
365
+ # Recurse into nested 'content'
366
+ item.content = sorted(item.content, key=inner_sort_key)
367
+ sorted_content.append(item)
368
+ return sorted(sorted_content, key=inner_sort_key)
369
+
370
+ # Sort top-level entries by 'card_title'
371
+ sorted_data = sorted(data, key=inner_sort_key)
372
+
373
+ # Sort nested 'content'
374
+ for section in sorted_data:
375
+ section.content = sort_content(section.content)
376
+
377
+ return sorted_data
@@ -12,7 +12,7 @@
12
12
  #
13
13
  # You should have received a copy of the GNU General Public License
14
14
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
15
-
15
+ import dataclasses
16
16
  import json
17
17
  from typing import Any, Dict, List, Optional
18
18
  import subprocess
@@ -47,6 +47,8 @@ class MetadataEncoder(json.JSONEncoder):
47
47
  def default(self, obj: Any) -> Any:
48
48
  if isinstance(obj, set):
49
49
  return list(obj)
50
+ if dataclasses.is_dataclass(obj):
51
+ return dataclasses.asdict(obj)
50
52
  return json.JSONEncoder.default(self, obj)
51
53
 
52
54
 
@@ -75,9 +75,9 @@ def read_from_conf_file(context: CertoraContext) -> None:
75
75
 
76
76
  try:
77
77
  with conf_file_path.open() as conf_file:
78
- conf_file_attr = json5.load(conf_file, allow_duplicate_keys=False)
78
+ context.conf_file_attr = json5.load(conf_file, allow_duplicate_keys=False)
79
79
  try:
80
- check_conf_content(conf_file_attr, context)
80
+ check_conf_content(context)
81
81
  except Util.CertoraUserInputError as e:
82
82
  raise Util.CertoraUserInputError(f"Error when reading {conf_file_path}: {str(e)}", e) from None
83
83
  context.conf_file = str(conf_file_path)
@@ -99,6 +99,7 @@ def handle_override_base_config(context: CertoraContext) -> None:
99
99
  with Path(context.override_base_config).open() as conf_file:
100
100
  try:
101
101
  override_base_config_attrs = json5.load(conf_file, allow_duplicate_keys=False)
102
+ context.conf_file_attr = {**override_base_config_attrs, **context.conf_file_attr}
102
103
 
103
104
  if 'override_base_config' in override_base_config_attrs:
104
105
  raise Util.CertoraUserInputError("base config cannot include 'override_base_config'")
@@ -113,7 +114,7 @@ def handle_override_base_config(context: CertoraContext) -> None:
113
114
  raise Util.CertoraUserInputError(f"{attr} appears in the base conf file {context.override_base_config} but is not a known attribute.")
114
115
 
115
116
 
116
- def check_conf_content(conf_file_attr: Dict[str, Any], context: CertoraContext) -> None:
117
+ def check_conf_content(context: CertoraContext) -> None:
117
118
  """
118
119
  validating content read from the conf file
119
120
  Note: a command line definition trumps the definition in the file.
@@ -122,15 +123,15 @@ def check_conf_content(conf_file_attr: Dict[str, Any], context: CertoraContext)
122
123
  @param context: A namespace containing options from the command line, if any
123
124
  """
124
125
 
125
- for option in conf_file_attr:
126
+ for option in context.conf_file_attr:
126
127
  if hasattr(context, option):
127
128
  val = getattr(context, option)
128
129
  if val is None or val is False:
129
- setattr(context, option, conf_file_attr[option])
130
- elif option != Attrs.EvmProverAttributes.FILES.get_conf_key() and val != conf_file_attr[option]:
130
+ setattr(context, option, context.conf_file_attr[option])
131
+ elif option != 'files' and val != context.conf_file_attr[option]:
131
132
  cli_val = ' '.join(val) if isinstance(val, list) else str(val)
132
- conf_val = ' '.join(conf_file_attr[option]) \
133
- if isinstance(conf_file_attr[option], list) else str(conf_file_attr[option])
133
+ conf_val = ' '.join(context.conf_file_attr[option]) \
134
+ if isinstance(context.conf_file_attr[option], list) else str(context.conf_file_attr[option])
134
135
  run_logger.warning(f"Note: attribute {option} value in CLI ({cli_val}) overrides value stored in conf"
135
136
  f" file ({conf_val})")
136
137
  else:
@@ -138,12 +139,10 @@ def check_conf_content(conf_file_attr: Dict[str, Any], context: CertoraContext)
138
139
 
139
140
  handle_override_base_config(context)
140
141
 
141
- if Attrs.is_evm_app() and 'files' not in conf_file_attr and not context.project_sanity and not context.foundry:
142
+ if Attrs.is_evm_app() and not context.files and not context.project_sanity and not context.foundry:
142
143
  raise Util.CertoraUserInputError("Mandatory 'files' attribute is missing from the configuration")
144
+ context.files = context.conf_file_attr.get('files')
145
+ if Attrs.is_soroban_app() and not context.files and not context.build_script:
146
+ raise Util.CertoraUserInputError("'files' or 'build script' must be set for Soroban runs")
143
147
 
144
- if Attrs.is_rust_app():
145
- has_build_script = getattr(context, 'build_script', False)
146
- if not has_build_script and 'files' not in conf_file_attr:
147
- raise Util.CertoraUserInputError("Mandatory 'build_script' or 'files' attribute is missing from the configuration")
148
-
149
- context.files = conf_file_attr.get('files')
148
+ context.files = context.conf_file_attr.get('files')
@@ -120,9 +120,9 @@ def get_local_run_cmd(context: CertoraContext) -> List[str]:
120
120
  """
121
121
  run_args = []
122
122
 
123
- if hasattr(context, 'rust_executables') and hasattr(context, 'rust_project_directory'):
124
- run_args.append(os.path.join(context.rust_project_directory, context.rust_executables))
125
- elif context.is_tac or Attrs.is_rust_app():
123
+ if Attrs.is_rust_app():
124
+ run_args.append(Path(context.files[0]).name)
125
+ elif context.is_tac:
126
126
  # For Rust app we assume the files holds the executable for the prover, currently we support a single file
127
127
  try:
128
128
  run_args.append(context.files[0])
@@ -159,10 +159,16 @@ class ProverParser(AttrUtil.ContextAttributeParser):
159
159
 
160
160
  def format_help(self) -> str:
161
161
  console = Console()
162
- console.print("\n\nThe Certora Prover - A formal verification tool for smart contracts")
162
+ if Attrs.is_ranger_app():
163
+ console.print("\n\nRanger - Certora’s bounded model checker for smart contracts")
164
+ else:
165
+ console.print("\n\nThe Certora Prover - A formal verification tool for smart contracts")
163
166
  # Using sys.stdout.write() as print() would color some of the strings here
164
167
  sys.stdout.write(f"\n\nUsage: {sys.argv[0]} <Files> <Flags>\n\n")
165
- if Attrs.is_evm_app():
168
+ if Attrs.is_ranger_app():
169
+ sys.stdout.write("Ranger supports only Solidity (.sol) and configuration (.conf) files.\n"
170
+ "Rust and Vyper contracts are not currently supported.\n\n")
171
+ elif Attrs.is_evm_app():
166
172
  sys.stdout.write("Acceptable files for EVM projects are Solidity files (.sol suffix), "
167
173
  "Vyper files (.vy suffix), or conf files (.conf suffix)\n\n")
168
174
  elif Attrs.is_solana_app():
@@ -252,6 +258,8 @@ def get_args(args_list: Optional[List[str]] = None) -> CertoraContext:
252
258
  Cv.check_mode_of_operation(context) # Here boolean run characteristics are set
253
259
 
254
260
  validator = Cv.CertoraContextValidator(context)
261
+ if Attrs.is_evm_app():
262
+ validator.handle_ranger_attrs()
255
263
  validator.validate()
256
264
  if Attrs.is_evm_app() or Attrs.is_rust_app():
257
265
  current_build_directory = Util.get_build_dir()