contract_build/workspace/
manifest.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17use anyhow::{
18    Context,
19    Result,
20};
21
22use super::{
23    Profile,
24    metadata,
25};
26use crate::CrateMetadata;
27
28use std::{
29    convert::TryFrom,
30    fs,
31    path::{
32        Path,
33        PathBuf,
34    },
35};
36use toml::value;
37
38const MANIFEST_FILE: &str = "Cargo.toml";
39const LEGACY_METADATA_PACKAGE_PATH: &str = ".ink/abi_gen";
40const METADATA_PACKAGE_PATH: &str = ".ink/metadata_gen";
41
42/// Path to a `Cargo.toml` file
43#[derive(Clone, Debug)]
44pub struct ManifestPath {
45    path: PathBuf,
46}
47
48impl ManifestPath {
49    /// Create a new [`ManifestPath`], errors if not path to `Cargo.toml`
50    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
51        let manifest = path.as_ref();
52        if let Some(file_name) = manifest.file_name()
53            && file_name != MANIFEST_FILE
54        {
55            anyhow::bail!("Manifest file must be a Cargo.toml")
56        }
57        Ok(ManifestPath {
58            path: manifest.into(),
59        })
60    }
61
62    /// Create an arg `--manifest-path=` for `cargo` command
63    pub fn cargo_arg(&self) -> Result<String> {
64        let path = self.path.canonicalize().map_err(|err| {
65            anyhow::anyhow!("Failed to canonicalize {:?}: {:?}", self.path, err)
66        })?;
67        Ok(format!("--manifest-path={}", path.to_string_lossy()))
68    }
69
70    /// The directory path of the manifest path.
71    ///
72    /// Returns `None` if the path is just the plain file name `Cargo.toml`
73    pub fn directory(&self) -> Option<&Path> {
74        let just_a_file_name =
75            self.path.iter().collect::<Vec<_>>() == vec![Path::new(MANIFEST_FILE)];
76        if !just_a_file_name {
77            self.path.parent()
78        } else {
79            None
80        }
81    }
82
83    /// Returns the absolute directory path of the manifest.
84    pub fn absolute_directory(&self) -> Result<PathBuf, std::io::Error> {
85        let directory = match self.directory() {
86            Some(dir) => dir,
87            None => Path::new("./"),
88        };
89        directory.canonicalize()
90    }
91}
92
93impl<P> TryFrom<Option<P>> for ManifestPath
94where
95    P: AsRef<Path>,
96{
97    type Error = anyhow::Error;
98
99    fn try_from(value: Option<P>) -> Result<Self, Self::Error> {
100        value.map_or(Ok(Default::default()), ManifestPath::new)
101    }
102}
103
104impl Default for ManifestPath {
105    fn default() -> ManifestPath {
106        ManifestPath::new(MANIFEST_FILE).expect("it's a valid manifest file")
107    }
108}
109
110impl AsRef<Path> for ManifestPath {
111    fn as_ref(&self) -> &Path {
112        self.path.as_ref()
113    }
114}
115
116impl From<ManifestPath> for PathBuf {
117    fn from(path: ManifestPath) -> Self {
118        path.path
119    }
120}
121
122/// Create, amend and save a copy of the specified `Cargo.toml`.
123pub struct Manifest {
124    path: ManifestPath,
125    toml: value::Table,
126    /// True if a metadata package should be generated for this manifest
127    metadata_package: bool,
128}
129
130impl Manifest {
131    /// Create new Manifest for the given manifest path.
132    ///
133    /// The path *must* be to a `Cargo.toml`.
134    pub fn new(manifest_path: ManifestPath) -> Result<Manifest> {
135        let toml = fs::read_to_string(&manifest_path).context("Loading Cargo.toml")?;
136        let toml: value::Table = toml::from_str(&toml)?;
137        let manifest = Manifest {
138            path: manifest_path,
139            toml,
140            metadata_package: false,
141        };
142        Ok(manifest)
143    }
144
145    /// Get the name of the package.
146    fn name(&self) -> Result<&str> {
147        self.toml
148            .get("package")
149            .ok_or_else(|| anyhow::anyhow!("package section not found"))?
150            .as_table()
151            .ok_or_else(|| anyhow::anyhow!("package section should be a table"))?
152            .get("name")
153            .ok_or_else(|| anyhow::anyhow!("package must have a name"))?
154            .as_str()
155            .ok_or_else(|| anyhow::anyhow!("package name must be a string"))
156    }
157
158    /// Get a mutable reference to the `[lib]` section.
159    fn lib_target_mut(&mut self) -> Result<&mut value::Table> {
160        self.toml
161            .get_mut("lib")
162            .ok_or_else(|| anyhow::anyhow!("lib section not found"))?
163            .as_table_mut()
164            .ok_or_else(|| anyhow::anyhow!("lib section should be a table"))
165    }
166
167    /// Get mutable reference to `[lib] crate-types = []` section.
168    fn crate_types_mut(&mut self) -> Result<&mut value::Array> {
169        let crate_types = self
170            .lib_target_mut()?
171            .entry("crate-type")
172            .or_insert(value::Value::Array(Default::default()));
173
174        crate_types
175            .as_array_mut()
176            .ok_or_else(|| anyhow::anyhow!("crate-types should be an Array"))
177    }
178
179    /// Replaces the `[lib]` target by a single `[bin]` target using the same file and
180    /// name.
181    pub fn with_replaced_lib_to_bin(&mut self) -> Result<&mut Self> {
182        let mut lib = self.lib_target_mut()?.clone();
183        self.toml.remove("lib");
184        if !lib.contains_key("name") {
185            lib.insert("name".into(), self.name()?.into());
186        }
187        lib.remove("crate-types");
188        self.toml.insert("bin".into(), vec![lib].into());
189        Ok(self)
190    }
191
192    /// Add a value to the `[lib] crate-types = []` section.
193    ///
194    /// If the value already exists, does nothing.
195    pub fn with_added_crate_type(&mut self, crate_type: &str) -> Result<&mut Self> {
196        let crate_types = self.crate_types_mut()?;
197        if !crate_type_exists(crate_type, crate_types) {
198            crate_types.push(crate_type.into());
199        }
200        Ok(self)
201    }
202
203    /// Set preferred defaults for the `[profile.release]` section
204    ///
205    /// # Note
206    ///
207    /// Existing user defined settings for this section are preserved. Only if a setting
208    /// is not defined is the preferred default set.
209    pub fn with_profile_release_defaults(
210        &mut self,
211        defaults: Profile,
212    ) -> Result<&mut Self> {
213        let profile_release = self.profile_release_table_mut()?;
214        defaults.merge(profile_release);
215        Ok(self)
216    }
217
218    /// Set `[workspace]` section to an empty table. When building a contract project any
219    /// workspace members are not copied to the temporary workspace, so need to be
220    /// removed.
221    ///
222    /// Additionally, where no workspace is already specified, this can in some cases
223    /// reduce the size of the contract.
224    pub fn with_empty_workspace(&mut self) -> &mut Self {
225        self.toml
226            .insert("workspace".into(), value::Value::Table(Default::default()));
227        self
228    }
229
230    /// Get mutable reference to `[profile.release]` section
231    fn profile_release_table_mut(&mut self) -> Result<&mut value::Table> {
232        let profile = self
233            .toml
234            .entry("profile")
235            .or_insert(value::Value::Table(Default::default()));
236        let release = profile
237            .as_table_mut()
238            .ok_or_else(|| anyhow::anyhow!("profile should be a table"))?
239            .entry("release")
240            .or_insert(value::Value::Table(Default::default()));
241        release
242            .as_table_mut()
243            .ok_or_else(|| anyhow::anyhow!("release should be a table"))
244    }
245
246    /// Remove a value from the `[lib] crate-types = []` section
247    ///
248    /// If the value does not exist, does nothing.
249    pub fn with_removed_crate_type(&mut self, crate_type: &str) -> Result<&mut Self> {
250        let crate_types = self.crate_types_mut()?;
251        if crate_type_exists(crate_type, crate_types) {
252            crate_types.retain(|v| v.as_str() != Some(crate_type));
253        }
254        Ok(self)
255    }
256
257    /// Adds a metadata package to the manifest workspace for generating metadata
258    pub fn with_metadata_package(&mut self) -> Result<&mut Self> {
259        let workspace = self
260            .toml
261            .entry("workspace")
262            .or_insert(value::Value::Table(Default::default()));
263        let members = workspace
264            .as_table_mut()
265            .ok_or_else(|| anyhow::anyhow!("workspace should be a table"))?
266            .entry("members")
267            .or_insert(value::Value::Array(Default::default()))
268            .as_array_mut()
269            .ok_or_else(|| anyhow::anyhow!("members should be an array"))?;
270
271        if members.contains(&LEGACY_METADATA_PACKAGE_PATH.into()) {
272            // warn user if they have legacy metadata generation artifacts
273            use colored::Colorize;
274            eprintln!(
275                "{} {} {} {}",
276                "warning:".yellow().bold(),
277                "please remove".bold(),
278                LEGACY_METADATA_PACKAGE_PATH.bold(),
279                "from the `[workspace]` section in the `Cargo.toml`, \
280                and delete that directory. These are now auto-generated."
281                    .bold()
282            );
283        } else {
284            members.push(METADATA_PACKAGE_PATH.into());
285        }
286
287        self.metadata_package = true;
288        Ok(self)
289    }
290
291    pub fn with_dylint(&mut self) -> Result<&mut Self> {
292        let ink_dylint = |lib_name: &str| {
293            let mut map = value::Table::new();
294            map.insert("git".into(), crate::lint::GIT_URL.into());
295            map.insert("rev".into(), crate::lint::GIT_REV.into());
296            map.insert(
297                "pattern".into(),
298                value::Value::String(format!("linting/{lib_name}")),
299            );
300            value::Value::Table(map)
301        };
302
303        self.toml
304            .entry("workspace")
305            .or_insert(value::Value::Table(Default::default()))
306            .as_table_mut()
307            .context("workspace section should be a table")?
308            .entry("metadata")
309            .or_insert(value::Value::Table(Default::default()))
310            .as_table_mut()
311            .context("workspace.metadata section should be a table")?
312            .entry("dylint")
313            .or_insert(value::Value::Table(Default::default()))
314            .as_table_mut()
315            .context("workspace.metadata.dylint section should be a table")?
316            .entry("libraries")
317            .or_insert(value::Value::Array(Default::default()))
318            .as_array_mut()
319            .context("workspace.metadata.dylint.libraries section should be an array")?
320            .extend(vec![ink_dylint("mandatory"), ink_dylint("extra")]);
321
322        Ok(self)
323    }
324
325    /// Merge the workspace dependencies with the crate dependencies.
326    pub fn with_merged_workspace_dependencies(
327        &mut self,
328        crate_metadata: &CrateMetadata,
329    ) -> Result<&mut Self> {
330        let workspace_manifest_path =
331            crate_metadata.cargo_meta.workspace_root.join("Cargo.toml");
332
333        // If the workspace manifest is the same as the crate manifest, there's not
334        // workspace to fix
335        if workspace_manifest_path == self.path.path {
336            return Ok(self)
337        }
338
339        let workspace_toml =
340            fs::read_to_string(&workspace_manifest_path).context("Loading Cargo.toml")?;
341        let workspace_toml: value::Table = toml::from_str(&workspace_toml)?;
342
343        let workspace_dependencies = workspace_toml
344            .get("workspace")
345            .ok_or_else(|| {
346                anyhow::anyhow!("[workspace] should exist in workspace manifest")
347            })?
348            .as_table()
349            .ok_or_else(|| anyhow::anyhow!("[workspace] should be a table"))?
350            .get("dependencies");
351
352        // If no workspace dependencies are defined, return
353        let Some(workspace_dependencies) = workspace_dependencies else {
354            return Ok(self)
355        };
356
357        let workspace_dependencies =
358            workspace_dependencies.as_table().ok_or_else(|| {
359                anyhow::anyhow!("[workspace.dependencies] should be a table")
360            })?;
361
362        merge_workspace_with_crate_dependencies(
363            "dependencies",
364            &mut self.toml,
365            workspace_dependencies,
366        )?;
367        merge_workspace_with_crate_dependencies(
368            "dev-dependencies",
369            &mut self.toml,
370            workspace_dependencies,
371        )?;
372
373        Ok(self)
374    }
375
376    /// Replace relative paths with absolute paths with the working directory.
377    ///
378    /// Enables the use of a temporary amended copy of the manifest.
379    ///
380    /// # Rewrites
381    ///
382    /// - `[lib]/path`
383    /// - `[dependencies]`
384    pub fn rewrite_relative_paths(&mut self) -> Result<()> {
385        let manifest_dir = self.path.absolute_directory()?;
386        let path_rewrite = PathRewrite { manifest_dir };
387        path_rewrite.rewrite_relative_paths(&mut self.toml)
388    }
389
390    /// Writes the amended manifest to the given path.
391    pub fn write(&self, manifest_path: &ManifestPath) -> Result<()> {
392        if let Some(dir) = manifest_path.directory() {
393            fs::create_dir_all(dir)
394                .context(format!("Creating directory '{}'", dir.display()))?;
395        }
396
397        if self.metadata_package {
398            let dir = if let Some(manifest_dir) = manifest_path.directory() {
399                manifest_dir.join(METADATA_PACKAGE_PATH)
400            } else {
401                METADATA_PACKAGE_PATH.into()
402            };
403
404            fs::create_dir_all(&dir)
405                .context(format!("Creating directory '{}'", dir.display()))?;
406
407            let contract_package_name = self
408                .toml
409                .get("package")
410                .ok_or_else(|| anyhow::anyhow!("package section not found"))?
411                .get("name")
412                .ok_or_else(|| anyhow::anyhow!("[package] name field not found"))?
413                .as_str()
414                .ok_or_else(|| anyhow::anyhow!("[package] name should be a string"))?;
415
416            let ink_crate = self
417                .toml
418                .get("dependencies")
419                .ok_or_else(|| anyhow::anyhow!("[dependencies] section not found"))?
420                .get("ink")
421                .ok_or_else(|| anyhow::anyhow!("ink dependency not found"))?
422                .as_table()
423                .ok_or_else(|| anyhow::anyhow!("ink dependency should be a table"))?;
424
425            let features = self
426                .toml
427                .get("features")
428                .ok_or_else(|| anyhow::anyhow!("[features] section not found"))?
429                .as_table()
430                .ok_or_else(|| anyhow::anyhow!("[features] section should be a table"))?;
431
432            metadata::generate_package(
433                dir,
434                contract_package_name,
435                ink_crate.clone(),
436                features,
437            )?;
438        }
439
440        let updated_toml = toml::to_string(&self.toml)?;
441        tracing::debug!(
442            "Writing updated manifest to '{}'",
443            manifest_path.as_ref().display()
444        );
445        fs::write(manifest_path, updated_toml)?;
446        Ok(())
447    }
448}
449
450/// Replace relative paths with absolute paths with the working directory.
451struct PathRewrite {
452    manifest_dir: PathBuf,
453}
454
455impl PathRewrite {
456    /// Replace relative paths with absolute paths with the working directory.
457    fn rewrite_relative_paths(&self, toml: &mut value::Table) -> Result<()> {
458        // Rewrite `[package.build]` path to an absolute path.
459        if let Some(package) = toml.get_mut("package") {
460            let package = package
461                .as_table_mut()
462                .ok_or_else(|| anyhow::anyhow!("`[package]` should be a table"))?;
463            if let Some(build) = package.get_mut("build") {
464                self.to_absolute_path("[package.build]".to_string(), build)?
465            }
466        }
467
468        // Rewrite `[lib] path = /path/to/lib.rs`
469        if let Some(lib) = toml.get_mut("lib") {
470            self.rewrite_path(lib, "lib", "src/lib.rs")?;
471        }
472
473        // Rewrite `[[bin]] path = /path/to/main.rs`
474        if let Some(bin) = toml.get_mut("bin") {
475            let bins = bin.as_array_mut().ok_or_else(|| {
476                anyhow::anyhow!("'[[bin]]' section should be a table array")
477            })?;
478
479            // Rewrite `[[bin]] path =` value to an absolute path.
480            for bin in bins {
481                self.rewrite_path(bin, "[bin]", "src/main.rs")?;
482            }
483        }
484
485        self.rewrite_dependencies_relative_paths(toml, "dependencies")?;
486        self.rewrite_dependencies_relative_paths(toml, "dev-dependencies")?;
487
488        Ok(())
489    }
490
491    fn rewrite_path(
492        &self,
493        table_value: &mut value::Value,
494        table_section: &str,
495        default: &str,
496    ) -> Result<()> {
497        let table = table_value.as_table_mut().ok_or_else(|| {
498            anyhow::anyhow!("'[{table_section}]' section should be a table")
499        })?;
500
501        match table.get_mut("path") {
502            Some(existing_path) => {
503                self.to_absolute_path(format!("[{table_section}]/path"), existing_path)
504            }
505            None => {
506                let default_path = PathBuf::from(default);
507                if !default_path.exists() {
508                    anyhow::bail!(
509                        "No path specified, and the default `{default}` was not found"
510                    )
511                }
512                let path = self.manifest_dir.join(default_path);
513                tracing::debug!("Adding default path '{}'", path.display());
514                table.insert(
515                    "path".into(),
516                    value::Value::String(path.to_string_lossy().into()),
517                );
518                Ok(())
519            }
520        }
521    }
522
523    /// Expand a relative path to an absolute path.
524    fn to_absolute_path(
525        &self,
526        value_id: String,
527        existing_path: &mut value::Value,
528    ) -> Result<()> {
529        let path_str = existing_path
530            .as_str()
531            .ok_or_else(|| anyhow::anyhow!("{value_id} should be a string"))?;
532        #[cfg(windows)]
533        // On Windows path separators are `\`, hence we need to replace the `/` in
534        // e.g. `src/lib.rs`.
535        let path_str = &path_str.replace("/", "\\");
536        let path = PathBuf::from(path_str);
537        if path.is_relative() {
538            let lib_abs = self.manifest_dir.join(path);
539            tracing::debug!("Rewriting {} to '{}'", value_id, lib_abs.display());
540            *existing_path = value::Value::String(lib_abs.to_string_lossy().into())
541        }
542        Ok(())
543    }
544
545    /// Rewrite the relative paths of dependencies.
546    fn rewrite_dependencies_relative_paths(
547        &self,
548        toml: &mut value::Table,
549        section_name: &str,
550    ) -> Result<()> {
551        if let Some(dependencies) = toml.get_mut(section_name) {
552            let table = dependencies
553                .as_table_mut()
554                .ok_or_else(|| anyhow::anyhow!("dependencies should be a table"))?;
555            for (name, value) in table {
556                let package_name = {
557                    let package = value.get("package");
558                    let package_name = package.and_then(|p| p.as_str()).unwrap_or(name);
559                    package_name.to_string()
560                };
561
562                if let Some(dependency) = value.as_table_mut()
563                    && let Some(dep_path) = dependency.get_mut("path")
564                {
565                    self.to_absolute_path(
566                        format!("dependency {package_name}"),
567                        dep_path,
568                    )?;
569                }
570            }
571        }
572        Ok(())
573    }
574}
575
576fn crate_type_exists(crate_type: &str, crate_types: &[value::Value]) -> bool {
577    crate_types.iter().any(|v| v.as_str() == Some(crate_type))
578}
579
580fn merge_workspace_with_crate_dependencies(
581    section_name: &str,
582    crate_toml: &mut value::Table,
583    workspace_dependencies: &value::Table,
584) -> Result<()> {
585    let Some(dependencies) = crate_toml.get_mut(section_name) else {
586        return Ok(())
587    };
588
589    let table = dependencies
590        .as_table_mut()
591        .ok_or_else(|| anyhow::anyhow!("dependencies should be a table"))?;
592
593    for (name, value) in table {
594        let Some(dependency) = value.as_table_mut() else {
595            continue
596        };
597
598        let is_workspace_dependency = dependency
599            .get_mut("workspace")
600            .unwrap_or(&mut toml::Value::Boolean(false))
601            .as_bool()
602            .unwrap_or(false);
603        if !is_workspace_dependency {
604            continue
605        }
606
607        let workspace_dependency = workspace_dependencies.get(name).ok_or_else(|| {
608            anyhow::anyhow!("'{name}' is not a key in workspace_dependencies")
609        })?;
610        let workspace_dependency = match workspace_dependency {
611            toml::Value::Table(table) => table.to_owned(),
612            // If the workspace dependency is just a version string, we create a table
613            toml::Value::String(version) => {
614                let mut table = toml::value::Table::new();
615                table.insert("version".to_string(), toml::Value::String(version.clone()));
616                table
617            }
618            // If the workspace dependency is invalid, we throw an error
619            _ => {
620                anyhow::bail!("Invalid workspace dependency for {name}");
621            }
622        };
623
624        dependency.remove("workspace");
625        for (key, value) in workspace_dependency {
626            if let Some(config) = dependency.get_mut(&key) {
627                // If it's an array we merge the values,
628                // otherwise we keep the crate value.
629                if let toml::Value::Array(value) = value
630                    && let toml::Value::Array(config) = config
631                {
632                    config.extend(value.clone());
633
634                    let mut new_config = Vec::new();
635                    for v in config.iter() {
636                        if !new_config.contains(v) {
637                            new_config.push(v.clone());
638                        }
639                    }
640                    *config = new_config;
641                }
642            } else {
643                dependency.insert(key.clone(), value.clone());
644            }
645        }
646    }
647
648    Ok(())
649}
650
651#[cfg(test)]
652mod test {
653    use super::ManifestPath;
654    use crate::util::tests::with_tmp_dir;
655    use std::fs;
656
657    #[test]
658    fn must_return_absolute_path_from_absolute_path() {
659        with_tmp_dir(|path| {
660            // given
661            let cargo_toml_path = path.join("Cargo.toml");
662            let _ = fs::File::create(&cargo_toml_path).expect("file creation failed");
663            let manifest_path = ManifestPath::new(cargo_toml_path)
664                .expect("manifest path creation failed");
665
666            // when
667            let absolute_path = manifest_path
668                .absolute_directory()
669                .expect("absolute path extraction failed");
670
671            // then
672            assert_eq!(absolute_path.as_path(), path);
673            Ok(())
674        })
675    }
676}