multiversx_sc_meta_lib/cargo_toml/
cargo_toml_contents.rs

1use std::{
2    fs,
3    io::Write,
4    path::{Path, PathBuf},
5};
6
7use toml::{value::Table, Value};
8
9use crate::contract::sc_config::ContractVariantProfile;
10
11use super::DependencyRawValue;
12
13pub const CARGO_TOML_DEPENDENCIES: &str = "dependencies";
14pub const CARGO_TOML_DEV_DEPENDENCIES: &str = "dev-dependencies";
15pub const PACKAGE: &str = "package";
16pub const AUTHORS: &str = "authors";
17const AUTO_GENERATED: &str = "# Code generated by the multiversx-sc build system. DO NOT EDIT.
18
19# ##########################################
20# ############## AUTO-GENERATED #############
21# ##########################################
22
23";
24
25/// Contains an in-memory representation of a Cargo.toml file.
26///
27/// Implementation notes:
28///
29/// - Currently contains a raw toml tree, but in principle it could also work with a cargo_toml::Manifest.
30/// - It keeps an ordered representation, thanks to the `toml` `preserve_order` feature.
31#[derive(Clone, Debug)]
32pub struct CargoTomlContents {
33    pub path: PathBuf,
34    pub toml_value: toml::Value,
35    pub prepend_auto_generated_comment: bool,
36}
37
38impl CargoTomlContents {
39    pub fn parse_string(raw_str: &str, path: &Path) -> Self {
40        let toml_value = raw_str.parse::<toml::Value>().unwrap_or_else(|e| {
41            panic!(
42                "failed to parse Cargo.toml toml format, path:{}, error: {e}",
43                path.display()
44            )
45        });
46        CargoTomlContents {
47            path: path.to_owned(),
48            toml_value,
49            prepend_auto_generated_comment: false,
50        }
51    }
52
53    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Self {
54        let path_ref = path.as_ref();
55        let cargo_toml_content = fs::read(path_ref).expect("failed to open Cargo.toml file");
56        let cargo_toml_content_str =
57            String::from_utf8(cargo_toml_content).expect("error decoding Cargo.toml utf-8");
58        Self::parse_string(&cargo_toml_content_str, path_ref)
59    }
60
61    pub fn new() -> Self {
62        CargoTomlContents {
63            path: PathBuf::new(),
64            toml_value: toml::Value::Table(Table::new()),
65            prepend_auto_generated_comment: false,
66        }
67    }
68
69    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) {
70        let cargo_toml_content_str = &self.to_string_pretty();
71        let mut file = std::fs::File::create(path).expect("failed to create Cargo.toml file");
72        file.write_all(cargo_toml_content_str.as_bytes())
73            .expect("failed to write Cargo.toml contents to file");
74    }
75
76    pub fn package_name(&self) -> String {
77        self.toml_value
78            .get(PACKAGE)
79            .expect("missing package in Cargo.toml")
80            .get("name")
81            .expect("missing package name in Cargo.toml")
82            .as_str()
83            .expect("package name not a string value")
84            .to_string()
85    }
86
87    pub fn package_edition(&self) -> String {
88        self.toml_value
89            .get(PACKAGE)
90            .expect("missing package in Cargo.toml")
91            .get("edition")
92            .expect("missing package name in Cargo.toml")
93            .as_str()
94            .expect("package name not a string value")
95            .to_string()
96    }
97
98    /// Interprets the dependency value and organizes values in a struct.
99    pub fn dependency_raw_value(&self, crate_name: &str) -> Option<DependencyRawValue> {
100        self.dependency(crate_name)
101            .map(DependencyRawValue::parse_toml_value)
102    }
103
104    pub fn insert_dependency_raw_value(&mut self, crate_name: &str, raw_value: DependencyRawValue) {
105        self.dependencies_mut()
106            .insert(crate_name.to_owned(), raw_value.into_toml_value());
107    }
108
109    /// Assumes that a package section already exists.
110    pub fn change_package_name(&mut self, new_package_name: String) {
111        let package = self
112            .toml_value
113            .get_mut("package")
114            .expect("missing package in Cargo.toml");
115        package
116            .as_table_mut()
117            .expect("malformed package in Cargo.toml")
118            .insert("name".to_string(), toml::Value::String(new_package_name));
119    }
120
121    pub fn dependencies_table(&self) -> Option<&Table> {
122        if let Some(deps) = self.toml_value.get(CARGO_TOML_DEPENDENCIES) {
123            deps.as_table()
124        } else if let Some(deps) = self.toml_value.get(CARGO_TOML_DEV_DEPENDENCIES) {
125            deps.as_table()
126        } else {
127            None
128        }
129    }
130
131    pub fn dependency(&self, dep_name: &str) -> Option<&Value> {
132        if let Some(deps_map) = self.dependencies_table() {
133            deps_map.get(dep_name)
134        } else {
135            None
136        }
137    }
138
139    pub fn has_dependencies(&self) -> bool {
140        self.toml_value.get(CARGO_TOML_DEPENDENCIES).is_some()
141    }
142
143    pub fn dependencies_mut(&mut self) -> &mut Table {
144        self.toml_value
145            .as_table_mut()
146            .expect("add deps cargo toml error wasm adapter")
147            .entry(CARGO_TOML_DEPENDENCIES)
148            .or_insert(toml::Value::Table(toml::map::Map::new()))
149            .as_table_mut()
150            .expect("malformed crate Cargo.toml")
151    }
152
153    pub fn has_dev_dependencies(&self) -> bool {
154        self.toml_value.get(CARGO_TOML_DEV_DEPENDENCIES).is_some()
155    }
156
157    pub fn change_author(&mut self, authors: String) -> bool {
158        let package = self
159            .toml_value
160            .get_mut(PACKAGE)
161            .unwrap_or_else(|| panic!("no dependencies found in crate {}", self.path.display()))
162            .as_table_mut()
163            .expect("missing package in Cargo.toml");
164
165        package.remove(AUTHORS);
166
167        package.insert(
168            AUTHORS.to_owned(),
169            toml::Value::Array(vec![toml::Value::String(authors)]),
170        );
171
172        true
173    }
174
175    pub fn dev_dependencies_mut(&mut self) -> &mut Table {
176        self.toml_value
177            .get_mut(CARGO_TOML_DEV_DEPENDENCIES)
178            .unwrap_or_else(|| panic!("no dependencies found in crate {}", self.path.display()))
179            .as_table_mut()
180            .expect("malformed crate Cargo.toml")
181    }
182
183    pub fn add_crate_type(&mut self, crate_type: &str) {
184        let mut value = toml::map::Map::new();
185        let array = vec![toml::Value::String(crate_type.to_string())];
186        let members = toml::Value::Array(array);
187        value.insert("crate-type".to_string(), members);
188
189        self.toml_value
190            .as_table_mut()
191            .expect("malformed package in Cargo.toml")
192            .insert("lib".to_string(), toml::Value::Table(value));
193    }
194
195    pub fn add_package_info(
196        &mut self,
197        name: &String,
198        version: String,
199        current_edition: String,
200        publish: bool,
201    ) {
202        let mut value = toml::map::Map::new();
203        value.insert("name".to_string(), Value::String(name.to_string()));
204        value.insert("version".to_string(), Value::String(version));
205        value.insert("edition".to_string(), Value::String(current_edition));
206        value.insert("publish".to_string(), Value::Boolean(publish));
207
208        self.toml_value
209            .as_table_mut()
210            .expect("malformed package in Cargo.toml / add_package")
211            .insert("package".to_string(), toml::Value::Table(value));
212    }
213
214    pub fn add_contract_variant_profile(&mut self, contract_profile: &ContractVariantProfile) {
215        let mut profile_props = toml::map::Map::new();
216        profile_props.insert(
217            "codegen-units".to_string(),
218            Value::Integer(contract_profile.codegen_units.into()),
219        );
220        profile_props.insert(
221            "opt-level".to_string(),
222            Value::String(contract_profile.opt_level.to_owned()),
223        );
224        profile_props.insert("lto".to_string(), Value::Boolean(contract_profile.lto));
225        profile_props.insert("debug".to_string(), Value::Boolean(contract_profile.debug));
226        profile_props.insert(
227            "panic".to_string(),
228            Value::String(contract_profile.panic.to_owned()),
229        );
230        profile_props.insert(
231            "overflow-checks".to_string(),
232            Value::Boolean(contract_profile.overflow_checks),
233        );
234
235        // add contract variant profile
236        let mut toml_table = toml::map::Map::new();
237        toml_table.insert("release".to_string(), toml::Value::Table(profile_props));
238
239        // add profile dev
240        let mut dev_value = toml::map::Map::new();
241        dev_value.insert("panic".to_string(), Value::String("abort".to_string()));
242        toml_table.insert("dev".to_string(), toml::Value::Table(dev_value));
243
244        self.toml_value
245            .as_table_mut()
246            .expect("malformed package in Cargo.toml")
247            .insert("profile".to_string(), toml::Value::Table(toml_table));
248    }
249
250    pub fn add_workspace(&mut self, members: &[&str]) {
251        let array: Vec<toml::Value> = members
252            .iter()
253            .map(|s| toml::Value::String(s.to_string()))
254            .collect();
255        let members_toml = toml::Value::Array(array);
256
257        let mut workspace = toml::Value::Table(toml::map::Map::new());
258        workspace
259            .as_table_mut()
260            .expect("malformed package in Cargo.toml")
261            .insert("members".to_string(), members_toml);
262
263        self.toml_value
264            .as_table_mut()
265            .expect("malformed package in Cargo.toml")
266            .insert("workspace".to_string(), workspace);
267    }
268
269    pub fn local_dependency_paths(&self, ignore_deps: &[&str]) -> Vec<String> {
270        let mut result = Vec::new();
271        if let Some(deps_map) = self.dependencies_table() {
272            for (key, value) in deps_map {
273                if ignore_deps.contains(&key.as_str()) {
274                    continue;
275                }
276
277                if let Some(path) = value.get("path") {
278                    result.push(path.as_str().expect("path is not a string").to_string());
279                }
280            }
281        }
282        result
283    }
284
285    pub fn change_features_for_parent_crate_dep(
286        &mut self,
287        features: &[String],
288        default_features: Option<bool>,
289    ) {
290        let deps_mut = self.dependencies_mut();
291        for (_, dep) in deps_mut {
292            if is_dep_path_above(dep) {
293                let feature_values = features
294                    .iter()
295                    .map(|feature| Value::String(feature.clone()))
296                    .collect();
297                let deps_table = dep.as_table_mut().expect("malformed crate Cargo.toml");
298                deps_table.insert("features".to_string(), Value::Array(feature_values));
299                if let Some(default_features_value) = default_features {
300                    deps_table.insert(
301                        "default-features".to_string(),
302                        Value::Boolean(default_features_value),
303                    );
304                }
305            }
306        }
307    }
308
309    pub fn to_string_pretty(&self) -> String {
310        let toml_string =
311            toml::to_string_pretty(&self.toml_value).expect("failed to format Cargo.toml contents");
312        if self.prepend_auto_generated_comment {
313            return format!("{}{}", AUTO_GENERATED, toml_string);
314        }
315
316        toml_string
317    }
318}
319
320/// Checks that path == ".." in a dependency.
321fn is_dep_path_above(dep: &Value) -> bool {
322    if let Some(path) = dep.get("path") {
323        if let Some(s) = path.as_str() {
324            return s == "..";
325        }
326    }
327
328    false
329}
330
331pub fn change_from_base_to_adapter_path(base_path: &Path) -> PathBuf {
332    let path = base_path.to_string_lossy().replace("base", "wasm-adapter");
333
334    Path::new("..").join(path)
335}
336
337/// TODO: still useful?
338#[allow(unused)]
339fn remove_quotes(var: &Value) -> String {
340    var.to_string().replace('\"', "")
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::cargo_toml::{DependencyReference, GitCommitReference, VersionReq};
347
348    #[test]
349    fn test_change_from_base_to_adapter_path() {
350        let base_path = Path::new("..")
351            .join("..")
352            .join("..")
353            .join("framework")
354            .join("base");
355        let adapter_path = Path::new("..")
356            .join("..")
357            .join("..")
358            .join("..")
359            .join("framework")
360            .join("wasm-adapter");
361
362        assert_eq!(
363            super::change_from_base_to_adapter_path(&base_path),
364            adapter_path
365        );
366    }
367
368    const CARGO_TOML_RAW: &str = r#"
369[dependencies.by-version-1]
370version = "0.54.0"
371
372[dependencies.by-version-1-strict]
373version = "=0.54.1"
374
375[dependencies.by-git-commit-1]
376git = "https://github.com/multiversx/repo1"
377rev = "85c31b9ce730bd5ffe41589c353d935a14baaa96"
378
379[dependencies.by-path-1]
380path = "a/b/c"
381
382[dependencies]
383by-version-2 = "0.54.2"
384by-version-2-strict = "=0.54.3"
385by-path-2 = { path = "d/e/f" }
386by-git-commit-2 = { git = "https://github.com/multiversx/repo2", rev = "e990be823f26d1e7f59c71536d337b7240dc3fa2" }
387    "#;
388
389    #[test]
390    fn test_dependency_value() {
391        let cargo_toml = CargoTomlContents::parse_string(CARGO_TOML_RAW, Path::new("test"));
392
393        // version
394        let raw_value = cargo_toml.dependency_raw_value("by-version-1").unwrap();
395        assert_eq!(
396            raw_value,
397            DependencyRawValue {
398                version: Some("0.54.0".to_owned()),
399                ..Default::default()
400            },
401        );
402        assert_eq!(
403            raw_value.interpret(),
404            DependencyReference::Version(VersionReq::from_version_str("0.54.0").unwrap()),
405        );
406
407        // version, strict
408        let raw_value = cargo_toml
409            .dependency_raw_value("by-version-1-strict")
410            .unwrap();
411        assert_eq!(
412            raw_value,
413            DependencyRawValue {
414                version: Some("=0.54.1".to_owned()),
415                ..Default::default()
416            },
417        );
418        assert_eq!(
419            raw_value.interpret(),
420            DependencyReference::Version(VersionReq::from_version_str("0.54.1").unwrap().strict()),
421        );
422
423        // version, compact
424        let raw_value = cargo_toml.dependency_raw_value("by-version-2").unwrap();
425        assert_eq!(
426            raw_value,
427            DependencyRawValue {
428                version: Some("0.54.2".to_owned()),
429                ..Default::default()
430            },
431        );
432        assert_eq!(
433            raw_value.interpret(),
434            DependencyReference::Version(VersionReq::from_version_str("0.54.2").unwrap()),
435        );
436
437        // version, compact, strict
438        let raw_value = cargo_toml
439            .dependency_raw_value("by-version-2-strict")
440            .unwrap();
441        assert_eq!(
442            raw_value,
443            DependencyRawValue {
444                version: Some("=0.54.3".to_owned()),
445                ..Default::default()
446            },
447        );
448        assert_eq!(
449            raw_value.interpret(),
450            DependencyReference::Version(VersionReq::from_version_str("0.54.3").unwrap().strict()),
451        );
452
453        // git
454        let raw_value = cargo_toml.dependency_raw_value("by-git-commit-1").unwrap();
455        assert_eq!(
456            raw_value,
457            DependencyRawValue {
458                git: Some("https://github.com/multiversx/repo1".to_owned()),
459                rev: Some("85c31b9ce730bd5ffe41589c353d935a14baaa96".to_owned()),
460                ..Default::default()
461            },
462        );
463        assert_eq!(
464            raw_value.interpret(),
465            DependencyReference::GitCommit(GitCommitReference {
466                git: "https://github.com/multiversx/repo1".to_owned(),
467                rev: "85c31b9ce730bd5ffe41589c353d935a14baaa96".to_owned(),
468            })
469        );
470
471        // git, compact
472        let raw_value = cargo_toml.dependency_raw_value("by-git-commit-2").unwrap();
473        assert_eq!(
474            raw_value,
475            DependencyRawValue {
476                git: Some("https://github.com/multiversx/repo2".to_owned()),
477                rev: Some("e990be823f26d1e7f59c71536d337b7240dc3fa2".to_owned()),
478                ..Default::default()
479            },
480        );
481        assert_eq!(
482            raw_value.interpret(),
483            DependencyReference::GitCommit(GitCommitReference {
484                git: "https://github.com/multiversx/repo2".to_owned(),
485                rev: "e990be823f26d1e7f59c71536d337b7240dc3fa2".to_owned(),
486            })
487        );
488
489        // path
490        let raw_value = cargo_toml.dependency_raw_value("by-path-1").unwrap();
491        let path = Path::new("a").join("b").join("c");
492        assert_eq!(
493            raw_value,
494            DependencyRawValue {
495                path: Some(path.clone()),
496                ..Default::default()
497            },
498        );
499        assert_eq!(raw_value.interpret(), DependencyReference::Path(path),);
500
501        // path, compact
502        let raw_value = cargo_toml.dependency_raw_value("by-path-2").unwrap();
503        let path = Path::new("d").join("e").join("f");
504        assert_eq!(
505            raw_value,
506            DependencyRawValue {
507                path: Some(path.clone()),
508                ..Default::default()
509            },
510        );
511        assert_eq!(raw_value.interpret(), DependencyReference::Path(path),);
512    }
513}