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