contract_build/
crate_metadata.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 crate::{
18    ManifestPath,
19    Target,
20};
21use anyhow::{
22    Context,
23    Result,
24};
25use cargo_metadata::{
26    Metadata as CargoMetadata,
27    MetadataCommand,
28    Package,
29    TargetKind,
30};
31use semver::Version;
32use serde_json::{
33    Map,
34    Value,
35};
36use std::{
37    fs,
38    path::PathBuf,
39};
40use toml::value;
41use url::Url;
42
43/// Relevant metadata obtained from Cargo.toml.
44#[derive(Debug)]
45pub struct CrateMetadata {
46    pub manifest_path: ManifestPath,
47    pub cargo_meta: cargo_metadata::Metadata,
48    pub contract_artifact_name: String,
49    pub root_package: Package,
50    pub original_code: PathBuf,
51    pub dest_code: PathBuf,
52    pub ink_version: Version,
53    pub documentation: Option<Url>,
54    pub homepage: Option<Url>,
55    pub user: Option<Map<String, Value>>,
56    pub target_directory: PathBuf,
57    pub target_file_path: PathBuf,
58}
59
60impl CrateMetadata {
61    /// Attempt to construct [`CrateMetadata`] from the given manifest path.
62    pub fn from_manifest_path(
63        manifest_path: Option<&PathBuf>,
64        target: Target,
65    ) -> Result<Self> {
66        let manifest_path = ManifestPath::try_from(manifest_path)?;
67        Self::collect(&manifest_path, target)
68    }
69
70    /// Parses the contract manifest and returns relevant metadata.
71    pub fn collect(manifest_path: &ManifestPath, target: Target) -> Result<Self> {
72        let (metadata, root_package) = get_cargo_metadata(manifest_path)?;
73        let mut target_directory = metadata.target_directory.as_path().join("ink");
74
75        // Normalize the final contract artifact name.
76        let contract_artifact_name = root_package.name.replace('-', "_");
77
78        if let Some(lib_name) = &root_package
79            .targets
80            .iter()
81            .find(|target| target.kind.iter().any(|f| *f == TargetKind::Lib))
82        {
83            if lib_name.name != root_package.name {
84                // warn user if they still specify a lib name different from the
85                // package name
86                use colored::Colorize;
87                eprintln!(
88                    "{} the `name` field in the `[lib]` section of the `Cargo.toml`, \
89                    is no longer used for the name of generated contract artifacts. \
90                    The package name is used instead. Remove the `[lib] name` to \
91                    stop this warning.",
92                    "warning:".yellow().bold(),
93                );
94            }
95        }
96
97        let absolute_manifest_path = manifest_path.absolute_directory()?;
98        let absolute_workspace_root = metadata.workspace_root.canonicalize()?;
99        if absolute_manifest_path != absolute_workspace_root {
100            // If the contract is a package in a workspace, we use the package name
101            // as the name of the sub-folder where we put the `.contract` bundle.
102            target_directory = target_directory.join(contract_artifact_name.clone());
103        }
104
105        // {target_dir}/{target}/release/{contract_artifact_name}.{extension}
106        let mut original_code = target_directory.clone();
107        original_code.push(target.llvm_target());
108        original_code.push("release");
109        original_code.push(root_package.name.clone());
110        original_code.set_extension(target.source_extension());
111
112        // {target_dir}/{contract_artifact_name}.code
113        let mut dest_code = target_directory.clone();
114        dest_code.push(contract_artifact_name.clone());
115        dest_code.set_extension(target.dest_extension());
116
117        let ink_version = metadata
118            .packages
119            .iter()
120            .find_map(|package| {
121                if package.name == "ink" || package.name == "ink_lang" {
122                    Some(
123                        Version::parse(&package.version.to_string())
124                            .expect("Invalid ink crate version string"),
125                    )
126                } else {
127                    None
128                }
129            })
130            .ok_or_else(|| anyhow::anyhow!("No 'ink' dependency found"))?;
131
132        let ExtraMetadata {
133            documentation,
134            homepage,
135            user,
136        } = get_cargo_toml_metadata(manifest_path)?;
137
138        let crate_metadata = CrateMetadata {
139            manifest_path: manifest_path.clone(),
140            cargo_meta: metadata,
141            root_package,
142            contract_artifact_name,
143            original_code: original_code.into(),
144            dest_code: dest_code.into(),
145            ink_version,
146            documentation,
147            homepage,
148            user,
149            target_file_path: target_directory.join(".target").into(),
150            target_directory: target_directory.into(),
151        };
152        Ok(crate_metadata)
153    }
154
155    /// Get the path of the contract metadata file
156    pub fn metadata_path(&self) -> PathBuf {
157        let metadata_file = format!("{}.json", self.contract_artifact_name);
158        self.target_directory.join(metadata_file)
159    }
160
161    /// Get the path of the contract bundle, containing metadata + code.
162    pub fn contract_bundle_path(&self) -> PathBuf {
163        let target_directory = self.target_directory.clone();
164        let fname_bundle = format!("{}.contract", self.contract_artifact_name);
165        target_directory.join(fname_bundle)
166    }
167}
168
169/// Get the result of `cargo metadata`, together with the root package id.
170fn get_cargo_metadata(manifest_path: &ManifestPath) -> Result<(CargoMetadata, Package)> {
171    tracing::debug!(
172        "Fetching cargo metadata for {}",
173        manifest_path.as_ref().to_string_lossy()
174    );
175    let mut cmd = MetadataCommand::new();
176    let metadata = cmd
177        .manifest_path(manifest_path.as_ref())
178        .exec()
179        .with_context(|| {
180            format!(
181                "Error invoking `cargo metadata` for {}",
182                manifest_path.as_ref().display()
183            )
184        })?;
185    let root_package_id = metadata
186        .resolve
187        .as_ref()
188        .and_then(|resolve| resolve.root.as_ref())
189        .context("Cannot infer the root project id")?
190        .clone();
191    // Find the root package by id in the list of packages. It is logical error if the
192    // root package is not found in the list.
193    let root_package = metadata
194        .packages
195        .iter()
196        .find(|package| package.id == root_package_id)
197        .expect("The package is not found in the `cargo metadata` output")
198        .clone();
199    Ok((metadata, root_package))
200}
201
202/// Extra metadata not available via `cargo metadata`.
203struct ExtraMetadata {
204    documentation: Option<Url>,
205    homepage: Option<Url>,
206    user: Option<Map<String, Value>>,
207}
208
209/// Read extra metadata not available via `cargo metadata` directly from `Cargo.toml`
210fn get_cargo_toml_metadata(manifest_path: &ManifestPath) -> Result<ExtraMetadata> {
211    let toml = fs::read_to_string(manifest_path)?;
212    let toml: value::Table = toml::from_str(&toml)?;
213
214    let get_url = |field_name| -> Result<Option<Url>> {
215        toml.get("package")
216            .ok_or_else(|| anyhow::anyhow!("package section not found"))?
217            .get(field_name)
218            .and_then(|v| v.as_str())
219            .map(Url::parse)
220            .transpose()
221            .context(format!("{field_name} should be a valid URL"))
222            .map_err(Into::into)
223    };
224
225    let documentation = get_url("documentation")?;
226    let homepage = get_url("homepage")?;
227
228    let user = toml
229        .get("package")
230        .and_then(|v| v.get("metadata"))
231        .and_then(|v| v.get("contract"))
232        .and_then(|v| v.get("user"))
233        .and_then(|v| v.as_table())
234        .map(|v| {
235            // convert user defined section from toml to json
236            serde_json::to_string(v).and_then(|json| serde_json::from_str(&json))
237        })
238        .transpose()?;
239
240    Ok(ExtraMetadata {
241        documentation,
242        homepage,
243        user,
244    })
245}