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    Abi,
19    ManifestPath,
20    Target,
21};
22use anyhow::{
23    Context,
24    Result,
25};
26use cargo_metadata::{
27    Metadata as CargoMetadata,
28    MetadataCommand,
29    Package,
30    TargetKind,
31};
32use semver::Version;
33use serde_json::{
34    Map,
35    Value,
36};
37use std::{
38    fs,
39    path::PathBuf,
40};
41use toml::value;
42use url::Url;
43
44/// Relevant metadata obtained from `Cargo.toml`.
45#[derive(Debug)]
46pub struct CrateMetadata {
47    pub manifest_path: ManifestPath,
48    pub cargo_meta: cargo_metadata::Metadata,
49    pub contract_artifact_name: String,
50    pub root_package: Package,
51    pub original_code: PathBuf,
52    pub dest_binary: PathBuf,
53    pub ink_version: Version,
54    pub abi: Option<Abi>,
55    pub documentation: Option<Url>,
56    pub homepage: Option<Url>,
57    pub user: Option<Map<String, Value>>,
58    /// Directory for intermediate build artifacts.
59    ///
60    /// Analog to `--target-dir` for cargo.
61    pub target_directory: PathBuf,
62    /// Directory for final build artifacts.
63    ///
64    /// Analog to the unstable `--artifact-dir` for cargo.
65    ///
66    /// Ref: <https://doc.rust-lang.org/cargo/commands/cargo-build.html#output-options>
67    pub artifact_directory: PathBuf,
68    pub target_file_path: PathBuf,
69    pub metadata_spec_path: PathBuf,
70}
71
72impl CrateMetadata {
73    /// Attempt to construct [`CrateMetadata`] from the given manifest path.
74    pub fn from_manifest_path(manifest_path: Option<&PathBuf>) -> Result<Self> {
75        let manifest_path = ManifestPath::try_from(manifest_path)?;
76        Self::collect(&manifest_path)
77    }
78
79    /// Parses the contract manifest and returns relevant metadata.
80    pub fn collect(manifest_path: &ManifestPath) -> Result<Self> {
81        Self::collect_with_target_dir(manifest_path, None)
82    }
83
84    /// Parses the contract manifest and returns relevant metadata.
85    pub fn collect_with_target_dir(
86        manifest_path: &ManifestPath,
87        target_dir: Option<PathBuf>,
88    ) -> Result<Self> {
89        let (metadata, root_package) = get_cargo_metadata(manifest_path)?;
90        let mut target_directory = target_dir
91            .as_deref()
92            .unwrap_or_else(|| metadata.target_directory.as_std_path())
93            .join("ink");
94
95        // Normalize the final contract artifact name.
96        let contract_artifact_name = root_package.name.replace('-', "_");
97
98        // Retrieves ABI from package metadata (if specified).
99        let abi = package_abi(&root_package).transpose()?;
100
101        if let Some(lib_name) = &root_package
102            .targets
103            .iter()
104            .find(|target| target.kind.contains(&TargetKind::Lib))
105        {
106            // Warn user if they still specify a lib name different from the
107            // package name.
108            // NOTE: If no lib name is specified, cargo "normalizes" the package name
109            // and auto inserts it as the lib name. So we need to normalize the package
110            // name before making the comparison.
111            // Ref: <https://github.com/rust-lang/cargo/blob/3c5bb555caf3fad02927fcfd790ee525da17ce5a/src/cargo/util/toml/targets.rs#L177-L178>
112            let expected_lib_name = root_package.name.replace("-", "_");
113            if lib_name.name != expected_lib_name {
114                use colored::Colorize;
115                eprintln!(
116                    "{} the `name` field in the `[lib]` section of the `Cargo.toml`, \
117                    is no longer used for the name of generated contract artifacts. \
118                    The package name is used instead. Remove the `[lib] name` to \
119                    stop this warning.",
120                    "warning:".yellow().bold(),
121                );
122            }
123        }
124
125        let absolute_manifest_path = manifest_path.absolute_directory()?;
126        let absolute_workspace_root = metadata.workspace_root.canonicalize()?;
127        // Allows the final build artifacts (e.g. contract binary, metadata e.t.c) to
128        // be placed in a separate directory from the "target" directory used for
129        // intermediate build artifacts. This is also similar to `cargo`'s
130        // currently unstable `--artifact-dir`, but it's only used internally
131        // (at the moment).
132        // Ref: <https://doc.rust-lang.org/cargo/commands/cargo-build.html#output-options>
133        let mut artifact_directory = target_directory.clone();
134        if absolute_manifest_path != absolute_workspace_root {
135            // If the contract is a package in a workspace, we use the package name
136            // as the name of the sub-folder where we put the `.contract` bundle.
137            artifact_directory = artifact_directory.join(contract_artifact_name.clone());
138        }
139
140        // Adds ABI sub-folders to target directory for intermediate build artifacts.
141        // This is necessary because the ABI is passed as a `cfg` flag,
142        // and this ensures that `cargo` will recompile all packages (including proc
143        // macros) for current ABI (similar to how it handles profiles and target
144        // triples).
145        target_directory.push("abi");
146        target_directory.push(abi.unwrap_or_default().as_ref());
147
148        // {target_dir}/{target}/release/{contract_artifact_name}.{extension}
149        let mut original_code = target_directory.clone();
150        original_code.push(Target::llvm_target_alias());
151        original_code.push("release");
152        original_code.push(root_package.name.as_str());
153        original_code.set_extension(Target::source_extension());
154
155        // {target_dir}/{contract_artifact_name}.code
156        let mut dest_code = artifact_directory.clone();
157        dest_code.push(contract_artifact_name.clone());
158        dest_code.set_extension(Target::dest_extension());
159
160        let ink_version = metadata
161            .packages
162            .iter()
163            .find_map(|package| {
164                if package.name.as_str() == "ink" || package.name.as_str() == "ink_lang" {
165                    Some(
166                        Version::parse(&package.version.to_string())
167                            .expect("Invalid ink crate version string"),
168                    )
169                } else {
170                    None
171                }
172            })
173            .ok_or_else(|| anyhow::anyhow!("No 'ink' dependency found"))?;
174
175        let ExtraMetadata {
176            documentation,
177            homepage,
178            user,
179        } = get_cargo_toml_metadata(manifest_path)?;
180
181        let crate_metadata = CrateMetadata {
182            manifest_path: manifest_path.clone(),
183            cargo_meta: metadata,
184            root_package,
185            contract_artifact_name,
186            original_code,
187            dest_binary: dest_code,
188            ink_version,
189            abi,
190            documentation,
191            homepage,
192            user,
193            target_file_path: artifact_directory.join(".target"),
194            metadata_spec_path: artifact_directory.join(".metadata_spec"),
195            target_directory,
196            artifact_directory,
197        };
198        Ok(crate_metadata)
199    }
200
201    /// Get the path of the contract metadata file
202    pub fn metadata_path(&self) -> PathBuf {
203        let metadata_file = format!("{}.json", self.contract_artifact_name);
204        self.artifact_directory.join(metadata_file)
205    }
206
207    /// Get the path of the contract bundle, containing metadata + code.
208    pub fn contract_bundle_path(&self) -> PathBuf {
209        let artifact_directory = self.artifact_directory.clone();
210        let fname_bundle = format!("{}.contract", self.contract_artifact_name);
211        artifact_directory.join(fname_bundle)
212    }
213}
214
215/// Get the result of `cargo metadata`, together with the root package id.
216fn get_cargo_metadata(manifest_path: &ManifestPath) -> Result<(CargoMetadata, Package)> {
217    tracing::debug!(
218        "Fetching cargo metadata for {}",
219        manifest_path.as_ref().to_string_lossy()
220    );
221    let mut cmd = MetadataCommand::new();
222    let metadata = cmd
223        .manifest_path(manifest_path.as_ref())
224        .exec()
225        .with_context(|| {
226            format!(
227                "Error invoking `cargo metadata` for {}",
228                manifest_path.as_ref().display()
229            )
230        })?;
231    let root_package_id = metadata
232        .resolve
233        .as_ref()
234        .and_then(|resolve| resolve.root.as_ref())
235        .context("Cannot infer the root project id")?
236        .clone();
237    // Find the root package by id in the list of packages. It is logical error if the
238    // root package is not found in the list.
239    let root_package = metadata
240        .packages
241        .iter()
242        .find(|package| package.id == root_package_id)
243        .expect("The package is not found in the `cargo metadata` output")
244        .clone();
245    Ok((metadata, root_package))
246}
247
248/// Extra metadata not available via `cargo metadata`.
249struct ExtraMetadata {
250    documentation: Option<Url>,
251    homepage: Option<Url>,
252    user: Option<Map<String, Value>>,
253}
254
255/// Read extra metadata not available via `cargo metadata` directly from `Cargo.toml`
256fn get_cargo_toml_metadata(manifest_path: &ManifestPath) -> Result<ExtraMetadata> {
257    let toml = fs::read_to_string(manifest_path)?;
258    let toml: value::Table = toml::from_str(&toml)?;
259
260    let get_url = |field_name| -> Result<Option<Url>> {
261        toml.get("package")
262            .ok_or_else(|| anyhow::anyhow!("package section not found"))?
263            .get(field_name)
264            .and_then(|v| v.as_str())
265            .map(Url::parse)
266            .transpose()
267            .context(format!("{field_name} should be a valid URL"))
268    };
269
270    let documentation = get_url("documentation")?;
271    let homepage = get_url("homepage")?;
272
273    let user = toml
274        .get("package")
275        .and_then(|v| v.get("metadata"))
276        .and_then(|v| v.get("contract"))
277        .and_then(|v| v.get("user"))
278        .and_then(|v| v.as_table())
279        .map(|v| {
280            // convert user defined section from toml to json
281            serde_json::to_string(v).and_then(|json| serde_json::from_str(&json))
282        })
283        .transpose()?;
284
285    Ok(ExtraMetadata {
286        documentation,
287        homepage,
288        user,
289    })
290}
291
292/// Returns ABI specified (if any) for the package (i.e. via
293/// `package.metadata.ink-lang.abi`).
294fn package_abi(package: &Package) -> Option<Result<Abi>> {
295    let abi_str = package.metadata.get("ink-lang")?.get("abi")?.as_str()?;
296    let abi = match abi_str {
297        "ink" => Abi::Ink,
298        "sol" => Abi::Solidity,
299        "all" => Abi::All,
300        _ => return Some(Err(anyhow::anyhow!("Unknown ABI: {abi_str}"))),
301    };
302
303    Some(Ok(abi))
304}
305
306#[cfg(test)]
307mod tests {
308    use std::fs;
309
310    use super::{
311        get_cargo_metadata,
312        package_abi,
313    };
314    use crate::{
315        Abi,
316        ManifestPath,
317        new_contract_project,
318        util::tests::with_tmp_dir,
319    };
320
321    #[test]
322    fn valid_package_abi_works() {
323        fn test_project_with_abi(abi: Abi) {
324            with_tmp_dir(|path| {
325                let name = "project_with_valid_abi";
326                let dir = path.join(name);
327                fs::create_dir_all(&dir).unwrap();
328                let result = new_contract_project(name, Some(path), Some(abi));
329                assert!(result.is_ok(), "Should succeed");
330
331                let manifest_path = ManifestPath::new(dir.join("Cargo.toml")).unwrap();
332                let (_, root_package) = get_cargo_metadata(&manifest_path).unwrap();
333                let parsed_abi = package_abi(&root_package)
334                    .expect("Expected an ABI declaration")
335                    .expect("Expected a valid ABI");
336                assert_eq!(parsed_abi, abi);
337
338                Ok(())
339            });
340        }
341
342        test_project_with_abi(Abi::Ink);
343        test_project_with_abi(Abi::Solidity);
344        test_project_with_abi(Abi::All);
345    }
346
347    #[test]
348    fn missing_package_abi_works() {
349        with_tmp_dir(|path| {
350            let name = "project_with_no_abi";
351            let dir = path.join(name);
352            fs::create_dir_all(&dir).unwrap();
353            let result = new_contract_project(name, Some(path), None);
354            assert!(result.is_ok(), "Should succeed");
355
356            let cargo_toml = dir.join("Cargo.toml");
357            let mut manifest_content = fs::read_to_string(&cargo_toml).unwrap();
358            manifest_content = manifest_content
359                .replace("[package.metadata.ink-lang]\nabi = \"ink\"", "");
360            let result = fs::write(&cargo_toml, manifest_content);
361            assert!(result.is_ok(), "Should succeed");
362
363            let manifest_path = ManifestPath::new(cargo_toml).unwrap();
364            let (_, root_package) = get_cargo_metadata(&manifest_path).unwrap();
365            let parsed_abi = package_abi(&root_package);
366            assert!(parsed_abi.is_none(), "Should be None");
367
368            Ok(())
369        });
370    }
371
372    #[test]
373    fn invalid_package_abi_fails() {
374        with_tmp_dir(|path| {
375            let name = "project_with_invalid_abi";
376            let dir = path.join(name);
377            fs::create_dir_all(&dir).unwrap();
378            let result = new_contract_project(name, Some(path), None);
379            assert!(result.is_ok(), "Should succeed");
380
381            let cargo_toml = dir.join("Cargo.toml");
382            let mut manifest_content = fs::read_to_string(&cargo_toml).unwrap();
383            manifest_content =
384                manifest_content.replace("abi = \"ink\"", "abi = \"move\"");
385            let result = fs::write(&cargo_toml, manifest_content);
386            assert!(result.is_ok(), "Should succeed");
387
388            let manifest_path = ManifestPath::new(cargo_toml).unwrap();
389            let (_, root_package) = get_cargo_metadata(&manifest_path).unwrap();
390            let parsed_abi =
391                package_abi(&root_package).expect("Expected an ABI declaration");
392            assert!(parsed_abi.is_err(), "Should be Err");
393            assert!(parsed_abi.unwrap_err().to_string().contains("Unknown ABI"));
394
395            Ok(())
396        });
397    }
398}