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 = "{}"
449
450 [dependencies]
451 "#,
452 name
453 );
454 fs::write(base_path.join("Forc.toml"), forc_toml)?;
455
456 for (file_name, content) in source_files {
458 let file_path = base_path.join("src").join(file_name);
460 if let Some(parent) = file_path.parent() {
461 fs::create_dir_all(parent)?;
462 }
463 fs::write(file_path, content)?;
464 }
465
466 let manifest_file = PackageManifestFile::from_file(base_path.join("Forc.toml"))?;
468
469 Ok((temp_dir, manifest_file))
470 }
471
472 fn create_test_workspace(
473 members: Vec<(&str, Vec<(&str, &str)>)>,
474 ) -> Result<(TempDir, WorkspaceManifestFile)> {
475 let temp_dir = tempdir()?;
476 let base_path = temp_dir.path();
477
478 let mut workspace_toml = "[workspace]\nmembers = [".to_string();
480
481 for (i, (name, _)) in members.iter().enumerate() {
482 if i > 0 {
483 workspace_toml.push_str(", ");
484 }
485 workspace_toml.push_str(&format!("\"{name}\""));
486 }
487 workspace_toml.push_str("]\n");
488
489 fs::write(base_path.join("Forc.toml"), workspace_toml)?;
490
491 for (name, source_files) in members {
493 let member_path = base_path.join(name);
494 fs::create_dir_all(member_path.join("src"))?;
495
496 let forc_toml = format!(
498 r#"
499 [project]
500 authors = ["Test"]
501 entry = "main.sw"
502 license = "MIT"
503 name = "{}"
504
505 [dependencies]
506 "#,
507 name
508 );
509 fs::write(member_path.join("Forc.toml"), forc_toml)?;
510
511 for (file_name, content) in source_files {
513 let file_path = member_path.join("src").join(file_name);
515 if let Some(parent) = file_path.parent() {
516 fs::create_dir_all(parent)?;
517 }
518 fs::write(file_path, content)?;
519 }
520 }
521
522 let manifest_file = WorkspaceManifestFile::from_file(base_path.join("Forc.toml"))?;
524
525 Ok((temp_dir, manifest_file))
526 }
527
528 #[test]
529 fn test_dep_from_str_name_only() {
530 let dep: DepSpec = "abc".parse().expect("parsing dep spec failed");
531 assert_eq!(dep.name, "abc".to_string());
532 assert_eq!(dep.version_req, None);
533 }
534
535 #[test]
536 fn test_dep_from_str_name_and_version() {
537 let dep: DepSpec = "abc@1".parse().expect("parsing dep spec failed");
538 assert_eq!(dep.name, "abc".to_string());
539 assert_eq!(dep.version_req, Some("1".to_string()));
540 }
541
542 #[test]
543 fn test_dep_spec_invalid_version_req() {
544 let input = "foo@not-a-version";
545 let result = DepSpec::from_str(input);
546
547 assert!(result.is_err());
548 assert!(
549 result
550 .unwrap_err()
551 .to_string()
552 .contains("invalid version requirement"),
553 "Expected version requirement parse failure"
554 );
555 }
556
557 #[test]
558 fn test_dep_from_str_invalid() {
559 assert!(DepSpec::from_str("").is_err());
560 }
561
562 #[test]
563 fn test_resolve_package_path_single_package_mode() {
564 let (temp_dir, pkg_manifest) =
565 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
566
567 let package_spec_dir = temp_dir.path().to_path_buf();
568 let expected_path = pkg_manifest.path;
569
570 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
571
572 let members = manifest_file.member_manifests().unwrap();
573 let root_dir = manifest_file.root_dir();
574 let result = resolve_package_path(&manifest_file, &None, &root_dir, &members).unwrap();
575
576 assert_eq!(result, expected_path);
577 }
578
579 #[test]
580 fn test_resolve_package_path_workspace_with_package_found() {
581 let (temp_dir, _) = create_test_workspace(vec![
582 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
583 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
584 ])
585 .unwrap();
586
587 let base_path = temp_dir.path();
588
589 let expected_path = base_path.join("pkg1/Forc.toml");
590
591 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
592 let members = manifest_file.member_manifests().unwrap();
593 let root_dir = manifest_file.root_dir();
594
595 let package = "pkg1".to_string();
596 let result =
597 resolve_package_path(&manifest_file, &Some(package), &root_dir, &members).unwrap();
598
599 assert_eq!(result, expected_path);
600 }
601
602 #[test]
603 fn test_resolve_package_path_workspace_package_not_found() {
604 let (temp_dir, _) = create_test_workspace(vec![
605 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
606 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
607 ])
608 .unwrap();
609
610 let base_path = temp_dir.path();
611
612 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
613 let members = manifest_file.member_manifests().unwrap();
614 let root_dir = manifest_file.root_dir();
615
616 let err = resolve_package_path(
617 &manifest_file,
618 &Some("missing_pkg".into()),
619 &root_dir,
620 &members,
621 )
622 .unwrap_err();
623
624 assert!(
625 err.to_string().contains("package(s) missing_pkg not found"),
626 "unexpected error: {err}"
627 );
628 }
629
630 #[test]
631 fn test_resolve_package_path_workspace_package_not_set() {
632 let (temp_dir, _) = create_test_workspace(vec![
633 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
634 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
635 ])
636 .unwrap();
637
638 let base_path = temp_dir.path();
639
640 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
641 let members = manifest_file.member_manifests().unwrap();
642 let root_dir = manifest_file.root_dir();
643
644 let err = resolve_package_path(&manifest_file, &None, &root_dir, &members).unwrap_err();
645
646 let resp = "`forc add` could not determine which package to modify. Use --package.\nAvailable: pkg1, pkg2".to_string();
647 assert!(err.to_string().contains(&resp), "unexpected error: {err}");
648 }
649
650 #[test]
651 fn test_resolve_dependency_simple_version() {
652 let opts = ModifyOpts {
653 dependencies: vec!["dep@1.0.0".to_string()],
654 ..Default::default()
655 };
656
657 let (temp_dir, _) =
658 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
659
660 let package_spec_dir = temp_dir.path().to_path_buf();
661
662 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
663 let members = manifest_file.member_manifests().unwrap();
664
665 let (name, data) =
666 resolve_dependency("dep@1.0.0", &opts, &members, &package_spec_dir).unwrap();
667
668 assert_eq!(name, "dep");
669 match data {
670 Dependency::Simple(v) => assert_eq!(v, "1.0.0"),
671 _ => panic!("Expected simple dependency"),
672 }
673 }
674
675 #[test]
676 fn test_resolve_dependency_detailed_variants() {
677 let base_opts = ModifyOpts {
678 ..Default::default()
679 };
680
681 let (temp_dir, _) =
682 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
683
684 let package_spec_dir = temp_dir.path().to_path_buf();
685
686 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
687 let members = manifest_file.member_manifests().unwrap();
688 let dep = "dummy_dep";
689 let git = "https://github.com/example/repo.git";
690
691 {
693 let mut opts = base_opts.clone();
694 opts.git = Some(git.to_string());
695
696 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
697 assert_eq!(name, dep);
698 match data {
699 Dependency::Detailed(details) => {
700 assert_eq!(details.git.as_deref(), Some(git));
701 }
702 _ => panic!("Expected detailed dependency with git"),
703 }
704 }
705
706 {
708 let mut opts = base_opts.clone();
709 opts.git = Some(git.to_string());
710 opts.branch = Some("main".to_string());
711
712 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
713 assert_eq!(name, dep);
714 match data {
715 Dependency::Detailed(details) => {
716 assert_eq!(details.git.as_deref(), Some(git));
717 assert_eq!(details.branch.as_deref(), Some("main"));
718 }
719 _ => panic!("Expected detailed dependency with git+branch"),
720 }
721 }
722
723 {
725 let mut opts = base_opts.clone();
726 opts.git = Some(git.to_string());
727 opts.rev = Some("deadbeef".to_string());
728
729 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
730 assert_eq!(name, dep);
731 match data {
732 Dependency::Detailed(details) => {
733 assert_eq!(details.git.as_deref(), Some(git));
734 assert_eq!(details.rev.as_deref(), Some("deadbeef"));
735 }
736 _ => panic!("Expected detailed dependency with git+rev"),
737 }
738 }
739
740 {
742 let mut opts = base_opts.clone();
743 opts.git = Some(git.to_string());
744 opts.tag = Some("v1.2.3".to_string());
745
746 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
747 assert_eq!(name, dep);
748 match data {
749 Dependency::Detailed(details) => {
750 assert_eq!(details.git.as_deref(), Some(git));
751 assert_eq!(details.tag.as_deref(), Some("v1.2.3"));
752 }
753 _ => panic!("Expected detailed dependency with git+tag"),
754 }
755 }
756
757 {
759 let mut opts = base_opts.clone();
760 opts.ipfs = Some("QmYwAPJzv5CZsnA".to_string());
761
762 let (name, data) = resolve_dependency(dep, &opts, &members, &package_spec_dir).unwrap();
763 assert_eq!(name, dep);
764 match data {
765 Dependency::Detailed(details) => {
766 assert_eq!(details.ipfs.as_deref(), Some("QmYwAPJzv5CZsnA"));
767 }
768 _ => panic!("Expected detailed dependency with git+tag"),
769 }
770 }
771 }
772
773 #[test]
774 fn test_resolve_dependency_detailed_variant_failure() {
775 let base_opts = ModifyOpts {
776 ..Default::default()
777 };
778
779 let (temp_dir, _) =
780 create_test_package("test_pkg", vec![("main.sw", "fn main() -> u64 { 42 }")]).unwrap();
781
782 let package_spec_dir = temp_dir.path().to_path_buf();
783 let manifest_file = ManifestFile::from_dir(&package_spec_dir).unwrap();
784 let members = manifest_file.member_manifests().unwrap();
785 let dep = "dummy_dep";
786 let git = "https://github.com/example/repo.git";
787
788 {
790 let mut opts = base_opts.clone();
791 opts.branch = Some("main".to_string());
792 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
793 assert!(result.is_err());
794 assert!(result
795 .unwrap_err()
796 .to_string()
797 .contains("Details reserved for git sources used without a git field"));
798 }
799
800 {
802 let mut opts = base_opts.clone();
803 opts.rev = Some("deadbeef".to_string());
804
805 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
806 assert!(result.is_err());
807 assert!(result
808 .unwrap_err()
809 .to_string()
810 .contains("Details reserved for git sources used without a git field"));
811 }
812
813 {
815 let mut opts = base_opts.clone();
816 opts.tag = Some("v1.2.3".to_string());
817
818 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
819 assert!(result.is_err());
820 assert!(result
821 .unwrap_err()
822 .to_string()
823 .contains("Details reserved for git sources used without a git field"));
824 }
825
826 {
828 let mut opts = base_opts.clone();
829 opts.git = Some(git.to_string());
830 opts.tag = Some("v1.2.3".to_string());
831 opts.rev = Some("deadbeef".to_string());
832 opts.branch = Some("main".to_string());
833
834 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
835 assert!(result.is_err());
836 assert!(result
837 .unwrap_err()
838 .to_string()
839 .contains("Cannot specify `branch`, `tag`, and `rev` together for dependency with a Git source"));
840 }
841
842 {
844 let mut opts = base_opts.clone();
845 opts.git = Some(git.to_string());
846 opts.tag = Some("v1.2.3".to_string());
847 opts.branch = Some("main".to_string());
848
849 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
850 assert!(result.is_err());
851 assert!(result.unwrap_err().to_string().contains(
852 "Cannot specify both `branch` and `tag` for dependency with a Git source"
853 ));
854 }
855
856 {
858 let mut opts = base_opts.clone();
859 opts.git = Some(git.to_string());
860 opts.tag = Some("v1.2.3".to_string());
861 opts.rev = Some("deadbeef".to_string());
862
863 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
864 assert!(result.is_err());
865 assert!(result
866 .unwrap_err()
867 .to_string()
868 .contains("Cannot specify both `rev` and `tag` for dependency with a Git source"));
869 }
870
871 {
873 let mut opts = base_opts.clone();
874 opts.git = Some(git.to_string());
875 opts.rev = Some("deadbeef".to_string());
876 opts.branch = Some("main".to_string());
877
878 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
879 assert!(result.is_err());
880 assert!(result.unwrap_err().to_string().contains(
881 "Cannot specify both `branch` and `rev` for dependency with a Git source"
882 ));
883 }
884
885 {
887 let opts = base_opts.clone();
888 let result = resolve_dependency(dep, &opts, &members, &package_spec_dir);
889
890 assert!(result.is_err());
891 assert!(result.unwrap_err().to_string().contains(
892 "dependency `dummy_dep` source not specified. Please specify a source (e.g., git, path) or version"
893 ));
894 }
895 }
896
897 #[test]
898 fn test_resolve_dependency_from_workspace_sibling() {
899 let (temp_dir, _) = create_test_workspace(vec![
900 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
901 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
902 ])
903 .unwrap();
904
905 let base_path = temp_dir.path();
906 let package_dir = base_path.join("pkg2");
907
908 let dep = "pkg1";
909
910 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
911 let members = manifest_file.member_manifests().unwrap();
912
913 let opts = ModifyOpts {
914 source_path: None,
915 dependencies: vec![dep.to_string()],
916 package: Some("pkg2".to_string()),
917 ..Default::default()
918 };
919
920 let (name, data) =
921 resolve_dependency(dep, &opts, &members, &package_dir).expect("should resolve");
922
923 assert_eq!(name, dep);
924 match data {
925 Dependency::Detailed(details) => {
926 assert!(details.path.is_some());
927 let actual_path = details.path.as_ref().unwrap();
928 assert_eq!(actual_path, "../pkg1");
929 }
930 _ => panic!("Expected detailed dependency with fallback path"),
931 }
932 }
933
934 #[test]
935 fn test_resolve_dependency_self_dependency_error() {
936 let (temp_dir, _) = create_test_workspace(vec![
937 ("pkg1", vec![("main.sw", "fn main() -> u64 { 1 }")]),
938 ("pkg2", vec![("main.sw", "fn main() -> u64 { 2 }")]),
939 ])
940 .unwrap();
941
942 let base_path = temp_dir.path();
943 let package_dir = base_path.join("pkg1");
944 let dep = "pkg1";
945 let resp = format!("cannot add `{}` as a dependency to itself", dep);
946
947 let manifest_file = ManifestFile::from_dir(base_path).unwrap();
948 let members = manifest_file.member_manifests().unwrap();
949
950 let opts = ModifyOpts {
951 dependencies: vec![dep.to_string()],
952 package: Some("package-1".to_string()),
953 ..Default::default()
954 };
955
956 let error = resolve_dependency(dep, &opts, &members, &package_dir).unwrap_err();
957 assert!(error.to_string().contains(&resp));
958 }
959
960 #[test]
961 fn test_resolve_dependency_invalid_string() {
962 let opts = ModifyOpts {
963 dependencies: vec!["".to_string()],
964 ..Default::default()
965 };
966
967 let result = resolve_dependency("", &opts, &BTreeMap::new(), &PathBuf::new());
968 assert!(result.is_err());
969 assert!(result
970 .unwrap_err()
971 .to_string()
972 .contains("Dependency spec cannot be empty"));
973 }
974
975 #[test]
976 fn test_dep_section_add_to_toml_regular_dependency_success() {
977 let toml_str = r#"
978 [project]
979 name = "package"
980 entry = "main.sw"
981 license = "Apache-2.0"
982 authors = ["Fuel Labs"]
983 "#;
984 let mut doc: DocumentMut = toml_str.parse().unwrap();
985
986 let dep_data = Dependency::Simple("1.0.0".into());
987
988 let section = Section::Deps;
989
990 section
991 .add_deps_manifest_table(&mut doc, "dep1".into(), dep_data, None)
992 .unwrap();
993
994 assert_eq!(doc["dependencies"]["dep1"].as_str(), Some("1.0.0"));
995 }
996
997 #[test]
998 fn test_dep_section_add_to_toml_regular_detailed_dependency_success() {
999 let toml_str = r#"
1000 [project]
1001 name = "package"
1002 entry = "main.sw"
1003 license = "Apache-2.0"
1004 authors = ["Fuel Labs"]
1005 "#;
1006 let mut doc: DocumentMut = toml_str.parse().unwrap();
1007
1008 let dep_data = Dependency::Detailed(DependencyDetails {
1009 git: Some("https://github.com/example/repo".to_string()),
1010 tag: Some("v1.2.3".to_string()),
1011 ..Default::default()
1012 });
1013
1014 let section = Section::Deps;
1015
1016 section
1017 .add_deps_manifest_table(&mut doc, "dep2".into(), dep_data, None)
1018 .unwrap();
1019
1020 let table = doc["dependencies"]["dep2"].as_inline_table().unwrap();
1021 assert_eq!(
1022 table.get("git").unwrap().as_str(),
1023 Some("https://github.com/example/repo")
1024 );
1025 assert_eq!(table.get("tag").unwrap().as_str(), Some("v1.2.3"));
1026 }
1027
1028 #[test]
1029 fn test_dep_section_add_contract_dependency_with_salt() {
1030 let toml_str = r#"
1031 [project]
1032 name = "contract_pkg"
1033 entry = "main.sw"
1034 license = "Apache-2.0"
1035 authors = ["Fuel Labs"]
1036 "#;
1037
1038 let mut doc: DocumentMut = toml_str.parse().unwrap();
1039
1040 let section = Section::ContractDeps;
1041 let dep_name = "custom_dep";
1042 let dep_data = Dependency::Simple("1.0.0".to_string());
1043 let salt_str = "0x2222222222222222222222222222222222222222222222222222222222222222";
1044 let hex_salt = HexSalt::from_str(salt_str).unwrap();
1045
1046 section
1047 .add_deps_manifest_table(
1048 &mut doc,
1049 dep_name.to_string(),
1050 dep_data,
1051 Some(salt_str.to_string()),
1052 )
1053 .unwrap();
1054
1055 let contract_table = doc["contract-dependencies"][dep_name]
1056 .as_inline_table()
1057 .expect("inline table not found");
1058
1059 assert_eq!(
1060 contract_table.get("version").unwrap().as_str(),
1061 Some("1.0.0")
1062 );
1063 assert_eq!(
1064 contract_table.get("salt").unwrap().as_str(),
1065 Some(format!("0x{}", hex_salt).as_str())
1066 );
1067 }
1068
1069 #[test]
1070 fn test_dep_section_add_contract_dependency_with_default_salt() {
1071 let toml_str = r#"
1072 [project]
1073 name = "contract_pkg"
1074 entry = "main.sw"
1075 license = "Apache-2.0"
1076 authors = ["Fuel Labs"]
1077 "#;
1078
1079 let mut doc: DocumentMut = toml_str.parse().unwrap();
1080
1081 let section = Section::ContractDeps;
1082 let dep_name = "custom_dep";
1083 let dep_data = Dependency::Simple("1.0.0".to_string());
1084
1085 section
1086 .add_deps_manifest_table(&mut doc, dep_name.to_string(), dep_data, None)
1087 .unwrap();
1088
1089 let contract_table = doc["contract-dependencies"][dep_name]
1090 .as_inline_table()
1091 .expect("inline table not found");
1092
1093 assert_eq!(
1094 contract_table.get("version").unwrap().as_str(),
1095 Some("1.0.0")
1096 );
1097 assert_eq!(
1098 contract_table.get("salt").unwrap().as_str(),
1099 Some(format!("0x{}", fuel_tx::Salt::default()).as_str())
1100 );
1101 }
1102
1103 #[test]
1104 fn test_dep_section_add_contract_dependency_with_invalid_salt() {
1105 let toml_str = r#"
1106 [project]
1107 name = "contract_pkg"
1108 entry = "main.sw"
1109 license = "Apache-2.0"
1110 authors = ["Fuel Labs"]
1111 "#;
1112
1113 let mut doc: DocumentMut = toml_str.parse().unwrap();
1114
1115 let section = Section::ContractDeps;
1116 let dep_name = "custom_dep";
1117 let dep_data = Dependency::Simple("1.0.0".to_string());
1118
1119 let result = section.add_deps_manifest_table(
1120 &mut doc,
1121 dep_name.to_string(),
1122 dep_data,
1123 Some("not_hex".to_string()),
1124 );
1125
1126 assert!(result.is_err());
1127 assert!(format!("{}", result.unwrap_err()).contains("Invalid salt format"));
1128 }
1129
1130 #[test]
1131 fn test_dep_section_remove_regular_dependency_success() {
1132 let toml_str = r#"
1133 [project]
1134 name = "package"
1135 entry = "main.sw"
1136 license = "Apache-2.0"
1137 authors = ["Fuel Labs"]
1138
1139 [dependencies]
1140 foo = "1.0.0"
1141 bar = "2.0.0"
1142 "#;
1143
1144 let mut doc: DocumentMut = toml_str.parse().unwrap();
1145
1146 let section = Section::Deps;
1147 section
1148 .remove_deps_manifest_table(&mut doc, &["foo"])
1149 .unwrap();
1150
1151 assert!(doc["dependencies"].as_table().unwrap().get("foo").is_none());
1152 assert!(doc["dependencies"].as_table().unwrap().get("bar").is_some());
1153 }
1154
1155 #[test]
1156 fn test_dep_section_remove_regular_dependency_not_found() {
1157 let toml_str = r#"
1158 [project]
1159 name = "package"
1160 entry = "main.sw"
1161 license = "Apache-2.0"
1162 authors = ["Fuel Labs"]
1163
1164 [dependencies]
1165 bar = "2.0.0"
1166 "#;
1167
1168 let mut doc: DocumentMut = toml_str.parse().unwrap();
1169
1170 let section = Section::Deps;
1171
1172 let err = section
1173 .remove_deps_manifest_table(&mut doc, &["notfound"])
1174 .unwrap_err()
1175 .to_string();
1176
1177 assert!(err.contains("the dependency `notfound` could not be found in `dependencies`"));
1178 }
1179
1180 #[test]
1181 fn test_dep_section_remove_contract_dependency_success() {
1182 let toml_str = r#"
1183 [project]
1184 name = "package"
1185 entry = "main.sw"
1186 license = "Apache-2.0"
1187 authors = ["Fuel Labs"]
1188
1189 [contract-dependencies]
1190 baz = { path = "../baz", salt = "0x1111111111111111111111111111111111111111111111111111111111111111" }
1191 "#;
1192
1193 let mut doc: DocumentMut = toml_str.parse().unwrap();
1194
1195 let section = Section::ContractDeps;
1196 section
1197 .remove_deps_manifest_table(&mut doc, &["baz"])
1198 .unwrap();
1199
1200 assert!(doc["contract-dependencies"]
1201 .as_table()
1202 .unwrap()
1203 .get("baz")
1204 .is_none());
1205 }
1206
1207 #[test]
1208 fn test_dep_section_remove_contract_dependency_not_found() {
1209 let toml_str = r#"
1210 [project]
1211 name = "package"
1212 entry = "main.sw"
1213 license = "Apache-2.0"
1214 authors = ["Fuel Labs"]
1215
1216 [contract-dependencies]
1217 baz = { path = "../baz", salt = "0x1111111111111111111111111111111111111111111111111111111111111111" }
1218 "#;
1219
1220 let mut doc: DocumentMut = toml_str.parse().unwrap();
1221
1222 let section = Section::ContractDeps;
1223
1224 let result = section.remove_deps_manifest_table(&mut doc, &["ghost"]);
1225 assert!(result.is_err());
1226 assert!(result
1227 .unwrap_err()
1228 .to_string()
1229 .contains("the dependency `ghost` could not be found in `contract-dependencies`"));
1230 }
1231
1232 #[test]
1233 fn test_dep_section_remove_from_missing_section() {
1234 let toml_str = r#"
1235 [project]
1236 authors = ["Fuel Labs <contact@fuel.sh>"]
1237 entry = "main.sw"
1238 license = "Apache-2.0"
1239 name = "package-1"
1240
1241 [dependencies]
1242 foo = "1.0.0"
1243 "#;
1244
1245 let mut doc: DocumentMut = toml_str.parse().unwrap();
1246
1247 let section = Section::ContractDeps;
1248
1249 let result = section.remove_deps_manifest_table(&mut doc, &["ghost"]);
1250
1251 assert!(result.is_err());
1252 assert!(result
1253 .unwrap_err()
1254 .to_string()
1255 .contains("the dependency `ghost` could not be found in `contract-dependencies`"));
1256 }
1257
1258 #[test]
1259 fn test_generate_table_basic_fields() {
1260 let details = DependencyDetails {
1261 version: Some("1.2.3".to_string()),
1262 git: Some("https://github.com/example/repo".to_string()),
1263 branch: Some("main".to_string()),
1264 tag: Some("v1.0.0".to_string()),
1265 rev: Some("deadbeef".to_string()),
1266 path: Some("./lib".to_string()),
1267 ipfs: Some("QmYw...".to_string()),
1268 namespace: None,
1269 package: None,
1270 };
1271
1272 let table = generate_table(&details);
1273
1274 assert_eq!(table.get("version").unwrap().as_str(), Some("1.2.3"));
1275 assert_eq!(
1276 table.get("git").unwrap().as_str(),
1277 Some("https://github.com/example/repo")
1278 );
1279 assert_eq!(table.get("branch").unwrap().as_str(), Some("main"));
1280 assert_eq!(table.get("tag").unwrap().as_str(), Some("v1.0.0"));
1281 assert_eq!(table.get("rev").unwrap().as_str(), Some("deadbeef"));
1282 assert_eq!(table.get("path").unwrap().as_str(), Some("./lib"));
1283 assert_eq!(table.get("cid").unwrap().as_str(), Some("QmYw..."));
1284 }
1285}