1use 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#[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 pub target_directory: PathBuf,
62 pub artifact_directory: PathBuf,
68 pub target_file_path: PathBuf,
69 pub metadata_spec_path: PathBuf,
70}
71
72impl CrateMetadata {
73 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 pub fn collect(manifest_path: &ManifestPath) -> Result<Self> {
81 Self::collect_with_target_dir(manifest_path, None)
82 }
83
84 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 let contract_artifact_name = root_package.name.replace('-', "_");
97
98 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 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 let mut artifact_directory = target_directory.clone();
134 if absolute_manifest_path != absolute_workspace_root {
135 artifact_directory = artifact_directory.join(contract_artifact_name.clone());
138 }
139
140 target_directory.push("abi");
146 target_directory.push(abi.unwrap_or_default().as_ref());
147
148 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 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 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 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
215fn 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 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
248struct ExtraMetadata {
250 documentation: Option<Url>,
251 homepage: Option<Url>,
252 user: Option<Map<String, Value>>,
253}
254
255fn 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 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
292fn 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}