certora-cli-beta-mirror 7.28.0__py3-none-any.whl → 7.29.1__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 -55
  5. certora_cli/CertoraProver/certoraCloudIO.py +52 -63
  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 +17 -5
  10. certora_cli/CertoraProver/certoraContextAttributes.py +98 -26
  11. certora_cli/CertoraProver/certoraContextValidator.py +47 -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 +13 -15
  21. certora_cli/certoraSolanaProver.py +9 -2
  22. certora_cli/certoraSorobanProver.py +253 -4
  23. certora_cli_beta_mirror-7.29.1.dist-info/LICENSE +15 -0
  24. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/METADATA +18 -4
  25. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/RECORD +30 -29
  26. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.dist-info}/WHEEL +1 -1
  27. {certora_cli_beta_mirror-7.28.0.dist-info → certora_cli_beta_mirror-7.29.1.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.1.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
@@ -69,6 +70,16 @@ Response = requests.models.Response
69
70
 
70
71
  FEATURES_REPORT_FILE = Path("featuresReport.json")
71
72
 
73
+ class EcoEnum(Util.NoValEnum):
74
+ EVM = Util.auto()
75
+ SOROBAN = Util.auto()
76
+ SOLANA = Util.auto()
77
+
78
+ class ProductEnum(Util.NoValEnum):
79
+ PROVER = Util.auto()
80
+ RANGER = Util.auto()
81
+ SOPHY = Util.auto()
82
+
72
83
 
73
84
  class TimeError(Exception):
74
85
  """A custom exception used to report on time elapsed errors"""
@@ -264,7 +275,9 @@ def compress_files(zip_file_path: Path, *resource_paths: Path, short_output: boo
264
275
  cloud_logger.error(f"{GENERAL_ERR_PREFIX}" f"Could not compress {path}")
265
276
  return False
266
277
 
267
- if zip_file_path.stat().st_size > MAX_FILE_SIZE:
278
+ # Solana binary files can become heavy, thus we need to increase size limit.
279
+ max_file_size = MAX_FILE_SIZE if not Attrs.is_rust_app() else SOLANA_MAX_FILE_SIZE
280
+ if zip_file_path.stat().st_size > max_file_size:
268
281
  cloud_logger.error(f"{GENERAL_ERR_PREFIX} Max 25MB file size exceeded.")
269
282
  return False
270
283
 
@@ -592,43 +605,25 @@ class CloudVerification:
592
605
 
593
606
  jar_settings = Ctx.collect_jar_args(self.context)
594
607
 
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)
608
+ if Attrs.is_rust_app():
609
+ rust_jar_settings = [Path(self.context.files[0]).name]
601
610
 
602
- else:
603
- for file in self.context.files:
604
- solana_jar_settings.append(file.split('../')[-1])
611
+ if Attrs.is_solana_app():
612
+ def paths_in_source_dir(attr_values: List[str]) -> str:
613
+ cwd_rel_in_sources = Util.get_certora_sources_dir() / self.context.cwd_rel_in_sources
614
+ values: list[str] = \
615
+ [os.path.relpath(cwd_rel_in_sources / value, Util.get_build_dir()) for value in attr_values]
616
+ return ','.join(values)
605
617
 
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)
618
+ if self.context.solana_summaries:
619
+ rust_jar_settings.append('-solanaSummaries')
620
+ rust_jar_settings.append(paths_in_source_dir(self.context.solana_summaries))
613
621
 
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
622
+ if self.context.solana_inlining:
623
+ rust_jar_settings.append('-solanaInlining')
624
+ rust_jar_settings.append(paths_in_source_dir(self.context.solana_inlining))
625
+
626
+ auth_data["jarSettings"] = rust_jar_settings + jar_settings
632
627
  else:
633
628
  auth_data["jarSettings"] = jar_settings
634
629
 
@@ -646,6 +641,20 @@ class CloudVerification:
646
641
 
647
642
  auth_data["useLatestFe"] = self.context.fe_version == str(Util.FeValue.LATEST)
648
643
 
644
+ if Attrs.is_solana_app():
645
+ auth_data["ecosystem"] = EcoEnum.SOLANA.name
646
+ elif Attrs.is_soroban_app():
647
+ auth_data["ecosystem"] = EcoEnum.SOROBAN.name
648
+ else:
649
+ auth_data["ecosystem"] = EcoEnum.EVM.name
650
+
651
+ if Attrs.is_ranger_app():
652
+ auth_data["product"] = ProductEnum.RANGER.name
653
+ elif Attrs.is_sophy_app():
654
+ auth_data["product"] = ProductEnum.SOPHY.name
655
+ else:
656
+ auth_data["product"] = ProductEnum.PROVER.name
657
+
649
658
  if Attrs.is_evm_app() and self.context.cache is not None:
650
659
  auth_data["toolSceneCacheKey"] = self.context.cache
651
660
 
@@ -747,7 +756,10 @@ class CloudVerification:
747
756
  result = compress_files(self.ZipFilePath, *paths,
748
757
  short_output=Ctx.is_minimal_cli_output(self.context))
749
758
  elif Attrs.is_rust_app():
750
- files_list = [Util.get_certora_metadata_file(), Util.get_configuration_layout_data_file()]
759
+ files_list = [Util.get_certora_metadata_file(),
760
+ Util.get_configuration_layout_data_file(),
761
+ Util.get_build_dir() / Path(self.context.files[0]).name]
762
+
751
763
  if Util.get_certora_sources_dir().exists():
752
764
  files_list.append(Util.get_certora_sources_dir())
753
765
 
@@ -759,18 +771,11 @@ class CloudVerification:
759
771
  return False
760
772
  files_list.append(self.logZipFilePath)
761
773
 
762
- files_list.append(Util.get_build_dir() / Path(self.context.rust_executables).name)
763
-
764
774
  # Create a .RustExecution file to classify zipInput as a rust source code
765
775
  rust_execution_file = Util.get_build_dir() / ".RustExecution"
766
776
  rust_execution_file.touch(exist_ok=True)
767
777
  files_list.append(rust_execution_file)
768
778
 
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
779
  if attr_file := getattr(self.context, 'rust_logs_stdout', None):
775
780
  files_list.append(Util.get_build_dir() / Path(attr_file).name)
776
781
  if attr_file := getattr(self.context, 'rust_logs_stderr', None):
@@ -779,29 +784,10 @@ class CloudVerification:
779
784
  result = compress_files(self.ZipFilePath, *files_list,
780
785
  short_output=Ctx.is_minimal_cli_output(self.context))
781
786
 
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
-
787
+ elif Attrs.is_solana_app() or Attrs.is_soroban_app():
786
788
  for file in self.context.files:
787
789
  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
790
 
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
791
  result = compress_files(self.ZipFilePath, *files_list,
806
792
  short_output=Ctx.is_minimal_cli_output(self.context))
807
793
  else:
@@ -829,6 +815,8 @@ class CloudVerification:
829
815
  Util.flush_stdout()
830
816
  if not result:
831
817
  return False
818
+ if self.context.test == str(Util.TestValue.CHECK_ZIP):
819
+ raise Util.TestResultsReady(self.ZipFilePath)
832
820
 
833
821
  cloud_logger.debug("Uploading files...")
834
822
  if self.upload(self.presigned_url, self.ZipFilePath):
@@ -866,6 +854,7 @@ class CloudVerification:
866
854
  return False
867
855
 
868
856
  file_upload_success = self.__compress_and_upload_zip_files()
857
+
869
858
  if not file_upload_success:
870
859
  return False
871
860
 
@@ -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')