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