1use crate::manifest::{
2 ContractDependency, Dependency, DependencyDetails, GenericManifestFile, HexSalt,
3};
4use crate::source::IPFSNode;
5use crate::{self as pkg, Lock, PackageManifestFile};
6use anyhow::{anyhow, bail, Result};
7use pkg::manifest::ManifestFile;
8use std::collections::BTreeMap;
9use std::fmt;
10use std::path::Path;
11use std::path::PathBuf;
12use std::str::FromStr;
13use sway_core::fuel_prelude::fuel_tx;
14use toml_edit::{DocumentMut, InlineTable, Item, Table, Value};
15use tracing::info;
16
17#[derive(Clone, Debug, Default)]
18pub enum Action {
19 #[default]
20 Add,
21 Remove,
22}
23
24#[derive(Clone, Debug, Default)]
25pub struct ModifyOpts {
26 pub manifest_path: Option<String>,
28 pub package: Option<String>,
30 pub source_path: Option<String>,
32 pub git: Option<String>,
33 pub branch: Option<String>,
34 pub tag: Option<String>,
35 pub rev: Option<String>,
36 pub ipfs: Option<String>,
37 pub contract_deps: bool,
39 pub salt: Option<String>,
40 pub ipfs_node: Option<IPFSNode>,
42 pub dependencies: Vec<String>,
44 pub dry_run: bool,
45 pub offline: bool,
46 pub action: Action,
47}
48
49pub fn modify_dependencies(opts: ModifyOpts) -> Result<()> {
50 let manifest_file = if let Some(p) = &opts.manifest_path {
51 let path = &PathBuf::from(p);
52 ManifestFile::from_file(path)?
53 } else {
54 let cwd = std::env::current_dir()?;
55 ManifestFile::from_dir(cwd)?
56 };
57
58 let root_dir = manifest_file.root_dir();
59 let member_manifests = manifest_file.member_manifests()?;
60
61 let package_manifest_dir =
62 resolve_package_path(&manifest_file, &opts.package, &root_dir, &member_manifests)?;
63
64 let content = std::fs::read_to_string(&package_manifest_dir)?;
65 let mut toml_doc = content.parse::<DocumentMut>()?;
66 let backup_doc = toml_doc.clone();
67
68 let old_package_manifest = PackageManifestFile::from_file(&package_manifest_dir)?;
69 let lock_path = old_package_manifest.lock_path()?;
70 let old_lock = Lock::from_path(&lock_path).ok().unwrap_or_default();
71
72 let section = if opts.contract_deps {
73 Section::ContractDeps
74 } else {
75 Section::Deps
76 };
77
78 match opts.action {
79 Action::Add => {
80 for dependency in &opts.dependencies {
81 let (dep_name, dependency_data) = resolve_dependency(
82 dependency,
83 &opts,
84 &member_manifests,
85 &old_package_manifest.dir().to_path_buf(),
86 )?;
87
88 section.add_deps_manifest_table(
89 &mut toml_doc,
90 dep_name,
91 dependency_data,
92 opts.salt.clone(),
93 )?;
94 }
95 }
96 Action::Remove => {
97 let dep_refs: Vec<&str> = opts.dependencies.iter().map(String::as_str).collect();
98
99 section.remove_deps_manifest_table(&mut toml_doc, &dep_refs)?;
100 }
101 }
102
103 std::fs::write(&package_manifest_dir, toml_doc.to_string())?;
105
106 let updated_package_manifest = PackageManifestFile::from_file(&package_manifest_dir)?;
107
108 let member_manifests = updated_package_manifest.member_manifests()?;
109
110 let new_plan = pkg::BuildPlan::from_lock_and_manifests(
111 &lock_path,
112 &member_manifests,
113 false,
114 opts.offline,
115 &opts.ipfs_node.clone().unwrap_or_default(),
116 );
117
118 new_plan.or_else(|e| {
119 std::fs::write(&package_manifest_dir, backup_doc.to_string())
120 .map_err(|write_err| anyhow!("failed to write toml file: {}", write_err))?;
121 Err(e)
122 })?;
123
124 if opts.dry_run {
125 info!("Dry run enabled. toml file not modified.");
126 std::fs::write(&package_manifest_dir, backup_doc.to_string())?;
127
128 let string = toml::ser::to_string_pretty(&old_lock)?;
129 std::fs::write(&lock_path, string)?;
130
131 return Ok(());
132 }
133
134 Ok(())
135}
136
137fn resolve_package_path(
138 manifest_file: &ManifestFile,
139 package: &Option<String>,
140 root_dir: &Path,
141 member_manifests: &BTreeMap<String, PackageManifestFile>,
142) -> Result<PathBuf> {
143 if manifest_file.is_workspace() {
144 let Some(package_name) = package else {
145 let packages = member_manifests
146 .keys()
147 .cloned()
148 .collect::<Vec<_>>()
149 .join(", ");
150 bail!("`forc add` could not determine which package to modify. Use --package.\nAvailable: {}", packages);
151 };
152
153 resolve_workspace_path_inner(member_manifests, package_name, root_dir)
154 } else if let Some(package_name) = package {
155 resolve_workspace_path_inner(member_manifests, package_name, root_dir)
156 } else {
157 Ok(manifest_file.path().to_path_buf())
158 }
159}
160
161fn resolve_workspace_path_inner(
162 member_manifests: &BTreeMap<String, PackageManifestFile>,
163 package_name: &str,
164 root_dir: &Path,
165) -> Result<PathBuf> {
166 if let Some(dir) = member_manifests.get(package_name) {
167 Ok(dir.path().to_path_buf())
168 } else {
169 bail!(
170 "package(s) {} not found in workspace {}",
171 package_name,
172 root_dir.to_string_lossy()
173 )
174 }
175}
176
177fn resolve_dependency(
178 raw: &str,
179 opts: &ModifyOpts,
180 member_manifests: &BTreeMap<String, PackageManifestFile>,
181 package_dir: &PathBuf,
182) -> Result<(String, Dependency)> {
183 let dep_spec: DepSpec = raw.parse()?;
184 let dep_name = dep_spec.name;
185
186 let mut details = DependencyDetails {
187 version: dep_spec.version_req.clone(),
188 namespace: None,
189 path: opts.source_path.clone(),
190 git: opts.git.clone(),
191 branch: opts.branch.clone(),
192 tag: opts.tag.clone(),
193 package: None,
194 rev: opts.rev.clone(),
195 ipfs: opts.ipfs.clone(),
196 };
197
198 details.validate()?;
199
200 let dependency_data = if let Some(version) = dep_spec.version_req {
201 Dependency::Simple(version)
202 } else if details.is_source_empty() {
203 if let Some(member) = member_manifests.get(&dep_name) {
204 if member.dir() == package_dir {
205 bail!("cannot add `{}` as a dependency to itself", dep_name);
206 }
207
208 let sibling_parent = package_dir.parent().unwrap();
209 let rel_path = member
210 .dir()
211 .strip_prefix(sibling_parent)
212 .map(|p| PathBuf::from("..").join(p))
213 .unwrap_or_else(|_| member.dir().to_path_buf());
214
215 details.path = Some(rel_path.to_string_lossy().to_string());
216 Dependency::Detailed(details)
217 } else {
218 bail!(
221 "dependency `{}` source not specified. Please specify a source (e.g., git, path) or version.",
222 dep_name
223 );
224 }
225 } else {
226 Dependency::Detailed(details)
227 };
228
229 Ok((dep_name, dependency_data))
230}
231
232#[derive(Clone, Debug, Default)]
236pub struct DepSpec {
237 pub name: String,
238 pub version_req: Option<String>,
239}
240
241impl FromStr for DepSpec {
242 type Err = anyhow::Error;
243
244 fn from_str(s: &str) -> anyhow::Result<Self> {
245 if s.trim().is_empty() {
246 bail!("Dependency spec cannot be empty");
247 }
248
249 let mut s = s.trim().split('@');
250
251 let name = s
252 .next()
253 .ok_or_else(|| anyhow::anyhow!("missing dependency name"))?;
254
255 let version_req = s.next().map(|s| s.to_string());
256
257 if let Some(ref v) = version_req {
258 semver::VersionReq::parse(v)
259 .map_err(|_| anyhow::anyhow!("invalid version requirement `{v}`"))?;
260 }
261
262 Ok(Self {
263 name: name.to_string(),
264 version_req,
265 })
266 }
267}
268
269impl fmt::Display for DepSpec {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 match &self.version_req {
272 Some(version) => write!(f, "{}@{}", self.name, version),
273 None => write!(f, "{}", self.name),
274 }
275 }
276}
277
278#[derive(Clone)]
279pub enum Section {
280 Deps,
281 ContractDeps,
282}
283
284impl fmt::Display for Section {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 let section = match self {
287 Section::Deps => "dependencies",
288 Section::ContractDeps => "contract-dependencies",
289 };
290 write!(f, "{section}")
291 }
292}
293
294impl Section {
295 pub fn add_deps_manifest_table(
296 &self,
297 doc: &mut DocumentMut,
298 dep_name: String,
299 dep_data: Dependency,
300 salt: Option<String>,
301 ) -> Result<()> {
302 let section_name = self.to_string();
303
304 if !doc.as_table().contains_key(§ion_name) {
305 doc[§ion_name] = Item::Table(Table::new());
306 }
307
308 let table = doc[section_name.as_str()].as_table_mut().unwrap();
309
310 match self {
311 Section::Deps => {
312 let item = match dep_data {
313 Dependency::Simple(ver) => ver.to_string().into(),
314 Dependency::Detailed(details) => {
315 Item::Value(toml_edit::Value::InlineTable(generate_table(&details)))
316 }
317 };
318 table.insert(&dep_name, item);
319 }
320 Section::ContractDeps => {
321 let resolved_salt = match salt.as_ref().or(salt.as_ref()) {
322 Some(s) => {
323 HexSalt::from_str(s).map_err(|e| anyhow!("Invalid salt format: {}", e))?
324 }
325 None => HexSalt(fuel_tx::Salt::default()),
326 };
327 let contract_dep = ContractDependency {
328 dependency: dep_data,
329 salt: resolved_salt.clone(),
330 };
331
332 let dep = &contract_dep.dependency;
333 let salt: &HexSalt = &contract_dep.salt;
334 let item = match dep {
335 Dependency::Simple(ver) => {
336 let mut inline = InlineTable::default();
337 inline.insert("version", Value::from(ver.to_string()));
338 inline.insert("salt", Value::from(format!("0x{salt}")));
339 Item::Value(toml_edit::Value::InlineTable(inline))
340 }
341 Dependency::Detailed(details) => {
342 let mut inline = generate_table(details);
343 inline.insert("salt", Value::from(format!("0x{salt}")));
344 Item::Value(toml_edit::Value::InlineTable(inline))
345 }
346 };
347 table.insert(&dep_name, item);
348 }
349 };
350
351 Ok(())
352 }
353
354 pub fn remove_deps_manifest_table(self, doc: &mut DocumentMut, deps: &[&str]) -> Result<()> {
355 let section_name = self.to_string();
356
357 let section_table = doc[section_name.as_str()].as_table_mut().ok_or_else(|| {
358 anyhow!(
359 "the dependency `{}` could not be found in `{}`",
360 deps.join(", "),
361 section_name,
362 )
363 })?;
364
365 match self {
366 Section::Deps => {
367 for dep in deps {
368 if !section_table.contains_key(dep) {
369 bail!(
370 "the dependency `{}` could not be found in `{}`",
371 dep,
372 section_name
373 );
374 }
375 section_table.remove(dep);
376 }
377 }
378 Section::ContractDeps => {
379 for dep in deps {
380 if !section_table.contains_key(dep) {
381 bail!(
382 "the dependency `{}` could not be found in `{}`",
383 dep,
384 section_name
385 );
386 }
387 section_table.remove(dep);
388 }
389 }
390 }
391 Ok(())
392 }
393}
394
395fn generate_table(details: &DependencyDetails) -> InlineTable {
396 let mut inline = InlineTable::default();
397
398 if let Some(version) = &details.version {
399 inline.insert("version", Value::from(version.to_string()));
400 }
401 if let Some(git) = &details.git {
402 inline.insert("git", Value::from(git.to_string()));
403 }
404 if let Some(branch) = &details.branch {
405 inline.insert("branch", Value::from(branch.to_string()));
406 }
407 if let Some(tag) = &details.tag {
408 inline.insert("tag", Value::from(tag.to_string()));
409 }
410 if let Some(rev) = &details.rev {
411 inline.insert("rev", Value::from(rev.to_string()));
412 }
413 if let Some(path) = &details.path {
414 inline.insert("path", Value::from(path.to_string()));
415 }
416 if let Some(ipfs) = &details.ipfs {
417 inline.insert("cid", Value::from(ipfs.to_string()));
418 }
419
420 inline
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use crate::WorkspaceManifestFile;
427 use std::fs;
428 use std::str::FromStr;
429 use tempfile::{tempdir, TempDir};
430
431 fn create_test_package(
432 name: &str,
433 source_files: Vec<(&str, &str)>,
434 ) -> Result<(TempDir, PackageManifestFile)> {
435 let temp_dir = tempdir()?;
436 let base_path = temp_dir.path();
437
438 fs::create_dir_all(base_path.join("src"))?;
440
441 let forc_toml = format!(
443 r#"
444 [project]
445 authors = ["Test"]
446 entry = "main.sw"
447 license = "MIT"
448 name = "{name}"
449
450 [dependencies]
451 "#
452 );
453 fs::write(base_path.join("Forc.toml"), forc_toml)?;
454
455 for (file_name, content) in source_files {
457 let file_path = base_path.join("src").join(file_name);
459 if let Some(parent) = file_path.parent() {
460 fs::create_dir_all(parent)?;
461 }
462 fs::write(file_path, content)?;
463 }
464
465 let manifest_file = PackageManifestFile::from_file(base_path.join("Forc.toml"))?;
467
468 Ok((temp_dir, manifest_file))
469 }
470
471 fn create_test_workspace(
472 members: Vec<(&str, Vec<(&str, &str)>)>,
473 ) -> Result<(TempDir, WorkspaceManifestFile)> {
474 let temp_dir = tempdir()?;
475 let base_path = temp_dir.path();
476
477 let mut workspace_toml = "[workspace]\nmembers = [".to_string();
479
480 for (i, (name, _)) in members.iter().enumerate() {
481 if i > 0 {
482 workspace_toml.push_str(", ");
483 }
484 workspace_toml.push_str(&format!("\"{name}\""));
485 }
486 workspace_toml.push_str("]\n");
487
488 fs::write(base_path.join("Forc.toml"), workspace_toml)?;
489
490 for (name, source_files) in members {
492 let member_path = base_path.join(name);
493 fs::create_dir_all(member_path.join("src"))?;
494
495 let forc_toml = format!(
497 r#"
498 [project]
499 authors = ["Test"]
500 entry = "main.sw"
501 license = "MIT"
502 name = "{name}"
503
504 [dependencies]
505 "#
506 );
507 fs::write(member_path.join("Forc.toml"), forc_toml)?;
508
509 for (file_name, content) in source_files {
511 let file_path = member_path.join("src").join(file_name);
513 if let Some(parent) = file_path.parent() {
514 fs::create_dir_all(parent)?;
515 }
516 fs::write(file_path, content)?;
517 }
518 }
519
520 let manifest_file = WorkspaceManifestFile::from_file(base_path.join("Forc.toml"))?;
522
523 Ok((temp_dir, manifest_file))
524 }
525
526 #[test]
527 fn test_dep_from_str_name_only() {
528 let dep: DepSpec = "abc".parse().expect("parsing dep spec failed");
529 assert_eq!(dep.name, "abc".to_string());
530 assert_eq!(dep.version_req, None);
531 }
532
533 #[test]
534 fn test_dep_from_str_name_and_version() {
535 let dep: DepSpec = "abc@1".parse().expect("parsing dep spec failed");
536 assert_eq!(dep.name, "abc".to_string());
537 assert_eq!(dep.version_req, Some("1".to_string()));
538 }
539
540 #[test]
541 fn test_dep_spec_invalid_version_req() {
542 let input = "foo@not-a-version";
543 let result = DepSpec::from_str(input);
544
545 assert!(result.is_err());
546 assert!(
547 result
548 .unwrap_err()
549 .to_string()
550 .contains("invalid version requirement"),
551 "Expected version requirement parse failure"
552 );
553 }
554
555 #[test]
556 fn test_dep_from_str_invalid() {
557 assert!(DepSpec::from_str("").is_err());
558 }
559
560 #[test]
561 fn test_resolve_package_path_single_package_mode() {
562 let (temp_dir, pkg_manifest) =
563 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
564
565 let package_spec_dir = temp_dir.path().to_path_buf();
566 let expected_path = pkg_manifest.path;
567
568 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
569
570 let members = manifest_file.member_manifests().unwrap();
571 let root_dir = manifest_file.root_dir();
572 let result = resolve_package_path(&manifest_file, &None, &root_dir, &members).unwrap();
573
574 assert_eq!(result, expected_path);
575 }
576
577 #[test]
578 fn test_resolve_package_path_workspace_with_package_found() {
579 let (temp_dir, _) = create_test_workspace(vec![
580 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
581 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
582 ])
583 .unwrap();
584
585 let base_path = temp_dir.path();
586
587 let expected_path = base_path.join("pkg1/Forc.toml");
588
589 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
590 let members = manifest_file.member_manifests().unwrap();
591 let root_dir = manifest_file.root_dir();
592
593 let package = "pkg1".to_string();
594 let result =
595 resolve_package_path(&manifest_file, &Some(package), &root_dir, &members).unwrap();
596
597 assert_eq!(result, expected_path);
598 }
599
600 #[test]
601 fn test_resolve_package_path_workspace_package_not_found() {
602 let (temp_dir, _) = create_test_workspace(vec![
603 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
604 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
605 ])
606 .unwrap();
607
608 let base_path = temp_dir.path();
609
610 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
611 let members = manifest_file.member_manifests().unwrap();
612 let root_dir = manifest_file.root_dir();
613
614 let err = resolve_package_path(
615 &manifest_file,
616 &Some("missing_pkg".into()),
617 &root_dir,
618 &members,
619 )
620 .unwrap_err();
621
622 assert!(
623 err.to_string().contains("package(s) missing_pkg not found"),
624 "unexpected error: {err}"
625 );
626 }
627
628 #[test]
629 fn test_resolve_package_path_workspace_package_not_set() {
630 let (temp_dir, _) = create_test_workspace(vec![
631 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
632 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
633 ])
634 .unwrap();
635
636 let base_path = temp_dir.path();
637
638 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
639 let members = manifest_file.member_manifests().unwrap();
640 let root_dir = manifest_file.root_dir();
641
642 let err = resolve_package_path(&manifest_file, &None, &root_dir, &members).unwrap_err();
643
644 let resp = "`forc add` could not determine which package to modify. Use --package.\nAvailable: pkg1, pkg2".to_string();
645 assert!(err.to_string().contains(&resp), "unexpected error: {err}");
646 }
647
648 #[test]
649 fn test_resolve_dependency_simple_version() {
650 let opts = ModifyOpts {
651 dependencies: vec!["dep@1.0.0".to_string()],
652 ..Default::default()
653 };
654
655 let (temp_dir, _) =
656 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
657
658 let package_spec_dir = temp_dir.path().to_path_buf();
659
660 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
661 let members = manifest_file.member_manifests().unwrap();
662
663 let (name, data) =
664 resolve_dependency("dep@1.0.0", &opts, &members, &package_spec_dir).unwrap();
665
666 assert_eq!(name, "dep");
667 match data {
668 Dependency::Simple(v) => assert_eq!(v, "1.0.0"),
669 _ => panic!("Expected simple dependency"),
670 }
671 }
672
673 #[test]
674 fn test_resolve_dependency_detailed_variants() {
675 let base_opts = ModifyOpts {
676 ..Default::default()
677 };
678
679 let (temp_dir, _) =
680 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
681
682 let package_spec_dir = temp_dir.path().to_path_buf();
683
684 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
685 let members = manifest_file.member_manifests().unwrap();
686 let dep = "dummy_dep";
687 let git = "https://github.com/example/repo.git";
688
689 {
691 let mut opts = base_opts.clone();
692 opts.git = Some(git.to_string());
693
694 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
695 assert_eq!(name, dep);
696 match data {
697 Dependency::Detailed(details) => {
698 assert_eq!(details.git.as_deref(), Some(git));
699 }
700 _ => panic!("Expected detailed dependency with git"),
701 }
702 }
703
704 {
706 let mut opts = base_opts.clone();
707 opts.git = Some(git.to_string());
708 opts.branch = Some("main".to_string());
709
710 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
711 assert_eq!(name, dep);
712 match data {
713 Dependency::Detailed(details) => {
714 assert_eq!(details.git.as_deref(), Some(git));
715 assert_eq!(details.branch.as_deref(), Some("main"));
716 }
717 _ => panic!("Expected detailed dependency with git+branch"),
718 }
719 }
720
721 {
723 let mut opts = base_opts.clone();
724 opts.git = Some(git.to_string());
725 opts.rev = Some("deadbeef".to_string());
726
727 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
728 assert_eq!(name, dep);
729 match data {
730 Dependency::Detailed(details) => {
731 assert_eq!(details.git.as_deref(), Some(git));
732 assert_eq!(details.rev.as_deref(), Some("deadbeef"));
733 }
734 _ => panic!("Expected detailed dependency with git+rev"),
735 }
736 }
737
738 {
740 let mut opts = base_opts.clone();
741 opts.git = Some(git.to_string());
742 opts.tag = Some("v1.2.3".to_string());
743
744 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
745 assert_eq!(name, dep);
746 match data {
747 Dependency::Detailed(details) => {
748 assert_eq!(details.git.as_deref(), Some(git));
749 assert_eq!(details.tag.as_deref(), Some("v1.2.3"));
750 }
751 _ => panic!("Expected detailed dependency with git+tag"),
752 }
753 }
754
755 {
757 let mut opts = base_opts.clone();
758 opts.ipfs = Some("QmYwAPJzv5CZsnA".to_string());
759
760 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
761 assert_eq!(name, dep);
762 match data {
763 Dependency::Detailed(details) => {
764 assert_eq!(details.ipfs.as_deref(), Some("QmYwAPJzv5CZsnA"));
765 }
766 _ => panic!("Expected detailed dependency with git+tag"),
767 }
768 }
769 }
770
771 #[test]
772 fn test_resolve_dependency_detailed_variant_failure() {
773 let base_opts = ModifyOpts {
774 ..Default::default()
775 };
776
777 let (temp_dir, _) =
778 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
779
780 let package_spec_dir = temp_dir.path().to_path_buf();
781 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
782 let members = manifest_file.member_manifests().unwrap();
783 let dep = "dummy_dep";
784 let git = "https://github.com/example/repo.git";
785
786 {
788 let mut opts = base_opts.clone();
789 opts.branch = Some("main".to_string());
790 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
791 assert!(result.is_err());
792 assert!(result
793 .unwrap_err()
794 .to_string()
795 .contains("Details reserved for git sources used without a git field"));
796 }
797
798 {
800 let mut opts = base_opts.clone();
801 opts.rev = Some("deadbeef".to_string());
802
803 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
804 assert!(result.is_err());
805 assert!(result
806 .unwrap_err()
807 .to_string()
808 .contains("Details reserved for git sources used without a git field"));
809 }
810
811 {
813 let mut opts = base_opts.clone();
814 opts.tag = Some("v1.2.3".to_string());
815
816 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
817 assert!(result.is_err());
818 assert!(result
819 .unwrap_err()
820 .to_string()
821 .contains("Details reserved for git sources used without a git field"));
822 }
823
824 {
826 let mut opts = base_opts.clone();
827 opts.git = Some(git.to_string());
828 opts.tag = Some("v1.2.3".to_string());
829 opts.rev = Some("deadbeef".to_string());
830 opts.branch = Some("main".to_string());
831
832 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
833 assert!(result.is_err());
834 assert!(result
835 .unwrap_err()
836 .to_string()
837 .contains("Cannot specify `branch`, `tag`, and `rev` together for dependency with a Git source"));
838 }
839
840 {
842 let mut opts = base_opts.clone();
843 opts.git = Some(git.to_string());
844 opts.tag = Some("v1.2.3".to_string());
845 opts.branch = Some("main".to_string());
846
847 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
848 assert!(result.is_err());
849 assert!(result.unwrap_err().to_string().contains(
850 "Cannot specify both `branch` and `tag` for dependency with a Git source"
851 ));
852 }
853
854 {
856 let mut opts = base_opts.clone();
857 opts.git = Some(git.to_string());
858 opts.tag = Some("v1.2.3".to_string());
859 opts.rev = Some("deadbeef".to_string());
860
861 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
862 assert!(result.is_err());
863 assert!(result
864 .unwrap_err()
865 .to_string()
866 .contains("Cannot specify both `rev` and `tag` for dependency with a Git source"));
867 }
868
869 {
871 let mut opts = base_opts.clone();
872 opts.git = Some(git.to_string());
873 opts.rev = Some("deadbeef".to_string());
874 opts.branch = Some("main".to_string());
875
876 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
877 assert!(result.is_err());
878 assert!(result.unwrap_err().to_string().contains(
879 "Cannot specify both `branch` and `rev` for dependency with a Git source"
880 ));
881 }
882
883 {
885 let opts = base_opts.clone();
886 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
887
888 assert!(result.is_err());
889 assert!(result.unwrap_err().to_string().contains(
890 "dependency `dummy_dep` source not specified. Please specify a source (e.g., git, path) or version"
891 ));
892 }
893 }
894
895 #[test]
896 fn test_resolve_dependency_from_workspace_sibling() {
897 let (temp_dir, _) = create_test_workspace(vec![
898 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
899 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
900 ])
901 .unwrap();
902
903 let base_path = temp_dir.path();
904 let package_dir = base_path.join("pkg2");
905
906 let dep = "pkg1";
907
908 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
909 let members = manifest_file.member_manifests().unwrap();
910
911 let opts = ModifyOpts {
912 source_path: None,
913 dependencies: vec![dep.to_string()],
914 package: Some("pkg2".to_string()),
915 ..Default::default()
916 };
917
918 let (name, data) =
919 resolve_dependency(dep, &opts, &members, &package_dir).expect("should resolve");
920
921 assert_eq!(name, dep);
922 match data {
923 Dependency::Detailed(details) => {
924 assert!(details.path.is_some());
925 let actual_path = details.path.as_ref().unwrap();
926 assert_eq!(actual_path, "../pkg1");
927 }
928 _ => panic!("Expected detailed dependency with fallback path"),
929 }
930 }
931
932 #[test]
933 fn test_resolve_dependency_self_dependency_error() {
934 let (temp_dir, _) = create_test_workspace(vec![
935 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
936 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
937 ])
938 .unwrap();
939
940 let base_path = temp_dir.path();
941 let package_dir = base_path.join("pkg1");
942 let dep = "pkg1";
943 let resp = format!("cannot add `{dep}` as a dependency to itself");
944
945 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
946 let members = manifest_file.member_manifests().unwrap();
947
948 let opts = ModifyOpts {
949 dependencies: vec![dep.to_string()],
950 package: Some("package-1".to_string()),
951 ..Default::default()
952 };
953
954 let error = resolve_dependency(dep, &opts, &members, &package_dir).unwrap_err();
955 assert!(error.to_string().contains(&resp));
956 }
957
958 #[test]
959 fn test_resolve_dependency_invalid_string() {
960 let opts = ModifyOpts {
961 dependencies: vec!["".to_string()],
962 ..Default::default()
963 };
964
965 let result = resolve_dependency("", &opts, &BTreeMap::new(), &PathBuf::new());
966 assert!(result.is_err());
967 assert!(result
968 .unwrap_err()
969 .to_string()
970 .contains("Dependency spec cannot be empty"));
971 }
972
973 #[test]
974 fn test_dep_section_add_to_toml_regular_dependency_success() {
975 let toml_str = r#"
976 [project]
977 name = "package"
978 entry = "main.sw"
979 license = "Apache-2.0"
980 authors = ["Fuel Labs"]
981 "#;
982 let mut doc: DocumentMut = toml_str.parse().unwrap();
983
984 let dep_data = Dependency::Simple("1.0.0".into());
985
986 let section = Section::Deps;
987
988 section
989 .add_deps_manifest_table(&mut doc, "dep1".into(), dep_data, None)
990 .unwrap();
991
992 assert_eq!(doc["dependencies"]["dep1"].as_str(), Some("1.0.0"));
993 }
994
995 #[test]
996 fn test_dep_section_add_to_toml_regular_detailed_dependency_success() {
997 let toml_str = r#"
998 [project]
999 name = "package"
1000 entry = "main.sw"
1001 license = "Apache-2.0"
1002 authors = ["Fuel Labs"]
1003 "#;
1004 let mut doc: DocumentMut = toml_str.parse().unwrap();
1005
1006 let dep_data = Dependency::Detailed(DependencyDetails {
1007 git: Some("https://github.com/example/repo".to_string()),
1008 tag: Some("v1.2.3".to_string()),
1009 ..Default::default()
1010 });
1011
1012 let section = Section::Deps;
1013
1014 section
1015 .add_deps_manifest_table(&mut doc, "dep2".into(), dep_data, None)
1016 .unwrap();
1017
1018 let table = doc["dependencies"]["dep2"].as_inline_table().unwrap();
1019 assert_eq!(
1020 table.get("git").unwrap().as_str(),
1021 Some("https://github.com/example/repo")
1022 );
1023 assert_eq!(table.get("tag").unwrap().as_str(), Some("v1.2.3"));
1024 }
1025
1026 #[test]
1027 fn test_dep_section_add_contract_dependency_with_salt() {
1028 let toml_str = r#"
1029 [project]
1030 name = "contract_pkg"
1031 entry = "main.sw"
1032 license = "Apache-2.0"
1033 authors = ["Fuel Labs"]
1034 "#;
1035
1036 let mut doc: DocumentMut = toml_str.parse().unwrap();
1037
1038 let section = Section::ContractDeps;
1039 let dep_name = "custom_dep";
1040 let dep_data = Dependency::Simple("1.0.0".to_string());
1041 let salt_str = "0x2222222222222222222222222222222222222222222222222222222222222222";
1042 let hex_salt = HexSalt::from_str(salt_str).unwrap();
1043
1044 section
1045 .add_deps_manifest_table(
1046 &mut doc,
1047 dep_name.to_string(),
1048 dep_data,
1049 Some(salt_str.to_string()),
1050 )
1051 .unwrap();
1052
1053 let contract_table = doc["contract-dependencies"][dep_name]
1054 .as_inline_table()
1055 .expect("inline table not found");
1056
1057 assert_eq!(
1058 contract_table.get("version").unwrap().as_str(),
1059 Some("1.0.0")
1060 );
1061 assert_eq!(
1062 contract_table.get("salt").unwrap().as_str(),
1063 Some(format!("0x{hex_salt}").as_str())
1064 );
1065 }
1066
1067 #[test]
1068 fn test_dep_section_add_contract_dependency_with_default_salt() {
1069 let toml_str = r#"
1070 [project]
1071 name = "contract_pkg"
1072 entry = "main.sw"
1073 license = "Apache-2.0"
1074 authors = ["Fuel Labs"]
1075 "#;
1076
1077 let mut doc: DocumentMut = toml_str.parse().unwrap();
1078
1079 let section = Section::ContractDeps;
1080 let dep_name = "custom_dep";
1081 let dep_data = Dependency::Simple("1.0.0".to_string());
1082
1083 section
1084 .add_deps_manifest_table(&mut doc, dep_name.to_string(), dep_data, None)
1085 .unwrap();
1086
1087 let contract_table = doc["contract-dependencies"][dep_name]
1088 .as_inline_table()
1089 .expect("inline table not found");
1090
1091 assert_eq!(
1092 contract_table.get("version").unwrap().as_str(),
1093 Some("1.0.0")
1094 );
1095 assert_eq!(
1096 contract_table.get("salt").unwrap().as_str(),
1097 Some(format!("0x{}", fuel_tx::Salt::default()).as_str())
1098 );
1099 }
1100
1101 #[test]
1102 fn test_dep_section_add_contract_dependency_with_invalid_salt() {
1103 let toml_str = r#"
1104 [project]
1105 name = "contract_pkg"
1106 entry = "main.sw"
1107 license = "Apache-2.0"
1108 authors = ["Fuel Labs"]
1109 "#;
1110
1111 let mut doc: DocumentMut = toml_str.parse().unwrap();
1112
1113 let section = Section::ContractDeps;
1114 let dep_name = "custom_dep";
1115 let dep_data = Dependency::Simple("1.0.0".to_string());
1116
1117 let result = section.add_deps_manifest_table(
1118 &mut doc,
1119 dep_name.to_string(),
1120 dep_data,
1121 Some("not_hex".to_string()),
1122 );
1123
1124 assert!(result.is_err());
1125 assert!(format!("{}", result.unwrap_err()).contains("Invalid salt format"));
1126 }
1127
1128 #[test]
1129 fn test_dep_section_remove_regular_dependency_success() {
1130 let toml_str = r#"
1131 [project]
1132 name = "package"
1133 entry = "main.sw"
1134 license = "Apache-2.0"
1135 authors = ["Fuel Labs"]
1136
1137 [dependencies]
1138 foo = "1.0.0"
1139 bar = "2.0.0"
1140 "#;
1141
1142 let mut doc: DocumentMut = toml_str.parse().unwrap();
1143
1144 let section = Section::Deps;
1145 section
1146 .remove_deps_manifest_table(&mut doc, &["foo"])
1147 .unwrap();
1148
1149 assert!(doc["dependencies"].as_table().unwrap().get("foo").is_none());
1150 assert!(doc["dependencies"].as_table().unwrap().get("bar").is_some());
1151 }
1152
1153 #[test]
1154 fn test_dep_section_remove_regular_dependency_not_found() {
1155 let toml_str = r#"
1156 [project]
1157 name = "package"
1158 entry = "main.sw"
1159 license = "Apache-2.0"
1160 authors = ["Fuel Labs"]
1161
1162 [dependencies]
1163 bar = "2.0.0"
1164 "#;
1165
1166 let mut doc: DocumentMut = toml_str.parse().unwrap();
1167
1168 let section = Section::Deps;
1169
1170 let err = section
1171 .remove_deps_manifest_table(&mut doc, &["notfound"])
1172 .unwrap_err()
1173 .to_string();
1174
1175 assert!(err.contains("the dependency `notfound` could not be found in `dependencies`"));
1176 }
1177
1178 #[test]
1179 fn test_dep_section_remove_contract_dependency_success() {
1180 let toml_str = r#"
1181 [project]
1182 name = "package"
1183 entry = "main.sw"
1184 license = "Apache-2.0"
1185 authors = ["Fuel Labs"]
1186
1187 [contract-dependencies]
1188 baz = { path = "../baz", salt = "0x1111111111111111111111111111111111111111111111111111111111111111" }
1189 "#;
1190
1191 let mut doc: DocumentMut = toml_str.parse().unwrap();
1192
1193 let section = Section::ContractDeps;
1194 section
1195 .remove_deps_manifest_table(&mut doc, &["baz"])
1196 .unwrap();
1197
1198 assert!(doc["contract-dependencies"]
1199 .as_table()
1200 .unwrap()
1201 .get("baz")
1202 .is_none());
1203 }
1204
1205 #[test]
1206 fn test_dep_section_remove_contract_dependency_not_found() {
1207 let toml_str = r#"
1208 [project]
1209 name = "package"
1210 entry = "main.sw"
1211 license = "Apache-2.0"
1212 authors = ["Fuel Labs"]
1213
1214 [contract-dependencies]
1215 baz = { path = "../baz", salt = "0x1111111111111111111111111111111111111111111111111111111111111111" }
1216 "#;
1217
1218 let mut doc: DocumentMut = toml_str.parse().unwrap();
1219
1220 let section = Section::ContractDeps;
1221
1222 let result = section.remove_deps_manifest_table(&mut doc, &["ghost"]);
1223 assert!(result.is_err());
1224 assert!(result
1225 .unwrap_err()
1226 .to_string()
1227 .contains("the dependency `ghost` could not be found in `contract-dependencies`"));
1228 }
1229
1230 #[test]
1231 fn test_dep_section_remove_from_missing_section() {
1232 let toml_str = r#"
1233 [project]
1234 authors = ["Fuel Labs <contact@fuel.sh>"]
1235 entry = "main.sw"
1236 license = "Apache-2.0"
1237 name = "package-1"
1238
1239 [dependencies]
1240 foo = "1.0.0"
1241 "#;
1242
1243 let mut doc: DocumentMut = toml_str.parse().unwrap();
1244
1245 let section = Section::ContractDeps;
1246
1247 let result = section.remove_deps_manifest_table(&mut doc, &["ghost"]);
1248
1249 assert!(result.is_err());
1250 assert!(result
1251 .unwrap_err()
1252 .to_string()
1253 .contains("the dependency `ghost` could not be found in `contract-dependencies`"));
1254 }
1255
1256 #[test]
1257 fn test_generate_table_basic_fields() {
1258 let details = DependencyDetails {
1259 version: Some("1.2.3".to_string()),
1260 git: Some("https://github.com/example/repo".to_string()),
1261 branch: Some("main".to_string()),
1262 tag: Some("v1.0.0".to_string()),
1263 rev: Some("deadbeef".to_string()),
1264 path: Some("./lib".to_string()),
1265 ipfs: Some("QmYw...".to_string()),
1266 namespace: None,
1267 package: None,
1268 };
1269
1270 let table = generate_table(&details);
1271
1272 assert_eq!(table.get("version").unwrap().as_str(), Some("1.2.3"));
1273 assert_eq!(
1274 table.get("git").unwrap().as_str(),
1275 Some("https://github.com/example/repo")
1276 );
1277 assert_eq!(table.get("branch").unwrap().as_str(), Some("main"));
1278 assert_eq!(table.get("tag").unwrap().as_str(), Some("v1.0.0"));
1279 assert_eq!(table.get("rev").unwrap().as_str(), Some("deadbeef"));
1280 assert_eq!(table.get("path").unwrap().as_str(), Some("./lib"));
1281 assert_eq!(table.get("cid").unwrap().as_str(), Some("QmYw..."));
1282 }
1283}