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