1use crate::{
3 download::{find_install_path, find_install_path_sync},
4 errors::ConfigError,
5 lock::SOLDEER_LOCK,
6 remappings::RemappingsLocation,
7};
8use derive_more::derive::{Display, From, FromStr};
9use log::{debug, warn};
10use serde::Deserialize;
11use std::{
12 env, fmt, fs,
13 path::{Path, PathBuf},
14};
15use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, value};
16
17pub type Result<T> = std::result::Result<T, ConfigError>;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Display)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum UrlType {
22 Git(String),
23 Http(String),
24}
25
26impl UrlType {
27 pub fn git(url: impl Into<String>) -> Self {
28 Self::Git(url.into())
29 }
30
31 pub fn http(url: impl Into<String>) -> Self {
32 Self::Http(url.into())
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
58#[non_exhaustive]
60pub struct Paths {
61 pub root: PathBuf,
66
67 pub config: PathBuf,
73
74 pub dependencies: PathBuf,
78
79 pub lock: PathBuf,
83
84 pub remappings: PathBuf,
89}
90
91impl Paths {
92 pub fn new() -> Result<Self> {
99 Self::with_config(None)
100 }
101
102 pub fn with_config(config_location: Option<ConfigLocation>) -> Result<Self> {
114 let root = dunce::canonicalize(Self::get_root_path())?;
115 Self::with_root_and_config(root, config_location)
116 }
117
118 pub fn with_root_and_config(
126 root: impl AsRef<Path>,
127 config_location: Option<ConfigLocation>,
128 ) -> Result<Self> {
129 let root = root.as_ref();
130 let config = Self::get_config_path(root, config_location)?;
131 let dependencies = root.join("dependencies");
132 let lock = root.join(SOLDEER_LOCK);
133 let remappings = root.join("remappings.txt");
134
135 Ok(Self { root: root.to_path_buf(), config, dependencies, lock, remappings })
136 }
137
138 pub fn from_root(root: impl AsRef<Path>) -> Result<Self> {
144 let root = dunce::canonicalize(root.as_ref())?;
145 let config = Self::get_config_path(&root, None)?;
146 let dependencies = root.join("dependencies");
147 let lock = root.join(SOLDEER_LOCK);
148 let remappings = root.join("remappings.txt");
149
150 Ok(Self { root, config, dependencies, lock, remappings })
151 }
152
153 pub fn get_root_path() -> PathBuf {
158 let res = env::var("SOLDEER_PROJECT_ROOT").map_or_else(
159 |_| {
160 debug!("SOLDEER_PROJECT_ROOT not defined, searching for project root");
161 find_project_root(None::<PathBuf>).expect("could not find project root")
162 },
163 |p| {
164 if p.is_empty() {
165 debug!("SOLDEER_PROJECT_ROOT exists but is empty, searching for project root");
166 find_project_root(None::<PathBuf>).expect("could not find project root")
167 } else {
168 debug!(path = p; "root set by SOLDEER_PROJECT_ROOT");
169 PathBuf::from(p)
170 }
171 },
172 );
173 debug!(path:? = res; "found project root");
174 res
175 }
176
177 fn get_config_path(
184 root: impl AsRef<Path>,
185 config_location: Option<ConfigLocation>,
186 ) -> Result<PathBuf> {
187 let foundry_path = root.as_ref().join("foundry.toml");
188 let soldeer_path = root.as_ref().join("soldeer.toml");
189 let location = config_location.or_else(|| {
191 debug!("no preferred config location, trying to detect automatically");
192 detect_config_location(root)
193 }).unwrap_or_else(|| {
194 warn!("config file location could not be determined automatically, using foundry by default");
195 ConfigLocation::Foundry
196 });
197 debug!("using config location {location:?}");
198 create_or_modify_config(location, &foundry_path, &soldeer_path)
199 }
200
201 pub fn foundry_default() -> PathBuf {
203 let root: PathBuf =
204 dunce::canonicalize(Self::get_root_path()).expect("could not get the root");
205 root.join("foundry.toml")
206 }
207
208 pub fn soldeer_default() -> PathBuf {
210 let root: PathBuf =
211 dunce::canonicalize(Self::get_root_path()).expect("could not get the root");
212 root.join("soldeer.toml")
213 }
214}
215
216fn default_true() -> bool {
218 true
219}
220
221#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize))]
224pub struct SoldeerConfig {
225 #[serde(default = "default_true")]
229 pub remappings_generate: bool,
230
231 #[serde(default)]
235 pub remappings_regenerate: bool,
236
237 #[serde(default = "default_true")]
241 pub remappings_version: bool,
242
243 #[serde(default)]
247 pub remappings_prefix: String,
248
249 #[serde(default)]
257 pub remappings_location: RemappingsLocation,
258
259 #[serde(default)]
266 pub recursive_deps: bool,
267}
268
269impl Default for SoldeerConfig {
270 fn default() -> Self {
271 Self {
272 remappings_generate: true,
273 remappings_regenerate: false,
274 remappings_version: true,
275 remappings_prefix: String::new(),
276 remappings_location: RemappingsLocation::default(),
277 recursive_deps: false,
278 }
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Eq, Hash, Display)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
294pub enum GitIdentifier {
295 Rev(String),
297
298 Branch(String),
300
301 Tag(String),
303}
304
305impl GitIdentifier {
306 pub fn from_rev(rev: impl Into<String>) -> Self {
308 let rev: String = rev.into();
309 Self::Rev(rev)
310 }
311
312 pub fn from_branch(branch: impl Into<String>) -> Self {
314 let branch: String = branch.into();
315 Self::Branch(branch)
316 }
317
318 pub fn from_tag(tag: impl Into<String>) -> Self {
320 let tag: String = tag.into();
321 Self::Tag(tag)
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash, bon::Builder)]
329#[allow(clippy::duplicated_attributes)]
330#[builder(on(String, into), on(PathBuf, into))]
331#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
332pub struct GitDependency {
333 pub name: String,
335
336 #[cfg_attr(feature = "serde", serde(rename = "version"))]
343 pub version_req: String,
344
345 pub git: String,
347
348 pub identifier: Option<GitIdentifier>,
352
353 pub project_root: Option<PathBuf>,
358}
359
360impl fmt::Display for GitDependency {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
362 write!(f, "{}~{}", self.name, self.version_req)
363 }
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Hash, bon::Builder)]
370#[allow(clippy::duplicated_attributes)]
371#[builder(on(String, into), on(PathBuf, into))]
372#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
373pub struct HttpDependency {
374 pub name: String,
376
377 #[cfg_attr(feature = "serde", serde(rename = "version"))]
384 pub version_req: String,
385
386 pub url: Option<String>,
391
392 pub project_root: Option<PathBuf>,
397}
398
399impl fmt::Display for HttpDependency {
400 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
401 write!(f, "{}~{}", self.name, self.version_req)
402 }
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, From)]
422#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
423pub enum Dependency {
424 #[from(HttpDependency)]
425 Http(HttpDependency),
426
427 #[from(GitDependency)]
428 Git(GitDependency),
429}
430
431impl Dependency {
432 pub fn from_name_version(
474 name_version: &str,
475 custom_url: Option<UrlType>,
476 identifier: Option<GitIdentifier>,
477 ) -> Result<Self> {
478 let (dependency_name, dependency_version_req) = name_version
479 .split_once('~')
480 .ok_or(ConfigError::InvalidNameAndVersion(name_version.to_string()))?;
481 if dependency_version_req.is_empty() {
482 return Err(ConfigError::EmptyVersion(dependency_name.to_string()));
483 }
484 Ok(match custom_url {
485 Some(url) => {
486 if dependency_version_req.contains('=') {
491 return Err(ConfigError::InvalidVersionReq(dependency_name.to_string()));
492 }
493 debug!(url:% = url; "using custom url");
494 match url {
495 UrlType::Git(url) => GitDependency {
496 name: dependency_name.to_string(),
497 version_req: dependency_version_req.to_string(),
498 git: url,
499 identifier,
500 project_root: None,
501 }
502 .into(),
503 UrlType::Http(url) => HttpDependency {
504 name: dependency_name.to_string(),
505 version_req: dependency_version_req.to_string(),
506 url: Some(url),
507 project_root: None,
508 }
509 .into(),
510 }
511 }
512 None => HttpDependency {
513 name: dependency_name.to_string(),
514 version_req: dependency_version_req.to_string(),
515 url: None,
516 project_root: None,
517 }
518 .into(),
519 })
520 }
521
522 pub fn name(&self) -> &str {
524 match self {
525 Self::Http(dep) => &dep.name,
526 Self::Git(dep) => &dep.name,
527 }
528 }
529
530 pub fn version_req(&self) -> &str {
532 match self {
533 Self::Http(dep) => &dep.version_req,
534 Self::Git(dep) => &dep.version_req,
535 }
536 }
537
538 pub fn url(&self) -> Option<&String> {
540 match self {
541 Self::Http(dep) => dep.url.as_ref(),
542 Self::Git(dep) => Some(&dep.git),
543 }
544 }
545
546 pub fn install_path_sync(&self, deps: impl AsRef<Path>) -> Option<PathBuf> {
548 debug!(dep:% = self; "trying to find installation path of dependency (sync)");
549 find_install_path_sync(self, deps)
550 }
551
552 pub async fn install_path(&self, deps: impl AsRef<Path>) -> Option<PathBuf> {
554 debug!(dep:% = self; "trying to find installation path of dependency (async)");
555 find_install_path(self, deps).await
556 }
557
558 pub fn project_root(&self) -> Option<PathBuf> {
560 match self {
561 Self::Http(dep) => dep.project_root.clone(),
562 Self::Git(dep) => dep.project_root.clone(),
563 }
564 }
565
566 pub fn to_toml_value(&self) -> (String, Item) {
568 match self {
569 Self::Http(dep) => (
570 dep.name.clone(),
571 match &dep.url {
572 Some(url) => {
573 let mut table = InlineTable::new();
574 table.insert(
575 "version",
576 value(&dep.version_req)
577 .into_value()
578 .expect("version should be a valid toml value"),
579 );
580 table.insert(
581 "url",
582 value(url).into_value().expect("url should be a valid toml value"),
583 );
584 if let Some(path) = dep.project_root.as_ref() {
585 table.insert(
586 "project_root",
587 value(path.to_string_lossy().into_owned())
588 .into_value()
589 .expect("project_root should be a valid toml value"),
590 );
591 }
592 value(table)
593 }
594 None => value(&dep.version_req),
595 },
596 ),
597 Self::Git(dep) => {
598 let mut table = InlineTable::new();
599 table.insert(
600 "version",
601 value(&dep.version_req)
602 .into_value()
603 .expect("version should be a valid toml value"),
604 );
605 table.insert(
606 "git",
607 value(&dep.git).into_value().expect("git URL should be a valid toml value"),
608 );
609 match &dep.identifier {
610 Some(GitIdentifier::Rev(rev)) => {
611 table.insert(
612 "rev",
613 value(rev).into_value().expect("rev should be a valid toml value"),
614 );
615 }
616 Some(GitIdentifier::Branch(branch)) => {
617 table.insert(
618 "branch",
619 value(branch)
620 .into_value()
621 .expect("branch should be a valid toml value"),
622 );
623 }
624 Some(GitIdentifier::Tag(tag)) => {
625 table.insert(
626 "tag",
627 value(tag).into_value().expect("tag should be a valid toml value"),
628 );
629 }
630 None => {}
631 }
632 if let Some(path) = dep.project_root.as_ref() {
633 table.insert(
634 "project_root",
635 value(path.to_string_lossy().into_owned())
636 .into_value()
637 .expect("project_root should be a valid toml value"),
638 );
639 }
640 (dep.name.clone(), value(table))
641 }
642 }
643 }
644
645 pub fn is_http(&self) -> bool {
647 matches!(self, Self::Http(_))
648 }
649
650 pub fn as_http(&self) -> Option<&HttpDependency> {
652 if let Self::Http(v) = self { Some(v) } else { None }
653 }
654
655 pub fn as_http_mut(&mut self) -> Option<&mut HttpDependency> {
657 if let Self::Http(v) = self { Some(v) } else { None }
658 }
659
660 pub fn is_git(&self) -> bool {
662 matches!(self, Self::Git(_))
663 }
664
665 pub fn as_git(&self) -> Option<&GitDependency> {
667 if let Self::Git(v) = self { Some(v) } else { None }
668 }
669
670 pub fn as_git_mut(&mut self) -> Option<&mut GitDependency> {
672 if let Self::Git(v) = self { Some(v) } else { None }
673 }
674}
675
676impl From<&HttpDependency> for Dependency {
677 fn from(dep: &HttpDependency) -> Self {
678 Self::Http(dep.clone())
679 }
680}
681
682impl From<&GitDependency> for Dependency {
683 fn from(dep: &GitDependency) -> Self {
684 Self::Git(dep.clone())
685 }
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromStr)]
690#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
691pub enum ConfigLocation {
692 Foundry,
694
695 Soldeer,
697}
698
699impl From<ConfigLocation> for PathBuf {
700 fn from(value: ConfigLocation) -> Self {
701 match value {
702 ConfigLocation::Foundry => Paths::foundry_default(),
703 ConfigLocation::Soldeer => Paths::soldeer_default(),
704 }
705 }
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Hash)]
710#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
711pub struct ParsingWarning {
712 dependency_name: String,
713 message: String,
714}
715
716impl fmt::Display for ParsingWarning {
717 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
718 write!(f, "{}: {}", self.dependency_name, self.message)
719 }
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Hash)]
724#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
725pub struct ParsingResult {
726 pub dependency: Dependency,
727 pub warnings: Vec<ParsingWarning>,
728}
729
730impl ParsingResult {
731 pub fn has_warnings(&self) -> bool {
733 !self.warnings.is_empty()
734 }
735}
736
737impl From<HttpDependency> for ParsingResult {
738 fn from(value: HttpDependency) -> Self {
739 Self { dependency: value.into(), warnings: Vec::default() }
740 }
741}
742
743impl From<GitDependency> for ParsingResult {
744 fn from(value: GitDependency) -> Self {
745 Self { dependency: value.into(), warnings: Vec::default() }
746 }
747}
748
749impl From<Dependency> for ParsingResult {
750 fn from(value: Dependency) -> Self {
751 Self { dependency: value, warnings: Vec::default() }
752 }
753}
754
755pub fn detect_config_location(root: impl AsRef<Path>) -> Option<ConfigLocation> {
761 let foundry_path = root.as_ref().join("foundry.toml");
762 let soldeer_path = root.as_ref().join("soldeer.toml");
763 if let Ok(contents) = fs::read_to_string(&foundry_path) {
764 debug!(path:? = foundry_path; "found foundry.toml file");
765 if let Ok(doc) = contents.parse::<DocumentMut>() {
766 if doc.contains_table("dependencies") {
767 debug!("found `dependencies` table in foundry.toml, so using that file for config");
768 return Some(ConfigLocation::Foundry);
769 } else {
770 debug!("foundry.toml does not contain `dependencies`, trying to use soldeer.toml");
771 }
772 } else {
773 warn!(path:? = foundry_path; "foundry.toml could not be parsed a toml");
774 }
775 } else if soldeer_path.exists() {
776 debug!(path:? = soldeer_path; "soldeer.toml exists, using that file for config");
777 return Some(ConfigLocation::Soldeer);
778 }
779 debug!("could not determine existing config file location");
780 None
781}
782
783pub fn read_config_deps(path: impl AsRef<Path>) -> Result<(Vec<Dependency>, Vec<ParsingWarning>)> {
797 let contents = fs::read_to_string(&path)?;
798 let doc: DocumentMut = contents.parse::<DocumentMut>()?;
799 let Some(Some(data)) = doc.get("dependencies").map(|v| v.as_table()) else {
800 warn!("no `dependencies` table in config file");
801 return Ok(Default::default());
802 };
803
804 let mut dependencies: Vec<Dependency> = Vec::new();
805 let mut warnings: Vec<ParsingWarning> = Vec::new();
806 for (name, v) in data {
807 let mut res = parse_dependency(name, v)?;
808 dependencies.push(res.dependency);
809 warnings.append(&mut res.warnings);
810 }
811 debug!(path:? = path.as_ref(); "found {} dependencies in config file", dependencies.len());
812 Ok((dependencies, warnings))
813}
814
815pub fn read_soldeer_config(path: impl AsRef<Path>) -> Result<SoldeerConfig> {
817 #[derive(Deserialize)]
818 struct SoldeerConfigParsed {
819 #[serde(default)]
820 soldeer: SoldeerConfig,
821 }
822
823 let contents = fs::read_to_string(&path)?;
824
825 let config: SoldeerConfigParsed = toml_edit::de::from_str(&contents)?;
826
827 debug!(path:? = path.as_ref(); "parsed soldeer config from file");
828 Ok(config.soldeer)
829}
830
831pub fn add_to_config(dependency: &Dependency, config_path: impl AsRef<Path>) -> Result<()> {
833 let contents = fs::read_to_string(&config_path)?;
834 let mut doc: DocumentMut = contents.parse::<DocumentMut>()?;
835
836 if !doc.contains_table("dependencies") {
838 debug!("`dependencies` table added to config file because it was missing");
839 doc.insert("dependencies", Item::Table(Table::default()));
840 }
841
842 let (name, value) = dependency.to_toml_value();
843 doc["dependencies"]
844 .as_table_mut()
845 .expect("dependencies should be a table")
846 .insert(&name, value);
847
848 fs::write(&config_path, doc.to_string())?;
849 debug!(dep:% = dependency, path:? = config_path.as_ref(); "added dependency to config file");
850 Ok(())
851}
852
853pub fn delete_from_config(dependency_name: &str, path: impl AsRef<Path>) -> Result<Dependency> {
855 let contents = fs::read_to_string(&path)?;
856 let mut doc: DocumentMut = contents.parse::<DocumentMut>().expect("invalid doc");
857
858 let Some(dependencies) = doc["dependencies"].as_table_mut() else {
859 debug!("no `dependencies` table in config file");
860 return Err(ConfigError::MissingDependency(dependency_name.to_string()));
861 };
862 let Some(item_removed) = dependencies.remove(dependency_name) else {
863 debug!("dependency not present in config file");
864 return Err(ConfigError::MissingDependency(dependency_name.to_string()));
865 };
866
867 let dependency = parse_dependency(dependency_name, &item_removed)?;
868
869 fs::write(&path, doc.to_string())?;
870 debug!(dep = dependency_name, path:? = path.as_ref(); "removed dependency from config file");
871 Ok(dependency.dependency)
872}
873
874pub fn update_config_libs(foundry_config: impl AsRef<Path>) -> Result<()> {
877 let contents = fs::read_to_string(&foundry_config)?;
878 let mut doc: DocumentMut = contents.parse::<DocumentMut>()?;
879
880 if !doc.contains_key("profile") {
881 debug!("missing `profile` in config file, adding it");
882 let mut profile = Table::default();
883 profile["default"] = Item::Table(Table::default());
884 profile.set_implicit(true);
885 doc["profile"] = Item::Table(profile);
886 }
887
888 let profile = doc["profile"].as_table_mut().expect("profile should be a table");
889 if !profile.contains_key("default") {
890 debug!("missing `default` profile in config file, adding it");
891 profile["default"] = Item::Table(Table::default());
892 }
893
894 let default_profile =
895 profile["default"].as_table_mut().expect("default profile should be a table");
896 if !default_profile.contains_key("libs") {
897 debug!("missing `libs` array in config file, adding it");
898 default_profile["libs"] = value(Array::from_iter(&["dependencies".to_string()]));
899 }
900
901 let libs = default_profile["libs"].as_array_mut().expect("libs should be an array");
902 if !libs.iter().any(|v| v.as_str() == Some("dependencies")) {
903 debug!("adding `dependencies` folder to `libs` array");
904 libs.push("dependencies");
905 }
906
907 if !doc.contains_table("dependencies") {
909 debug!("adding `dependencies` table in config file");
910 doc.insert("dependencies", Item::Table(Table::default()));
911 }
912
913 fs::write(&foundry_config, doc.to_string())?;
914 debug!(path:? = foundry_config.as_ref(); "config file updated");
915 Ok(())
916}
917
918fn find_git_root(relative_to: impl AsRef<Path>) -> Result<Option<PathBuf>> {
922 let root = dunce::canonicalize(relative_to)?;
923 Ok(root.ancestors().find(|p| p.join(".git").is_dir()).map(Path::to_path_buf))
924}
925
926fn find_project_root(cwd: Option<impl AsRef<Path>>) -> Result<PathBuf> {
940 let cwd = match cwd {
941 Some(path) => dunce::canonicalize(path)?,
942 None => env::current_dir()?,
943 };
944 let boundary = find_git_root(&cwd)?;
945 let found = cwd
946 .ancestors()
947 .take_while(|p| boundary.as_ref().map(|b| p.starts_with(b)).unwrap_or(true))
948 .find(|p| p.join("foundry.toml").is_file() || p.join("soldeer.toml").is_file())
949 .map(Path::to_path_buf);
950 Ok(found.or(boundary).unwrap_or_else(|| cwd.to_path_buf()))
951}
952
953fn parse_dependency(name: impl Into<String>, value: &Item) -> Result<ParsingResult> {
968 let name: String = name.into();
969 if let Some(version_req) = value.as_str() {
970 if version_req.is_empty() {
971 return Err(ConfigError::EmptyVersion(name));
972 }
973 return Ok(HttpDependency {
975 name,
976 version_req: version_req.to_string(),
977 url: None,
978 project_root: None,
979 }
980 .into());
981 }
982
983 let table = {
985 match value.as_inline_table() {
986 Some(table) => table,
987 None => match value.as_table() {
988 Some(table) => &table.clone().into_inline_table(),
990 None => {
991 debug!(dep = name; "dependency config entry could not be parsed as a table");
992 return Err(ConfigError::InvalidDependency(name));
993 }
994 },
995 }
996 };
997
998 let mut warnings = Vec::new();
999
1000 warnings.extend(table.iter().filter_map(|(k, _)| {
1002 if !["version", "url", "git", "rev", "branch", "tag", "project_root"].contains(&k) {
1003 warn!(dependency = name; "toml parsing: `{k}` is not a valid dependency option");
1004 Some(ParsingWarning {
1005 dependency_name: name.clone(),
1006 message: format!("`{k}` is not a valid dependency option"),
1007 })
1008 } else {
1009 None
1010 }
1011 }));
1012
1013 let version_req = match table.get("version").map(|v| v.as_str()) {
1015 Some(None) => {
1016 debug!(dep = name; "dependency's `version` field is not a string");
1017 return Err(ConfigError::InvalidField { field: "version".to_string(), dep: name });
1018 }
1019 None => {
1020 return Err(ConfigError::MissingField { field: "version".to_string(), dep: name });
1021 }
1022 Some(Some(version_req)) => version_req.to_string(),
1023 };
1024 if version_req.is_empty() {
1025 return Err(ConfigError::EmptyVersion(name));
1026 }
1027
1028 let project_root = match table.get("project_root").map(|v| v.as_str()) {
1030 Some(Some(path)) => Some(path.into()),
1031 Some(None) => {
1032 debug!(dep = name; "dependency's `project_root` field is not a string");
1033 return Err(ConfigError::InvalidField { field: "project_root".to_string(), dep: name });
1034 }
1035 None => None,
1036 };
1037
1038 match table.get("git").map(|v| v.as_str()) {
1040 Some(None) => {
1041 debug!(dep = name; "dependency's `git` field is not a string");
1042 return Err(ConfigError::InvalidField { field: "git".to_string(), dep: name });
1043 }
1044 Some(Some(git)) => {
1045 if table.get("url").is_some() {
1047 return Err(ConfigError::FieldConflict {
1048 field: "url".to_string(),
1049 conflicts_with: "git".to_string(),
1050 dep: name,
1051 });
1052 }
1053
1054 if version_req.contains('=') {
1058 return Err(ConfigError::InvalidVersionReq(name));
1059 }
1060 let rev = match table.get("rev").map(|v| v.as_str()) {
1062 Some(Some(rev)) => Some(rev.to_string()),
1063 Some(None) => {
1064 debug!(dep = name; "dependency's `rev` field is not a string");
1065 return Err(ConfigError::InvalidField { field: "rev".to_string(), dep: name });
1066 }
1067 None => None,
1068 };
1069 let branch = match table.get("branch").map(|v| v.as_str()) {
1070 Some(Some(tag)) => Some(tag.to_string()),
1071 Some(None) => {
1072 debug!(dep = name; "dependency's `branch` field is not a string");
1073 return Err(ConfigError::InvalidField {
1074 field: "branch".to_string(),
1075 dep: name,
1076 });
1077 }
1078 None => None,
1079 };
1080 let tag = match table.get("tag").map(|v| v.as_str()) {
1081 Some(Some(tag)) => Some(tag.to_string()),
1082 Some(None) => {
1083 debug!(dep = name; "dependency's `tag` field is not a string");
1084 return Err(ConfigError::InvalidField { field: "tag".to_string(), dep: name });
1085 }
1086 None => None,
1087 };
1088 let identifier = match (rev, branch, tag) {
1089 (Some(rev), None, None) => Some(GitIdentifier::from_rev(rev)),
1090 (None, Some(branch), None) => Some(GitIdentifier::from_branch(branch)),
1091 (None, None, Some(tag)) => Some(GitIdentifier::from_tag(tag)),
1092 (None, None, None) => None,
1093 _ => {
1094 return Err(ConfigError::GitIdentifierConflict(name));
1095 }
1096 };
1097 return Ok(ParsingResult {
1098 dependency: GitDependency {
1099 name,
1100 git: git.to_string(),
1101 version_req,
1102 identifier,
1103 project_root,
1104 }
1105 .into(),
1106 warnings,
1107 });
1108 }
1109 None => {}
1110 }
1111
1112 warnings.extend(table.iter().filter_map(|(k, _)| {
1116 if ["rev", "branch", "tag"].contains(&k) {
1117 warn!(dependency = name; "toml parsing: `{k}` is ignored if no `git` URL is provided");
1118 Some(ParsingWarning {
1119 dependency_name: name.clone(),
1120 message: format!("`{k}` is ignored if no `git` URL is provided"),
1121 })
1122 } else {
1123 None
1124 }
1125 }));
1126
1127 match table.get("url").map(|v| v.as_str()) {
1128 Some(None) => {
1129 debug!(dep = name; "dependency's `url` field is not a string");
1130 Err(ConfigError::InvalidField { field: "url".to_string(), dep: name })
1131 }
1132 None => Ok(ParsingResult {
1133 dependency: HttpDependency { name, version_req, url: None, project_root }.into(),
1134 warnings,
1135 }),
1136 Some(Some(url)) => {
1137 if version_req.contains('=') {
1142 return Err(ConfigError::InvalidVersionReq(name));
1143 }
1144 Ok(ParsingResult {
1145 dependency: HttpDependency {
1146 name,
1147 version_req,
1148 url: Some(url.to_string()),
1149 project_root,
1150 }
1151 .into(),
1152 warnings,
1153 })
1154 }
1155 }
1156}
1157
1158fn create_or_modify_config(
1161 location: ConfigLocation,
1162 foundry_path: impl AsRef<Path>,
1163 soldeer_path: impl AsRef<Path>,
1164) -> Result<PathBuf> {
1165 match location {
1166 ConfigLocation::Foundry => {
1167 let foundry_path = foundry_path.as_ref();
1168 if foundry_path.exists() {
1169 update_config_libs(foundry_path)?;
1170 return Ok(foundry_path.to_path_buf());
1171 }
1172 debug!(path:? = foundry_path; "foundry.toml does not exist, creating it");
1173 let contents = r#"[profile.default]
1174src = "src"
1175out = "out"
1176libs = ["dependencies"]
1177
1178[dependencies]
1179
1180# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
1181"#;
1182
1183 fs::write(foundry_path, contents)?;
1184 Ok(foundry_path.to_path_buf())
1185 }
1186 ConfigLocation::Soldeer => {
1187 let soldeer_path = soldeer_path.as_ref();
1188 if soldeer_path.exists() {
1189 return Ok(soldeer_path.to_path_buf());
1190 }
1191 debug!(path:? = soldeer_path; "soldeer.toml does not exist, creating it");
1192 fs::write(soldeer_path, "[dependencies]\n")?;
1193 Ok(soldeer_path.to_path_buf())
1194 }
1195 }
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201 use crate::errors::ConfigError;
1202 use path_slash::PathBufExt;
1203 use std::{fs, path::PathBuf};
1204 use temp_env::with_var;
1205 use testdir::testdir;
1206
1207 fn write_to_config(content: &str, filename: &str) -> PathBuf {
1208 let path = testdir!().join(filename);
1209 fs::write(&path, content).unwrap();
1210 path
1211 }
1212
1213 #[test]
1214 fn test_paths_config_soldeer() {
1215 let config_path = write_to_config("[dependencies]\n", "soldeer.toml");
1216 with_var(
1217 "SOLDEER_PROJECT_ROOT",
1218 Some(config_path.parent().unwrap().to_string_lossy().to_string()),
1219 || {
1220 let res = Paths::new();
1221 assert!(res.is_ok(), "{res:?}");
1222 assert_eq!(res.unwrap().config.to_slash_lossy(), config_path.to_slash_lossy());
1223 },
1224 );
1225 }
1226
1227 #[test]
1228 fn test_paths_config_foundry() {
1229 let config_contents = r#"[profile.default]
1230libs = ["dependencies"]
1231
1232[dependencies]
1233"#;
1234 let config_path = write_to_config(config_contents, "foundry.toml");
1235 with_var(
1236 "SOLDEER_PROJECT_ROOT",
1237 Some(config_path.parent().unwrap().to_string_lossy().to_string()),
1238 || {
1239 let res = Paths::new();
1240 assert!(res.is_ok(), "{res:?}");
1241 assert_eq!(res.unwrap().config, config_path);
1242 },
1243 );
1244 }
1245
1246 #[test]
1247 fn test_paths_from_root() {
1248 let config_path = write_to_config("[dependencies]\n", "soldeer.toml");
1249 let root = config_path.parent().unwrap();
1250 let res = Paths::from_root(root);
1251 assert!(res.is_ok(), "{res:?}");
1252 assert_eq!(res.unwrap().root, root);
1253 }
1254
1255 #[test]
1256 fn test_from_name_version_no_url() {
1257 let res = Dependency::from_name_version("dependency~1.0.0", None, None);
1258 assert!(res.is_ok(), "{res:?}");
1259 assert_eq!(
1260 res.unwrap(),
1261 HttpDependency::builder().name("dependency").version_req("1.0.0").build().into()
1262 );
1263 }
1264
1265 #[test]
1266 fn test_from_name_version_with_http_url() {
1267 let res = Dependency::from_name_version(
1268 "dependency~1.0.0",
1269 Some(UrlType::http("https://github.com/user/repo/archive/123.zip")),
1270 None,
1271 );
1272 assert!(res.is_ok(), "{res:?}");
1273 assert_eq!(
1274 res.unwrap(),
1275 HttpDependency::builder()
1276 .name("dependency")
1277 .version_req("1.0.0")
1278 .url("https://github.com/user/repo/archive/123.zip")
1279 .build()
1280 .into()
1281 );
1282 }
1283
1284 #[test]
1285 fn test_from_name_version_with_git_url() {
1286 let res = Dependency::from_name_version(
1287 "dependency~1.0.0",
1288 Some(UrlType::git("https://github.com/user/repo.git")),
1289 None,
1290 );
1291 assert!(res.is_ok(), "{res:?}");
1292 assert_eq!(
1293 res.unwrap(),
1294 GitDependency::builder()
1295 .name("dependency")
1296 .version_req("1.0.0")
1297 .git("https://github.com/user/repo.git")
1298 .build()
1299 .into()
1300 );
1301
1302 let res = Dependency::from_name_version(
1303 "dependency~1.0.0",
1304 Some(UrlType::git("https://test:test@gitlab.com/user/repo.git")),
1305 None,
1306 );
1307 assert!(res.is_ok(), "{res:?}");
1308 assert_eq!(
1309 res.unwrap(),
1310 GitDependency::builder()
1311 .name("dependency")
1312 .version_req("1.0.0")
1313 .git("https://test:test@gitlab.com/user/repo.git")
1314 .build()
1315 .into()
1316 );
1317 }
1318
1319 #[test]
1320 fn test_from_name_version_with_git_url_rev() {
1321 let res = Dependency::from_name_version(
1322 "dependency~1.0.0",
1323 Some(UrlType::git("https://github.com/user/repo.git")),
1324 Some(GitIdentifier::from_rev("123456")),
1325 );
1326 assert!(res.is_ok(), "{res:?}");
1327 assert_eq!(
1328 res.unwrap(),
1329 GitDependency::builder()
1330 .name("dependency")
1331 .version_req("1.0.0")
1332 .git("https://github.com/user/repo.git")
1333 .identifier(GitIdentifier::from_rev("123456"))
1334 .build()
1335 .into()
1336 );
1337 }
1338
1339 #[test]
1340 fn test_from_name_version_with_git_url_branch() {
1341 let res = Dependency::from_name_version(
1342 "dependency~1.0.0",
1343 Some(UrlType::git("https://github.com/user/repo.git")),
1344 Some(GitIdentifier::from_branch("dev")),
1345 );
1346 assert!(res.is_ok(), "{res:?}");
1347 assert_eq!(
1348 res.unwrap(),
1349 GitDependency::builder()
1350 .name("dependency")
1351 .version_req("1.0.0")
1352 .git("https://github.com/user/repo.git")
1353 .identifier(GitIdentifier::from_branch("dev"))
1354 .build()
1355 .into()
1356 );
1357 }
1358
1359 #[test]
1360 fn test_from_name_version_with_git_url_tag() {
1361 let res = Dependency::from_name_version(
1362 "dependency~1.0.0",
1363 Some(UrlType::git("https://github.com/user/repo.git")),
1364 Some(GitIdentifier::from_tag("v1.0.0")),
1365 );
1366 assert!(res.is_ok(), "{res:?}");
1367 assert_eq!(
1368 res.unwrap(),
1369 GitDependency::builder()
1370 .name("dependency")
1371 .version_req("1.0.0")
1372 .git("https://github.com/user/repo.git")
1373 .identifier(GitIdentifier::from_tag("v1.0.0"))
1374 .build()
1375 .into()
1376 );
1377 }
1378
1379 #[test]
1380 fn test_from_name_version_with_git_ssh() {
1381 let res = Dependency::from_name_version(
1382 "dependency~1.0.0",
1383 Some(UrlType::git("git@github.com:user/repo.git")),
1384 None,
1385 );
1386 assert!(res.is_ok(), "{res:?}");
1387 assert_eq!(
1388 res.unwrap(),
1389 GitDependency::builder()
1390 .name("dependency")
1391 .version_req("1.0.0")
1392 .git("git@github.com:user/repo.git")
1393 .build()
1394 .into()
1395 );
1396 }
1397
1398 #[test]
1399 fn test_from_name_version_with_git_ssh_rev() {
1400 let res = Dependency::from_name_version(
1401 "dependency~1.0.0",
1402 Some(UrlType::git("git@github.com:user/repo.git")),
1403 Some(GitIdentifier::from_rev("123456")),
1404 );
1405 assert!(res.is_ok(), "{res:?}");
1406 assert_eq!(
1407 res.unwrap(),
1408 GitDependency::builder()
1409 .name("dependency")
1410 .version_req("1.0.0")
1411 .git("git@github.com:user/repo.git")
1412 .identifier(GitIdentifier::from_rev("123456"))
1413 .build()
1414 .into()
1415 );
1416 }
1417
1418 #[test]
1419 fn test_from_name_version_empty_version() {
1420 let res = Dependency::from_name_version("dependency~", None, None);
1421 assert!(matches!(res, Err(ConfigError::EmptyVersion(_))), "{res:?}");
1422 }
1423
1424 #[test]
1425 fn test_from_name_version_invalid_version() {
1426 let res = Dependency::from_name_version("dependency~asdf=", None, None);
1428 assert!(res.is_ok(), "{res:?}");
1429
1430 let res = Dependency::from_name_version(
1431 "dependency~asdf=",
1432 Some(UrlType::http("https://example.com")),
1433 None,
1434 );
1435 assert!(matches!(res, Err(ConfigError::InvalidVersionReq(_))), "{res:?}");
1436
1437 let res = Dependency::from_name_version(
1438 "dependency~asdf=",
1439 Some(UrlType::git("git@github.com:user/repo.git")),
1440 None,
1441 );
1442 assert!(matches!(res, Err(ConfigError::InvalidVersionReq(_))), "{res:?}");
1443 }
1444
1445 #[test]
1446 fn test_read_soldeer_config_default() {
1447 let config_contents = r#"[profile.default]
1448libs = ["dependencies"]
1449"#;
1450 let config_path = write_to_config(config_contents, "foundry.toml");
1451 let res = read_soldeer_config(config_path);
1452 assert!(res.is_ok(), "{res:?}");
1453 assert_eq!(res.unwrap(), SoldeerConfig::default());
1454 }
1455
1456 #[test]
1457 fn test_read_soldeer_config() {
1458 let config_contents = r#"[soldeer]
1459remappings_generate = false
1460remappings_regenerate = true
1461remappings_version = false
1462remappings_prefix = "@"
1463remappings_location = "config"
1464recursive_deps = true
1465"#;
1466 let expected = SoldeerConfig {
1467 remappings_generate: false,
1468 remappings_regenerate: true,
1469 remappings_version: false,
1470 remappings_prefix: "@".to_string(),
1471 remappings_location: RemappingsLocation::Config,
1472 recursive_deps: true,
1473 };
1474
1475 let config_path = write_to_config(config_contents, "soldeer.toml");
1476 let res = read_soldeer_config(config_path);
1477 assert!(res.is_ok(), "{res:?}");
1478 assert_eq!(res.unwrap(), expected);
1479
1480 let config_path = write_to_config(config_contents, "foundry.toml");
1481 let res = read_soldeer_config(config_path);
1482 assert!(res.is_ok(), "{res:?}");
1483 assert_eq!(res.unwrap(), expected);
1484 }
1485
1486 #[test]
1487 fn test_read_foundry_config_deps() {
1488 let config_contents = r#"[profile.default]
1489libs = ["dependencies"]
1490
1491[dependencies]
1492"lib1" = "1.0.0"
1493"lib2" = { version = "2.0.0" }
1494"lib3" = { version = "3.0.0", url = "https://example.com" }
1495"lib4" = { version = "4.0.0", git = "https://example.com/repo.git" }
1496"lib5" = { version = "5.0.0", git = "https://example.com/repo.git", rev = "123456" }
1497"lib6" = { version = "6.0.0", git = "https://example.com/repo.git", branch = "dev" }
1498"lib7" = { version = "7.0.0", git = "https://example.com/repo.git", tag = "v7.0.0" }
1499"lib8" = { version = "8.0.0", url = "https://example.com", project_root = "foo/bar" }
1500"lib9" = { version = "9.0.0", git = "https://example.com/repo.git", project_root = "test/test2" }
1501"#;
1502 let config_path = write_to_config(config_contents, "foundry.toml");
1503 let res = read_config_deps(config_path);
1504 assert!(res.is_ok(), "{res:?}");
1505 let (result, _) = res.unwrap();
1506
1507 assert_eq!(
1508 result[0],
1509 HttpDependency::builder().name("lib1").version_req("1.0.0").build().into()
1510 );
1511 assert_eq!(
1512 result[1],
1513 HttpDependency::builder().name("lib2").version_req("2.0.0").build().into()
1514 );
1515 assert_eq!(
1516 result[2],
1517 HttpDependency::builder()
1518 .name("lib3")
1519 .version_req("3.0.0")
1520 .url("https://example.com")
1521 .build()
1522 .into()
1523 );
1524 assert_eq!(
1525 result[3],
1526 GitDependency::builder()
1527 .name("lib4")
1528 .version_req("4.0.0")
1529 .git("https://example.com/repo.git")
1530 .build()
1531 .into()
1532 );
1533 assert_eq!(
1534 result[4],
1535 GitDependency::builder()
1536 .name("lib5")
1537 .version_req("5.0.0")
1538 .git("https://example.com/repo.git")
1539 .identifier(GitIdentifier::from_rev("123456"))
1540 .build()
1541 .into()
1542 );
1543 assert_eq!(
1544 result[5],
1545 GitDependency::builder()
1546 .name("lib6")
1547 .version_req("6.0.0")
1548 .git("https://example.com/repo.git")
1549 .identifier(GitIdentifier::from_branch("dev"))
1550 .build()
1551 .into()
1552 );
1553 assert_eq!(
1554 result[6],
1555 GitDependency::builder()
1556 .name("lib7")
1557 .version_req("7.0.0")
1558 .git("https://example.com/repo.git")
1559 .identifier(GitIdentifier::from_tag("v7.0.0"))
1560 .build()
1561 .into()
1562 );
1563 assert_eq!(
1564 result[7],
1565 HttpDependency::builder()
1566 .name("lib8")
1567 .version_req("8.0.0")
1568 .url("https://example.com")
1569 .project_root("foo/bar")
1570 .build()
1571 .into()
1572 );
1573 assert_eq!(
1574 result[8],
1575 GitDependency::builder()
1576 .name("lib9")
1577 .version_req("9.0.0")
1578 .git("https://example.com/repo.git")
1579 .project_root("test/test2")
1580 .build()
1581 .into()
1582 );
1583 }
1584
1585 #[test]
1586 fn test_read_soldeer_config_deps() {
1587 let config_contents = r#"[dependencies]
1588"lib1" = "1.0.0"
1589"lib2" = { version = "2.0.0" }
1590"lib3" = { version = "3.0.0", url = "https://example.com" }
1591"lib4" = { version = "4.0.0", git = "https://example.com/repo.git" }
1592"lib5" = { version = "5.0.0", git = "https://example.com/repo.git", rev = "123456" }
1593"lib6" = { version = "6.0.0", git = "https://example.com/repo.git", branch = "dev" }
1594"lib7" = { version = "7.0.0", git = "https://example.com/repo.git", tag = "v7.0.0" }
1595"lib8" = { version = "8.0.0", url = "https://example.com", project_root = "foo/bar" }
1596"lib9" = { version = "9.0.0", git = "https://example.com/repo.git", project_root = "test/test2" }
1597"#;
1598 let config_path = write_to_config(config_contents, "soldeer.toml");
1599 let res = read_config_deps(config_path);
1600 assert!(res.is_ok(), "{res:?}");
1601 let (result, _) = res.unwrap();
1602
1603 assert_eq!(
1604 result[0],
1605 HttpDependency::builder().name("lib1").version_req("1.0.0").build().into()
1606 );
1607 assert_eq!(
1608 result[1],
1609 HttpDependency::builder().name("lib2").version_req("2.0.0").build().into()
1610 );
1611 assert_eq!(
1612 result[2],
1613 HttpDependency::builder()
1614 .name("lib3")
1615 .version_req("3.0.0")
1616 .url("https://example.com")
1617 .build()
1618 .into()
1619 );
1620 assert_eq!(
1621 result[3],
1622 GitDependency::builder()
1623 .name("lib4")
1624 .version_req("4.0.0")
1625 .git("https://example.com/repo.git")
1626 .build()
1627 .into()
1628 );
1629 assert_eq!(
1630 result[4],
1631 GitDependency::builder()
1632 .name("lib5")
1633 .version_req("5.0.0")
1634 .git("https://example.com/repo.git")
1635 .identifier(GitIdentifier::from_rev("123456"))
1636 .build()
1637 .into()
1638 );
1639 assert_eq!(
1640 result[5],
1641 GitDependency::builder()
1642 .name("lib6")
1643 .version_req("6.0.0")
1644 .git("https://example.com/repo.git")
1645 .identifier(GitIdentifier::from_branch("dev"))
1646 .build()
1647 .into()
1648 );
1649 assert_eq!(
1650 result[6],
1651 GitDependency::builder()
1652 .name("lib7")
1653 .version_req("7.0.0")
1654 .git("https://example.com/repo.git")
1655 .identifier(GitIdentifier::from_tag("v7.0.0"))
1656 .build()
1657 .into()
1658 );
1659 assert_eq!(
1660 result[7],
1661 HttpDependency::builder()
1662 .name("lib8")
1663 .version_req("8.0.0")
1664 .url("https://example.com")
1665 .project_root("foo/bar")
1666 .build()
1667 .into()
1668 );
1669 assert_eq!(
1670 result[8],
1671 GitDependency::builder()
1672 .name("lib9")
1673 .version_req("9.0.0")
1674 .git("https://example.com/repo.git")
1675 .project_root("test/test2")
1676 .build()
1677 .into()
1678 );
1679 }
1680
1681 #[test]
1682 fn test_read_soldeer_config_deps_bad_version() {
1683 for dep in [
1684 r#""lib1" = """#,
1685 r#""lib1" = { version = "" }"#,
1686 r#""lib1" = { version = "", url = "https://example.com" }"#,
1687 r#""lib1" = { version = "", git = "https://example.com/repo.git" }"#,
1688 r#""lib1" = { version = "", git = "https://example.com/repo.git", rev = "123456" }"#,
1689 ] {
1690 let config_contents = format!("[dependencies]\n{dep}");
1691 let config_path = write_to_config(&config_contents, "soldeer.toml");
1692 let res = read_config_deps(config_path);
1693 assert!(matches!(res, Err(ConfigError::EmptyVersion(_))), "{res:?}");
1694 }
1695
1696 for dep in [
1697 r#""lib1" = { version = "asdf=", url = "https://example.com" }"#,
1698 r#""lib1" = { version = "asdf=", git = "https://example.com/repo.git" }"#,
1699 r#""lib1" = { version = "asdf=", git = "https://example.com/repo.git", rev = "123456" }"#,
1700 ] {
1701 let config_contents = format!("[dependencies]\n{dep}");
1702 let config_path = write_to_config(&config_contents, "soldeer.toml");
1703 let res = read_config_deps(config_path);
1704 assert!(matches!(res, Err(ConfigError::InvalidVersionReq(_))), "{res:?}");
1705 }
1706
1707 let config_contents = r#"[dependencies]
1710"lib1" = "asdf="
1711"lib2" = { version = "asdf=" }
1712"#;
1713 let config_path = write_to_config(config_contents, "soldeer.toml");
1714 let res = read_config_deps(config_path);
1715 assert!(res.is_ok(), "{res:?}");
1716 }
1717
1718 #[test]
1719 fn test_read_soldeer_config_deps_bad_git() {
1720 for dep in [
1721 r#""lib1" = { version = "1.0.0", git = "https://example.com/repo.git", rev = "123456", branch = "dev" }"#,
1722 r#""lib1" = { version = "1.0.0", git = "https://example.com/repo.git", rev = "123456", tag = "v1.0.0" }"#,
1723 r#""lib1" = { version = "1.0.0", git = "https://example.com/repo.git", branch = "dev", tag = "v1.0.0" }"#,
1724 r#""lib1" = { version = "1.0.0", git = "https://example.com/repo.git", rev = "123456", branch = "dev", tag = "v1.0.0" }"#,
1725 ] {
1726 let config_contents = format!("[dependencies]\n{dep}");
1727 let config_path = write_to_config(&config_contents, "soldeer.toml");
1728 let res = read_config_deps(config_path);
1729 assert!(matches!(res, Err(ConfigError::GitIdentifierConflict(_))), "{res:?}");
1730 }
1731 }
1732
1733 #[test]
1734 fn test_add_to_config() {
1735 let config_path = write_to_config("[dependencies]\n", "soldeer.toml");
1736
1737 let deps: &[Dependency] = &[
1738 HttpDependency::builder().name("lib1").version_req("1.0.0").build().into(),
1739 HttpDependency::builder()
1740 .name("lib2")
1741 .version_req("1.0.0")
1742 .url("https://test.com/test.zip")
1743 .build()
1744 .into(),
1745 HttpDependency::builder()
1746 .name("lib21")
1747 .version_req("1.0.0")
1748 .url("https://test.com/test.zip")
1749 .project_root("foo/bar")
1750 .build()
1751 .into(),
1752 GitDependency::builder()
1753 .name("lib3")
1754 .version_req("1.0.0")
1755 .git("https://example.com/repo.git")
1756 .build()
1757 .into(),
1758 GitDependency::builder()
1759 .name("lib4")
1760 .version_req("1.0.0")
1761 .git("https://example.com/repo.git")
1762 .identifier(GitIdentifier::from_rev("123456"))
1763 .build()
1764 .into(),
1765 GitDependency::builder()
1766 .name("lib5")
1767 .version_req("1.0.0")
1768 .git("https://example.com/repo.git")
1769 .identifier(GitIdentifier::from_branch("dev"))
1770 .build()
1771 .into(),
1772 GitDependency::builder()
1773 .name("lib6")
1774 .version_req("1.0.0")
1775 .git("https://example.com/repo.git")
1776 .identifier(GitIdentifier::from_tag("v1.0.0"))
1777 .build()
1778 .into(),
1779 GitDependency::builder()
1780 .name("lib7")
1781 .version_req("1.0.0")
1782 .git("https://example.com/repo.git")
1783 .project_root("foo/bar")
1784 .build()
1785 .into(),
1786 ];
1787 for dep in deps {
1788 let res = add_to_config(dep, &config_path);
1789 assert!(res.is_ok(), "{dep}: {res:?}");
1790 }
1791
1792 let (parsed, _) = read_config_deps(&config_path).unwrap();
1793 for (dep, parsed) in deps.iter().zip(parsed.iter()) {
1794 assert_eq!(dep, parsed);
1795 }
1796 }
1797
1798 #[test]
1799 fn test_add_to_config_no_section() {
1800 let config_path = write_to_config("", "soldeer.toml");
1801 let dep = Dependency::from_name_version("lib1~1.0.0", None, None).unwrap();
1802 let res = add_to_config(&dep, &config_path);
1803 assert!(res.is_ok(), "{res:?}");
1804 let (parsed, _) = read_config_deps(&config_path).unwrap();
1805 assert_eq!(parsed[0], dep);
1806 }
1807
1808 #[test]
1809 fn test_delete_from_config() {
1810 let config_contents = r#"[dependencies]
1811"lib1" = "1.0.0"
1812"lib2" = { version = "2.0.0" }
1813"lib3" = { version = "3.0.0", url = "https://example.com" }
1814"lib4" = { version = "4.0.0", git = "https://example.com/repo.git" }
1815"lib5" = { version = "5.0.0", git = "https://example.com/repo.git", rev = "123456" }
1816"lib6" = { version = "6.0.0", git = "https://example.com/repo.git", branch = "dev" }
1817"lib7" = { version = "7.0.0", git = "https://example.com/repo.git", tag = "v7.0.0" }
1818"lib8" = { version = "8.0.0", url = "https://example.com", project_root = "foo/bar" }
1819"lib9" = { version = "9.0.0", git = "https://example.com/repo.git", project_root = "foo/bar" }
1820 "#;
1821 let config_path = write_to_config(config_contents, "soldeer.toml");
1822 let res = delete_from_config("lib1", &config_path);
1823 assert!(res.is_ok(), "{res:?}");
1824 assert_eq!(res.unwrap().name(), "lib1");
1825 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 8);
1826
1827 let res = delete_from_config("lib2", &config_path);
1828 assert!(res.is_ok(), "{res:?}");
1829 assert_eq!(res.unwrap().name(), "lib2");
1830 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 7);
1831
1832 let res = delete_from_config("lib3", &config_path);
1833 assert!(res.is_ok(), "{res:?}");
1834 assert_eq!(res.unwrap().name(), "lib3");
1835 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 6);
1836
1837 let res = delete_from_config("lib4", &config_path);
1838 assert!(res.is_ok(), "{res:?}");
1839 assert_eq!(res.unwrap().name(), "lib4");
1840 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 5);
1841
1842 let res = delete_from_config("lib5", &config_path);
1843 assert!(res.is_ok(), "{res:?}");
1844 assert_eq!(res.unwrap().name(), "lib5");
1845 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 4);
1846
1847 let res = delete_from_config("lib6", &config_path);
1848 assert!(res.is_ok(), "{res:?}");
1849 assert_eq!(res.unwrap().name(), "lib6");
1850 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 3);
1851
1852 let res = delete_from_config("lib7", &config_path);
1853 assert!(res.is_ok(), "{res:?}");
1854 assert_eq!(res.unwrap().name(), "lib7");
1855 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 2);
1856
1857 let res = delete_from_config("lib8", &config_path);
1858 assert!(res.is_ok(), "{res:?}");
1859 assert_eq!(res.unwrap().name(), "lib8");
1860 assert_eq!(read_config_deps(&config_path).unwrap().0.len(), 1);
1861
1862 let res = delete_from_config("lib9", &config_path);
1863 assert!(res.is_ok(), "{res:?}");
1864 assert_eq!(res.unwrap().name(), "lib9");
1865 assert!(read_config_deps(&config_path).unwrap().0.is_empty());
1866 }
1867
1868 #[test]
1869 fn test_delete_from_config_missing() {
1870 let config_contents = r#"[dependencies]
1871"lib1" = "1.0.0"
1872 "#;
1873 let config_path = write_to_config(config_contents, "soldeer.toml");
1874 let res = delete_from_config("libfoo", &config_path);
1875 assert!(matches!(res, Err(ConfigError::MissingDependency(_))), "{res:?}");
1876 }
1877
1878 #[test]
1879 fn test_update_config_libs() {
1880 let config_contents = r#"[profile.default]
1881libs = ["lib"]
1882
1883[dependencies]
1884"#;
1885 let config_path = write_to_config(config_contents, "foundry.toml");
1886 let res = update_config_libs(&config_path);
1887 assert!(res.is_ok(), "{res:?}");
1888 let contents = fs::read_to_string(&config_path).unwrap();
1889 assert_eq!(
1890 contents,
1891 r#"[profile.default]
1892libs = ["lib", "dependencies"]
1893
1894[dependencies]
1895"#
1896 );
1897 }
1898
1899 #[test]
1900 fn test_update_config_profile_empty() {
1901 let config_contents = r#"[dependencies]
1902"#;
1903 let config_path = write_to_config(config_contents, "foundry.toml");
1904 let res = update_config_libs(&config_path);
1905 assert!(res.is_ok(), "{res:?}");
1906 let contents = fs::read_to_string(&config_path).unwrap();
1907 assert_eq!(
1908 contents,
1909 r#"[dependencies]
1910
1911[profile.default]
1912libs = ["dependencies"]
1913"#
1914 );
1915 }
1916
1917 #[test]
1918 fn test_update_config_libs_empty() {
1919 let config_contents = r#"[profile.default]
1920src = "src"
1921
1922[dependencies]
1923"#;
1924 let config_path = write_to_config(config_contents, "foundry.toml");
1925 let res = update_config_libs(&config_path);
1926 assert!(res.is_ok(), "{res:?}");
1927 let contents = fs::read_to_string(&config_path).unwrap();
1928 assert_eq!(
1929 contents,
1930 r#"[profile.default]
1931src = "src"
1932libs = ["dependencies"]
1933
1934[dependencies]
1935"#
1936 );
1937 }
1938
1939 #[test]
1940 fn test_parse_dependency() {
1941 let config_contents = r#"[dependencies]
1942"lib1" = "1.0.0"
1943"lib2" = { version = "2.0.0" }
1944"lib3" = { version = "3.0.0", url = "https://example.com" }
1945"lib4" = { version = "4.0.0", git = "https://example.com/repo.git" }
1946"lib5" = { version = "5.0.0", git = "https://example.com/repo.git", rev = "123456" }
1947"lib6" = { version = "6.0.0", git = "https://example.com/repo.git", branch = "dev" }
1948"lib7" = { version = "7.0.0", git = "https://example.com/repo.git", tag = "v7.0.0" }
1949"lib8" = { version = "8.0.0", url = "https://example.com", project_root = "foo/bar" }
1950"lib9" = { version = "9.0.0", git = "https://example.com/repo.git", project_root = "foo/bar" }
1951"#;
1952 let doc: DocumentMut = config_contents.parse::<DocumentMut>().unwrap();
1953 let data = doc.get("dependencies").map(|v| v.as_table()).unwrap().unwrap();
1954 for (name, v) in data {
1955 let res = parse_dependency(name, v);
1956 assert!(res.is_ok(), "{res:?}");
1957 }
1958 }
1959
1960 #[test]
1961 fn test_parse_dependency_extra_field() {
1962 let config_contents = r#"[dependencies]
1963"lib1" = { version = "3.0.0", url = "https://example.com", foo = "bar" }
1964"#;
1965 let doc: DocumentMut = config_contents.parse::<DocumentMut>().unwrap();
1966 let data = doc.get("dependencies").map(|v| v.as_table()).unwrap().unwrap();
1967 for (name, v) in data {
1968 let res = parse_dependency(name, v).unwrap();
1969 assert_eq!(res.warnings[0].message, "`foo` is not a valid dependency option");
1970 }
1971 }
1972
1973 #[test]
1974 fn test_parse_dependency_git_extra_url() {
1975 let config_contents = r#"[dependencies]
1976"lib1" = { version = "3.0.0", git = "https://example.com/repo.git", url = "https://example.com" }
1977"#;
1978 let doc: DocumentMut = config_contents.parse::<DocumentMut>().unwrap();
1979 let data = doc.get("dependencies").map(|v| v.as_table()).unwrap().unwrap();
1980 for (name, v) in data {
1981 let res = parse_dependency(name, v);
1982 assert!(
1983 matches!(
1984 res,
1985 Err(ConfigError::FieldConflict { field: _, conflicts_with: _, dep: _ })
1986 ),
1987 "{res:?}"
1988 );
1989 }
1990 }
1991
1992 #[test]
1993 fn test_parse_dependency_git_field_conflict() {
1994 let config_contents = r#"[dependencies]
1995"lib2" = { version = "3.0.0", git = "https://example.com/repo.git", rev = "123456", branch = "dev" }
1996"lib3" = { version = "3.0.0", git = "https://example.com/repo.git", rev = "123456", tag = "v7.0.0" }
1997"lib4" = { version = "3.0.0", git = "https://example.com/repo.git", branch = "dev", tag = "v7.0.0" }
1998"#;
1999 let doc: DocumentMut = config_contents.parse::<DocumentMut>().unwrap();
2000 let data = doc.get("dependencies").map(|v| v.as_table()).unwrap().unwrap();
2001 for (name, v) in data {
2002 let res = parse_dependency(name, v);
2003 assert!(matches!(res, Err(ConfigError::GitIdentifierConflict(_))), "{res:?}");
2004 }
2005 }
2006
2007 #[test]
2008 fn test_parse_dependency_missing_url() {
2009 let config_contents = r#"[dependencies]
2010"lib1" = { version = "3.0.0", rev = "123456" }
2011"lib2" = { version = "3.0.0", branch = "dev" }
2012"lib3" = { version = "3.0.0", tag = "v7.0.0" }
2013"#;
2014 let doc: DocumentMut = config_contents.parse::<DocumentMut>().unwrap();
2015 let data = doc.get("dependencies").map(|v| v.as_table()).unwrap().unwrap();
2016 for (name, v) in data {
2017 let res = parse_dependency(name, v).unwrap();
2018 assert!(res.warnings[0].message.ends_with("is ignored if no `git` URL is provided"));
2019 }
2020 }
2021
2022 #[test]
2023 fn test_find_git_root() {
2024 let test_dir = testdir!();
2025 let git_dir = test_dir.join(".git");
2026 fs::create_dir(&git_dir).unwrap();
2027
2028 let result = find_git_root(&test_dir);
2029 assert!(result.is_ok(), "{result:?}");
2030 assert_eq!(result.unwrap(), Some(test_dir.clone()));
2031
2032 let sub_dir = test_dir.join("subdir");
2034 fs::create_dir(&sub_dir).unwrap();
2035
2036 let result = find_git_root(&sub_dir);
2037 assert!(result.is_ok(), "{result:?}");
2038 assert_eq!(result.unwrap(), Some(test_dir));
2039
2040 let temp_dir = std::env::temp_dir().join("soldeer_test_no_git");
2042 if !temp_dir.exists() {
2043 fs::create_dir(&temp_dir).unwrap();
2044 }
2045
2046 let result = find_git_root(&temp_dir);
2047 assert_eq!(result.unwrap(), None);
2048
2049 fs::remove_dir(&temp_dir).unwrap();
2051 }
2052
2053 #[test]
2054 fn test_find_git_root_nested() {
2055 let outer_dir = testdir!();
2057 fs::create_dir(outer_dir.join(".git")).unwrap();
2058
2059 let inner_dir = outer_dir.join("inner");
2060 fs::create_dir(&inner_dir).unwrap();
2061 fs::create_dir(inner_dir.join(".git")).unwrap();
2062
2063 let result = find_git_root(&inner_dir);
2065 assert!(result.is_ok(), "{result:?}");
2066 assert_eq!(result.unwrap(), Some(inner_dir));
2067
2068 let result = find_git_root(&outer_dir);
2070 assert!(result.is_ok(), "{result:?}");
2071 assert_eq!(result.unwrap(), Some(outer_dir));
2072 }
2073
2074 #[test]
2075 fn test_find_project_root_with_foundry_toml() {
2076 let test_dir = testdir!();
2077 let foundry_toml = test_dir.join("foundry.toml");
2078 fs::write(&foundry_toml, "[dependencies]\n").unwrap();
2079
2080 let result = find_project_root(Some(&test_dir));
2081 assert!(result.is_ok(), "{result:?}");
2082 assert_eq!(result.unwrap(), test_dir);
2083 }
2084
2085 #[test]
2086 fn test_find_project_root_with_soldeer_toml() {
2087 let test_dir = testdir!();
2088 let soldeer_toml = test_dir.join("soldeer.toml");
2089 fs::write(&soldeer_toml, "[dependencies]\n").unwrap();
2090
2091 let result = find_project_root(Some(&test_dir));
2092 assert!(result.is_ok(), "{result:?}");
2093 assert_eq!(result.unwrap(), test_dir);
2094 }
2095
2096 #[test]
2097 fn test_find_project_root_in_subdirectory() {
2098 let test_dir = testdir!();
2099 let foundry_toml = test_dir.join("foundry.toml");
2100 fs::write(&foundry_toml, "[dependencies]\n").unwrap();
2101
2102 let sub_dir = test_dir.join("src");
2103 fs::create_dir(&sub_dir).unwrap();
2104
2105 let result = find_project_root(Some(&sub_dir));
2106 assert!(result.is_ok(), "{result:?}");
2107 assert_eq!(result.unwrap(), test_dir);
2108 }
2109
2110 #[test]
2111 fn test_find_project_root_git_boundary() {
2112 let test_dir = testdir!();
2113 let git_folder = test_dir.join(".git");
2114 fs::create_dir(&git_folder).unwrap();
2115
2116 let sub_dir = test_dir.join("src");
2117 fs::create_dir(&sub_dir).unwrap();
2118
2119 let result = find_project_root(Some(&sub_dir));
2120 assert!(result.is_ok(), "{result:?}");
2121 assert_eq!(result.unwrap(), test_dir);
2122 }
2123}