crate_metadata.rs 6.44 KB
Newer Older
Andrew Jones's avatar
Andrew Jones committed
1
// Copyright 2018-2021 Parity Technologies (UK) Ltd.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This file is part of cargo-contract.
//
// cargo-contract is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// cargo-contract is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.

17
use crate::ManifestPath;
18
19
20
21
22
23
24
25
26
27
28
29
30
use anyhow::{Context, Result};
use cargo_metadata::{Metadata as CargoMetadata, MetadataCommand, Package};
use semver::Version;
use serde_json::{Map, Value};
use std::{fs, path::PathBuf};
use toml::value;
use url::Url;

/// Relevant metadata obtained from Cargo.toml.
#[derive(Debug)]
pub struct CrateMetadata {
    pub manifest_path: ManifestPath,
    pub cargo_meta: cargo_metadata::Metadata,
31
    pub contract_artifact_name: String,
32
33
34
35
36
37
38
    pub root_package: Package,
    pub original_wasm: PathBuf,
    pub dest_wasm: PathBuf,
    pub ink_version: Version,
    pub documentation: Option<Url>,
    pub homepage: Option<Url>,
    pub user: Option<Map<String, Value>>,
39
    pub target_directory: PathBuf,
40
41
42
43
44
45
}

impl CrateMetadata {
    /// Parses the contract manifest and returns relevant metadata.
    pub fn collect(manifest_path: &ManifestPath) -> Result<Self> {
        let (metadata, root_package) = get_cargo_metadata(manifest_path)?;
46
        let mut target_directory = metadata.target_directory.as_path().join("ink");
47

48
        // Normalize the package and lib name.
49
        let package_name = root_package.name.replace("-", "_");
50
51
52
53
54
55
56
        let lib_name = &root_package
            .targets
            .iter()
            .find(|target| target.kind.iter().any(|t| t == "cdylib"))
            .expect("lib name not found")
            .name
            .replace("-", "_");
57

58
59
60
        let absolute_manifest_path = manifest_path.absolute_directory()?;
        let absolute_workspace_root = metadata.workspace_root.canonicalize()?;
        if absolute_manifest_path != absolute_workspace_root {
61
62
63
            // If the contract is a package in a workspace, we use the package name
            // as the name of the sub-folder where we put the `.contract` bundle.
            target_directory = target_directory.join(package_name);
64
65
        }

66
        // {target_dir}/wasm32-unknown-unknown/release/{lib_name}.wasm
67
        let mut original_wasm = target_directory.clone();
68
69
        original_wasm.push("wasm32-unknown-unknown");
        original_wasm.push("release");
70
        original_wasm.push(lib_name.clone());
71
72
        original_wasm.set_extension("wasm");

73
        // {target_dir}/{lib_name}.wasm
74
        let mut dest_wasm = target_directory.clone();
75
        dest_wasm.push(lib_name.clone());
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
        dest_wasm.set_extension("wasm");

        let ink_version = metadata
            .packages
            .iter()
            .find_map(|package| {
                if package.name == "ink_lang" {
                    Some(
                        Version::parse(&package.version.to_string())
                            .expect("Invalid ink_lang version string"),
                    )
                } else {
                    None
                }
            })
honeywest's avatar
honeywest committed
91
            .ok_or_else(|| anyhow::anyhow!("No 'ink_lang' dependency found"))?;
92

93
94
95
96
97
        let ExtraMetadata {
            documentation,
            homepage,
            user,
        } = get_cargo_toml_metadata(manifest_path)?;
98
99
100
101
102

        let crate_metadata = CrateMetadata {
            manifest_path: manifest_path.clone(),
            cargo_meta: metadata,
            root_package,
103
            contract_artifact_name: lib_name.to_string(),
104
105
            original_wasm: original_wasm.into(),
            dest_wasm: dest_wasm.into(),
106
107
108
109
            ink_version,
            documentation,
            homepage,
            user,
110
            target_directory: target_directory.into(),
111
112
113
114
115
116
117
118
119
        };
        Ok(crate_metadata)
    }
}

/// Get the result of `cargo metadata`, together with the root package id.
fn get_cargo_metadata(manifest_path: &ManifestPath) -> Result<(CargoMetadata, Package)> {
    let mut cmd = MetadataCommand::new();
    let metadata = cmd
120
        .manifest_path(manifest_path.as_ref())
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
        .exec()
        .context("Error invoking `cargo metadata`")?;
    let root_package_id = metadata
        .resolve
        .as_ref()
        .and_then(|resolve| resolve.root.as_ref())
        .context("Cannot infer the root project id")?
        .clone();
    // Find the root package by id in the list of packages. It is logical error if the root
    // package is not found in the list.
    let root_package = metadata
        .packages
        .iter()
        .find(|package| package.id == root_package_id)
        .expect("The package is not found in the `cargo metadata` output")
        .clone();
    Ok((metadata, root_package))
}

140
141
142
143
144
145
146
/// Extra metadata not available via `cargo metadata`.
struct ExtraMetadata {
    documentation: Option<Url>,
    homepage: Option<Url>,
    user: Option<Map<String, Value>>,
}

147
/// Read extra metadata not available via `cargo metadata` directly from `Cargo.toml`
148
fn get_cargo_toml_metadata(manifest_path: &ManifestPath) -> Result<ExtraMetadata> {
149
150
151
152
153
    let toml = fs::read_to_string(manifest_path)?;
    let toml: value::Table = toml::from_str(&toml)?;

    let get_url = |field_name| -> Result<Option<Url>> {
        toml.get("package")
honeywest's avatar
honeywest committed
154
            .ok_or_else(|| anyhow::anyhow!("package section not found"))?
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
            .get(field_name)
            .and_then(|v| v.as_str())
            .map(Url::parse)
            .transpose()
            .context(format!("{} should be a valid URL", field_name))
            .map_err(Into::into)
    };

    let documentation = get_url("documentation")?;
    let homepage = get_url("homepage")?;

    let user = toml
        .get("package")
        .and_then(|v| v.get("metadata"))
        .and_then(|v| v.get("contract"))
        .and_then(|v| v.get("user"))
        .and_then(|v| v.as_table())
        .map(|v| {
            // convert user defined section from toml to json
            serde_json::to_string(v).and_then(|json| serde_json::from_str(&json))
        })
        .transpose()?;

178
179
180
181
182
    Ok(ExtraMetadata {
        documentation,
        homepage,
        user,
    })
183
}