forc_pkg/manifest/
mod.rs

1pub mod build_profile;
2
3use crate::pkg::{manifest_file_missing, parsing_failed, wrong_program_type};
4use anyhow::{anyhow, bail, Context, Result};
5use forc_tracing::println_warning;
6use forc_util::{validate_name, validate_project_name};
7use semver::Version;
8use serde::{de, Deserialize, Serialize};
9use serde_with::{serde_as, DisplayFromStr};
10use std::{
11    collections::{BTreeMap, HashMap},
12    fmt::Display,
13    path::{Path, PathBuf},
14    str::FromStr,
15    sync::Arc,
16};
17use sway_core::{fuel_prelude::fuel_tx, language::parsed::TreeType, parse_tree_type, BuildTarget};
18use sway_error::handler::Handler;
19use sway_utils::{
20    constants, find_nested_manifest_dir, find_parent_manifest_dir,
21    find_parent_manifest_dir_with_check,
22};
23use url::Url;
24
25use self::build_profile::BuildProfile;
26
27/// The name of a workspace member package.
28pub type MemberName = String;
29/// A manifest for each workspace member, or just one manifest if working with a single package
30pub type MemberManifestFiles = BTreeMap<MemberName, PackageManifestFile>;
31
32pub trait GenericManifestFile {
33    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self>
34    where
35        Self: Sized;
36    fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self>
37    where
38        Self: Sized;
39
40    /// The path to the `Forc.toml` from which this manifest was loaded.
41    ///
42    /// This will always be a canonical path.
43    fn path(&self) -> &Path;
44
45    /// The path to the directory containing the `Forc.toml` from which this manifest was loaded.
46    ///
47    /// This will always be a canonical path.
48    fn dir(&self) -> &Path {
49        self.path()
50            .parent()
51            .expect("failed to retrieve manifest directory")
52    }
53
54    /// Returns the path of the `Forc.lock` file.
55    fn lock_path(&self) -> Result<PathBuf>;
56
57    /// Returns a mapping of member member names to package manifest files.
58    fn member_manifests(&self) -> Result<MemberManifestFiles>;
59}
60
61#[derive(Clone, Debug)]
62pub enum ManifestFile {
63    Package(Box<PackageManifestFile>),
64    Workspace(WorkspaceManifestFile),
65}
66
67impl GenericManifestFile for ManifestFile {
68    /// Returns a `PackageManifestFile` if the path is within a package directory, otherwise
69    /// returns a `WorkspaceManifestFile` if within a workspace directory.
70    fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self> {
71        let maybe_pkg_manifest = PackageManifestFile::from_dir(path.as_ref());
72        let manifest_file = if let Err(e) = maybe_pkg_manifest {
73            if e.to_string().contains("missing field `project`") {
74                // This might be a workspace manifest file
75                let workspace_manifest_file = WorkspaceManifestFile::from_dir(path.as_ref())?;
76                ManifestFile::Workspace(workspace_manifest_file)
77            } else {
78                bail!("{}", e)
79            }
80        } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
81            ManifestFile::Package(Box::new(pkg_manifest))
82        } else {
83            bail!(
84                "Cannot find a valid `Forc.toml` at {}",
85                path.as_ref().to_string_lossy()
86            )
87        };
88        Ok(manifest_file)
89    }
90
91    /// Returns a `PackageManifestFile` if the path is pointing to package manifest, otherwise
92    /// returns a `WorkspaceManifestFile` if it is pointing to a workspace manifest.
93    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
94        let maybe_pkg_manifest = PackageManifestFile::from_file(path.as_ref());
95        let manifest_file = if let Err(e) = maybe_pkg_manifest {
96            if e.to_string().contains("missing field `project`") {
97                // This might be a workspace manifest file
98                let workspace_manifest_file = WorkspaceManifestFile::from_file(path.as_ref())?;
99                ManifestFile::Workspace(workspace_manifest_file)
100            } else {
101                bail!("{}", e)
102            }
103        } else if let Ok(pkg_manifest) = maybe_pkg_manifest {
104            ManifestFile::Package(Box::new(pkg_manifest))
105        } else {
106            bail!(
107                "Cannot find a valid `Forc.toml` at {}",
108                path.as_ref().to_string_lossy()
109            )
110        };
111        Ok(manifest_file)
112    }
113
114    /// The path to the `Forc.toml` from which this manifest was loaded.
115    ///
116    /// This will always be a canonical path.
117    fn path(&self) -> &Path {
118        match self {
119            ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.path(),
120            ManifestFile::Workspace(workspace_manifest_file) => workspace_manifest_file.path(),
121        }
122    }
123
124    fn member_manifests(&self) -> Result<MemberManifestFiles> {
125        match self {
126            ManifestFile::Package(pkg_manifest_file) => pkg_manifest_file.member_manifests(),
127            ManifestFile::Workspace(workspace_manifest_file) => {
128                workspace_manifest_file.member_manifests()
129            }
130        }
131    }
132
133    /// Returns the path of the lock file for the given ManifestFile
134    fn lock_path(&self) -> Result<PathBuf> {
135        match self {
136            ManifestFile::Package(pkg_manifest) => pkg_manifest.lock_path(),
137            ManifestFile::Workspace(workspace_manifest) => workspace_manifest.lock_path(),
138        }
139    }
140}
141
142impl TryInto<PackageManifestFile> for ManifestFile {
143    type Error = anyhow::Error;
144
145    fn try_into(self) -> Result<PackageManifestFile> {
146        match self {
147            ManifestFile::Package(pkg_manifest_file) => Ok(*pkg_manifest_file),
148            ManifestFile::Workspace(_) => {
149                bail!("Cannot convert workspace manifest to package manifest")
150            }
151        }
152    }
153}
154
155impl TryInto<WorkspaceManifestFile> for ManifestFile {
156    type Error = anyhow::Error;
157
158    fn try_into(self) -> Result<WorkspaceManifestFile> {
159        match self {
160            ManifestFile::Package(_) => {
161                bail!("Cannot convert package manifest to workspace manifest")
162            }
163            ManifestFile::Workspace(workspace_manifest_file) => Ok(workspace_manifest_file),
164        }
165    }
166}
167
168type PatchMap = BTreeMap<String, Dependency>;
169
170/// A [PackageManifest] that was deserialized from a file at a particular path.
171#[derive(Clone, Debug, PartialEq)]
172pub struct PackageManifestFile {
173    /// The deserialized `Forc.toml`.
174    manifest: PackageManifest,
175    /// The path from which the `Forc.toml` file was read.
176    path: PathBuf,
177}
178
179/// A direct mapping to a `Forc.toml`.
180#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
181#[serde(rename_all = "kebab-case")]
182pub struct PackageManifest {
183    pub project: Project,
184    pub network: Option<Network>,
185    pub dependencies: Option<BTreeMap<String, Dependency>>,
186    pub patch: Option<BTreeMap<String, PatchMap>>,
187    /// A list of [configuration-time constants](https://github.com/FuelLabs/sway/issues/1498).
188    pub build_target: Option<BTreeMap<String, BuildTarget>>,
189    build_profile: Option<BTreeMap<String, BuildProfile>>,
190    pub contract_dependencies: Option<BTreeMap<String, ContractDependency>>,
191    pub proxy: Option<Proxy>,
192}
193
194#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
195#[serde(rename_all = "kebab-case")]
196pub struct Project {
197    pub authors: Option<Vec<String>>,
198    #[serde(deserialize_with = "validate_package_name")]
199    pub name: String,
200    pub version: Option<Version>,
201    pub description: Option<String>,
202    pub organization: Option<String>,
203    pub license: String,
204    pub homepage: Option<Url>,
205    pub repository: Option<Url>,
206    pub documentation: Option<Url>,
207    pub categories: Option<Vec<String>>,
208    pub keywords: Option<Vec<String>>,
209    #[serde(default = "default_entry")]
210    pub entry: String,
211    pub implicit_std: Option<bool>,
212    pub forc_version: Option<semver::Version>,
213    #[serde(default)]
214    pub experimental: HashMap<String, bool>,
215    pub metadata: Option<toml::Value>,
216}
217
218// Validation function for the `name` field
219fn validate_package_name<'de, D>(deserializer: D) -> Result<String, D::Error>
220where
221    D: de::Deserializer<'de>,
222{
223    let name: String = Deserialize::deserialize(deserializer)?;
224    match validate_project_name(&name) {
225        Ok(_) => Ok(name),
226        Err(e) => Err(de::Error::custom(e.to_string())),
227    }
228}
229
230#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
231#[serde(rename_all = "kebab-case")]
232pub struct Network {
233    #[serde(default = "default_url")]
234    pub url: String,
235}
236
237#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
238pub struct HexSalt(pub fuel_tx::Salt);
239
240impl FromStr for HexSalt {
241    type Err = anyhow::Error;
242
243    fn from_str(s: &str) -> Result<Self, Self::Err> {
244        // cut 0x from start.
245        let normalized = s
246            .strip_prefix("0x")
247            .ok_or_else(|| anyhow::anyhow!("hex salt declaration needs to start with 0x"))?;
248        let salt: fuel_tx::Salt =
249            fuel_tx::Salt::from_str(normalized).map_err(|e| anyhow::anyhow!("{e}"))?;
250        let hex_salt = Self(salt);
251        Ok(hex_salt)
252    }
253}
254
255impl Display for HexSalt {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        let salt = self.0;
258        write!(f, "{}", salt)
259    }
260}
261
262fn default_hex_salt() -> HexSalt {
263    HexSalt(fuel_tx::Salt::default())
264}
265
266#[serde_as]
267#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
268#[serde(rename_all = "kebab-case")]
269pub struct ContractDependency {
270    #[serde(flatten)]
271    pub dependency: Dependency,
272    #[serde_as(as = "DisplayFromStr")]
273    #[serde(default = "default_hex_salt")]
274    pub salt: HexSalt,
275}
276
277#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
278#[serde(untagged)]
279pub enum Dependency {
280    /// In the simple format, only a version is specified, eg.
281    /// `package = "<version>"`
282    Simple(String),
283    /// The simple format is equivalent to a detailed dependency
284    /// specifying only a version, eg.
285    /// `package = { version = "<version>" }`
286    Detailed(DependencyDetails),
287}
288
289#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
290#[serde(rename_all = "kebab-case")]
291pub struct DependencyDetails {
292    pub(crate) version: Option<String>,
293    pub path: Option<String>,
294    pub(crate) git: Option<String>,
295    pub(crate) branch: Option<String>,
296    pub(crate) tag: Option<String>,
297    pub(crate) package: Option<String>,
298    pub(crate) rev: Option<String>,
299    pub(crate) ipfs: Option<String>,
300}
301
302/// Describes the details around proxy contract.
303#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
304#[serde(rename_all = "kebab-case")]
305pub struct Proxy {
306    pub enabled: bool,
307    /// Points to the proxy contract to be updated with the new contract id.
308    /// If there is a value for this field, forc will try to update the proxy contract's storage
309    /// field such that it points to current contract's deployed instance.
310    pub address: Option<String>,
311}
312
313impl DependencyDetails {
314    /// Checks if dependency details reserved for a specific dependency type used without the main
315    /// detail for that type.
316    ///
317    /// Following dependency details sets are considered to be invalid:
318    /// 1. A set of dependency details which declares `branch`, `tag` or `rev` without `git`.
319    pub fn validate(&self) -> anyhow::Result<()> {
320        let DependencyDetails {
321            git,
322            branch,
323            tag,
324            rev,
325            ..
326        } = self;
327
328        if git.is_none() && (branch.is_some() || tag.is_some() || rev.is_some()) {
329            bail!("Details reserved for git sources used without a git field");
330        }
331
332        Ok(())
333    }
334}
335
336impl Dependency {
337    /// The string of the `package` field if specified.
338    pub fn package(&self) -> Option<&str> {
339        match *self {
340            Self::Simple(_) => None,
341            Self::Detailed(ref det) => det.package.as_deref(),
342        }
343    }
344
345    /// The string of the `version` field if specified.
346    pub fn version(&self) -> Option<&str> {
347        match *self {
348            Self::Simple(ref version) => Some(version),
349            Self::Detailed(ref det) => det.version.as_deref(),
350        }
351    }
352}
353
354impl PackageManifestFile {
355    /// Returns an iterator over patches defined in underlying `PackageManifest` if this is a
356    /// standalone package.
357    ///
358    /// If this package is a member of a workspace, patches are fetched from
359    /// the workspace manifest file, ignoring any patch defined in the package
360    /// manifest file, even if a patch section is not defined in the namespace.
361    fn resolve_patches(&self) -> Result<impl Iterator<Item = (String, PatchMap)>> {
362        if let Some(workspace) = self.workspace().ok().flatten() {
363            // If workspace is defined, passing a local patch is a warning, but the global patch is used
364            if self.patch.is_some() {
365                println_warning("Patch for the non root package will be ignored.");
366                println_warning(&format!(
367                    "Specify patch at the workspace root: {}",
368                    workspace.path().to_str().unwrap_or_default()
369                ));
370            }
371            Ok(workspace
372                .patch
373                .as_ref()
374                .cloned()
375                .unwrap_or_default()
376                .into_iter())
377        } else {
378            Ok(self.patch.as_ref().cloned().unwrap_or_default().into_iter())
379        }
380    }
381
382    /// Retrieve the listed patches for the given name from underlying `PackageManifest` if this is
383    /// a standalone package.
384    ///
385    /// If this package is a member of a workspace, patch is fetched from
386    /// the workspace manifest file.
387    pub fn resolve_patch(&self, patch_name: &str) -> Result<Option<PatchMap>> {
388        Ok(self
389            .resolve_patches()?
390            .find(|(p_name, _)| patch_name == p_name.as_str())
391            .map(|(_, patch)| patch))
392    }
393
394    /// Given the directory in which the file associated with this `PackageManifest` resides, produce the
395    /// path to the entry file as specified in the manifest.
396    ///
397    /// This will always be a canonical path.
398    pub fn entry_path(&self) -> PathBuf {
399        self.dir()
400            .join(constants::SRC_DIR)
401            .join(&self.project.entry)
402    }
403
404    /// Produces the string of the entry point file.
405    pub fn entry_string(&self) -> Result<Arc<str>> {
406        let entry_path = self.entry_path();
407        let entry_string = std::fs::read_to_string(entry_path)?;
408        Ok(Arc::from(entry_string))
409    }
410
411    /// Parse and return the associated project's program type.
412    pub fn program_type(&self) -> Result<TreeType> {
413        let entry_string = self.entry_string()?;
414        let handler = Handler::default();
415        let parse_res = parse_tree_type(&handler, entry_string);
416
417        parse_res.map_err(|_| {
418            let (errors, _warnings) = handler.consume();
419            parsing_failed(&self.project.name, &errors)
420        })
421    }
422
423    /// Given the current directory and expected program type,
424    /// determines whether the correct program type is present.
425    pub fn check_program_type(&self, expected_types: &[TreeType]) -> Result<()> {
426        let parsed_type = self.program_type()?;
427        if !expected_types.contains(&parsed_type) {
428            bail!(wrong_program_type(
429                &self.project.name,
430                expected_types,
431                parsed_type
432            ));
433        } else {
434            Ok(())
435        }
436    }
437
438    /// Access the build profile associated with the given profile name.
439    pub fn build_profile(&self, profile_name: &str) -> Option<&BuildProfile> {
440        self.build_profile
441            .as_ref()
442            .and_then(|profiles| profiles.get(profile_name))
443    }
444
445    /// Given the name of a `path` dependency, returns the full canonical `Path` to the dependency.
446    pub fn dep_path(&self, dep_name: &str) -> Option<PathBuf> {
447        let dir = self.dir();
448        let details = self.dep_detailed(dep_name)?;
449        details.path.as_ref().and_then(|path_str| {
450            let path = Path::new(path_str);
451            match path.is_absolute() {
452                true => Some(path.to_owned()),
453                false => dir.join(path).canonicalize().ok(),
454            }
455        })
456    }
457
458    /// Returns the workspace manifest file if this `PackageManifestFile` is one of the members.
459    pub fn workspace(&self) -> Result<Option<WorkspaceManifestFile>> {
460        let parent_dir = match self.dir().parent() {
461            None => return Ok(None),
462            Some(dir) => dir,
463        };
464        let ws_manifest = match WorkspaceManifestFile::from_dir(parent_dir) {
465            Ok(manifest) => manifest,
466            Err(e) => {
467                // Check if the error is missing workspace manifest file. Do not return that error if that
468                // is the case as we do not want to return error if this is a single project
469                // without a workspace.
470                if e.to_string().contains("could not find") {
471                    return Ok(None);
472                } else {
473                    return Err(e);
474                }
475            }
476        };
477        if ws_manifest.is_member_path(self.dir())? {
478            Ok(Some(ws_manifest))
479        } else {
480            Ok(None)
481        }
482    }
483
484    /// Returns an immutable reference to the project name that this manifest file describes.
485    pub fn project_name(&self) -> &str {
486        &self.project.name
487    }
488
489    /// Validate the `PackageManifestFile`.
490    ///
491    /// This checks:
492    /// 1. Validity of the underlying `PackageManifest`.
493    /// 2. Existence of the entry file.
494    pub fn validate(&self) -> Result<()> {
495        self.manifest.validate()?;
496        let mut entry_path = self.path.clone();
497        entry_path.pop();
498        let entry_path = entry_path
499            .join(constants::SRC_DIR)
500            .join(&self.project.entry);
501        if !entry_path.exists() {
502            bail!(
503                "failed to validate path from entry field {:?} in Forc manifest file.",
504                self.project.entry
505            )
506        }
507
508        // Check for nested packages.
509        //
510        // `path` is the path to manifest file. To start nested package search we need to start
511        // from manifest's directory. So, last part of the path (the filename, "/forc.toml") needs
512        // to be removed.
513        let mut pkg_dir = self.path.to_path_buf();
514        pkg_dir.pop();
515        if let Some(nested_package) = find_nested_manifest_dir(&pkg_dir) {
516            // remove file name from nested_package_manifest
517            bail!("Nested packages are not supported, please consider separating the nested package at {} from the package at {}, or if it makes sense consider creating a workspace.", nested_package.display(), pkg_dir.display())
518        }
519        Ok(())
520    }
521}
522
523impl GenericManifestFile for PackageManifestFile {
524    /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`.
525    ///
526    /// This also `validate`s the manifest, returning an `Err` in the case that invalid names,
527    /// fields were used.
528    ///
529    /// If `std` is unspecified, `std` will be added to the `dependencies` table
530    /// implicitly. In this case, the git tag associated with the version of this crate is used to
531    /// specify the pinned commit at which we fetch `std`.
532    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
533        let path = path.as_ref().canonicalize()?;
534        let manifest = PackageManifest::from_file(&path)?;
535        let manifest_file = Self { manifest, path };
536        manifest_file.validate()?;
537        Ok(manifest_file)
538    }
539
540    /// Read the manifest from the `Forc.toml` in the directory specified by the given `path` or
541    /// any of its parent directories.
542    ///
543    /// This is short for `PackageManifest::from_file`, but takes care of constructing the path to the
544    /// file.
545    fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
546        let manifest_dir = manifest_dir.as_ref();
547        let dir = find_parent_manifest_dir(manifest_dir)
548            .ok_or_else(|| manifest_file_missing(manifest_dir))?;
549        let path = dir.join(constants::MANIFEST_FILE_NAME);
550        Self::from_file(path)
551    }
552
553    fn path(&self) -> &Path {
554        &self.path
555    }
556
557    /// Returns the location of the lock file for `PackageManifestFile`.
558    /// Checks if this PackageManifestFile corresponds to a workspace member and if that is the case
559    /// returns the workspace level lock file's location.
560    ///
561    /// This will always be a canonical path.
562    fn lock_path(&self) -> Result<PathBuf> {
563        // Check if this package is in a workspace
564        let workspace_manifest = self.workspace()?;
565        if let Some(workspace_manifest) = workspace_manifest {
566            workspace_manifest.lock_path()
567        } else {
568            Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
569        }
570    }
571
572    fn member_manifests(&self) -> Result<MemberManifestFiles> {
573        let mut member_manifest_files = BTreeMap::new();
574        // Check if this package is in a workspace, in that case insert all member manifests
575        if let Some(workspace_manifest_file) = self.workspace()? {
576            for member_manifest in workspace_manifest_file.member_pkg_manifests()? {
577                let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
578                member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
579            }
580        } else {
581            let member_name = &self.project.name;
582            member_manifest_files.insert(member_name.clone(), self.clone());
583        }
584
585        Ok(member_manifest_files)
586    }
587}
588
589impl PackageManifest {
590    pub const DEFAULT_ENTRY_FILE_NAME: &'static str = "main.sw";
591
592    /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`.
593    ///
594    /// This also `validate`s the manifest, returning an `Err` in the case that invalid names,
595    /// fields were used.
596    ///
597    /// If `std` is unspecified, `std` will be added to the `dependencies` table
598    /// implicitly. In this case, the git tag associated with the version of this crate is used to
599    /// specify the pinned commit at which we fetch `std`.
600    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
601        // While creating a `ManifestFile` we need to check if the given path corresponds to a
602        // package or a workspace. While doing so, we should be printing the warnings if the given
603        // file parses so that we only see warnings for the correct type of manifest.
604        let path = path.as_ref();
605        let contents = std::fs::read_to_string(path)
606            .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
607        Self::from_string(contents)
608    }
609
610    /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`.
611    ///
612    /// This also `validate`s the manifest, returning an `Err` in the case that invalid names,
613    /// fields were used.
614    ///
615    /// If `std` is unspecified, `std` will be added to the `dependencies` table
616    /// implicitly. In this case, the git tag associated with the version of this crate is used to
617    /// specify the pinned commit at which we fetch `std`.
618    pub fn from_string(contents: String) -> Result<Self> {
619        // While creating a `ManifestFile` we need to check if the given path corresponds to a
620        // package or a workspace. While doing so, we should be printing the warnings if the given
621        // file parses so that we only see warnings for the correct type of manifest.
622        let mut warnings = vec![];
623        let toml_de = toml::de::Deserializer::new(&contents);
624        let mut manifest: Self = serde_ignored::deserialize(toml_de, |path| {
625            let warning = format!("unused manifest key: {path}");
626            warnings.push(warning);
627        })
628        .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
629        for warning in warnings {
630            println_warning(&warning);
631        }
632        manifest.implicitly_include_std_if_missing();
633        manifest.implicitly_include_default_build_profiles_if_missing();
634        manifest.validate()?;
635        Ok(manifest)
636    }
637
638    /// Validate the `PackageManifest`.
639    ///
640    /// This checks:
641    /// 1. The project and organization names against a set of reserved/restricted keywords and patterns.
642    /// 2. The validity of the details provided. Makes sure that there are no mismatching detail
643    ///    declarations (to prevent mixing details specific to certain types).
644    /// 3. The dependencies listed does not have an alias ("package" field) that is the same as package name.
645    pub fn validate(&self) -> Result<()> {
646        validate_project_name(&self.project.name)?;
647        if let Some(ref org) = self.project.organization {
648            validate_name(org, "organization name")?;
649        }
650        for (dep_name, dependency_details) in self.deps_detailed() {
651            dependency_details.validate()?;
652            if dependency_details
653                .package
654                .as_ref()
655                .is_some_and(|package_alias| package_alias == &self.project.name)
656            {
657                bail!(format!("Dependency \"{dep_name}\" declares an alias (\"package\" field) that is the same as project name"))
658            }
659            if dep_name == &self.project.name {
660                bail!(format!(
661                    "Dependency \"{dep_name}\" collides with project name."
662                ))
663            }
664        }
665        Ok(())
666    }
667
668    /// Given a directory to a forc project containing a `Forc.toml`, read the manifest.
669    ///
670    /// This is short for `PackageManifest::from_file`, but takes care of constructing the path to the
671    /// file.
672    pub fn from_dir<P: AsRef<Path>>(dir: P) -> Result<Self> {
673        let dir = dir.as_ref();
674        let manifest_dir =
675            find_parent_manifest_dir(dir).ok_or_else(|| manifest_file_missing(dir))?;
676        let file_path = manifest_dir.join(constants::MANIFEST_FILE_NAME);
677        Self::from_file(file_path)
678    }
679
680    /// Produce an iterator yielding all listed dependencies.
681    pub fn deps(&self) -> impl Iterator<Item = (&String, &Dependency)> {
682        self.dependencies
683            .as_ref()
684            .into_iter()
685            .flat_map(|deps| deps.iter())
686    }
687
688    /// Produce an iterator yielding all listed build profiles.
689    pub fn build_profiles(&self) -> impl Iterator<Item = (&String, &BuildProfile)> {
690        self.build_profile
691            .as_ref()
692            .into_iter()
693            .flat_map(|deps| deps.iter())
694    }
695
696    /// Produce an iterator yielding all listed contract dependencies
697    pub fn contract_deps(&self) -> impl Iterator<Item = (&String, &ContractDependency)> {
698        self.contract_dependencies
699            .as_ref()
700            .into_iter()
701            .flat_map(|deps| deps.iter())
702    }
703
704    /// Produce an iterator yielding all `Detailed` dependencies.
705    pub fn deps_detailed(&self) -> impl Iterator<Item = (&String, &DependencyDetails)> {
706        self.deps().filter_map(|(name, dep)| match dep {
707            Dependency::Detailed(ref det) => Some((name, det)),
708            Dependency::Simple(_) => None,
709        })
710    }
711
712    /// Produce an iterator yielding all listed patches.
713    pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
714        self.patch
715            .as_ref()
716            .into_iter()
717            .flat_map(|patches| patches.iter())
718    }
719
720    /// Retrieve the listed patches for the given name.
721    pub fn patch(&self, patch_name: &str) -> Option<&PatchMap> {
722        self.patch
723            .as_ref()
724            .and_then(|patches| patches.get(patch_name))
725    }
726
727    /// Retrieve the proxy table for the package.
728    pub fn proxy(&self) -> Option<&Proxy> {
729        self.proxy.as_ref()
730    }
731
732    /// Check for the `std` package under `[dependencies]`. If it is missing, add
733    /// `std` implicitly.
734    ///
735    /// This makes the common case of depending on `std` a lot smoother for most users, while still
736    /// allowing for the uncommon case of custom `std` deps.
737    fn implicitly_include_std_if_missing(&mut self) {
738        use sway_types::constants::STD;
739        // Don't include `std` if:
740        // - this *is* `std`.
741        // - `std` package is already specified.
742        // - a dependency already exists with the name "std".
743        if self.project.name == STD
744            || self.pkg_dep(STD).is_some()
745            || self.dep(STD).is_some()
746            || !self.project.implicit_std.unwrap_or(true)
747        {
748            return;
749        }
750        // Add a `[dependencies]` table if there isn't one.
751        let deps = self.dependencies.get_or_insert_with(Default::default);
752        // Add the missing dependency.
753        let std_dep = implicit_std_dep();
754        deps.insert(STD.to_string(), std_dep);
755    }
756
757    /// Check for the `debug` and `release` packages under `[build-profile]`. If they are missing add them.
758    /// If they are provided, use the provided `debug` or `release` so that they override the default `debug`
759    /// and `release`.
760    fn implicitly_include_default_build_profiles_if_missing(&mut self) {
761        let build_profiles = self.build_profile.get_or_insert_with(Default::default);
762
763        if build_profiles.get(BuildProfile::DEBUG).is_none() {
764            build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
765        }
766        if build_profiles.get(BuildProfile::RELEASE).is_none() {
767            build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
768        }
769    }
770
771    /// Retrieve a reference to the dependency with the given name.
772    pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
773        self.dependencies
774            .as_ref()
775            .and_then(|deps| deps.get(dep_name))
776    }
777
778    /// Retrieve a reference to the dependency with the given name.
779    pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
780        self.dep(dep_name).and_then(|dep| match dep {
781            Dependency::Simple(_) => None,
782            Dependency::Detailed(detailed) => Some(detailed),
783        })
784    }
785
786    /// Retrieve a reference to the contract dependency with the given name.
787    pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
788        self.contract_dependencies
789            .as_ref()
790            .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
791    }
792
793    /// Retrieve a reference to the contract dependency with the given name.
794    pub fn contract_dependency_detailed(
795        &self,
796        contract_dep_name: &str,
797    ) -> Option<&DependencyDetails> {
798        self.contract_dep(contract_dep_name)
799            .and_then(|contract_dep| match &contract_dep.dependency {
800                Dependency::Simple(_) => None,
801                Dependency::Detailed(detailed) => Some(detailed),
802            })
803    }
804
805    /// Finds and returns the name of the dependency associated with a package of the specified
806    /// name if there is one.
807    ///
808    /// Returns `None` in the case that no dependencies associate with a package of the given name.
809    fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
810        for (dep_name, dep) in self.deps() {
811            if dep.package().unwrap_or(dep_name) == pkg_name {
812                return Some(dep_name);
813            }
814        }
815        None
816    }
817}
818
819impl std::ops::Deref for PackageManifestFile {
820    type Target = PackageManifest;
821    fn deref(&self) -> &Self::Target {
822        &self.manifest
823    }
824}
825
826/// The definition for the implicit `std` dependency.
827///
828/// This can be configured using environment variables:
829/// - use `FORC_IMPLICIT_STD_PATH` for the path for the std-lib;
830/// - use `FORC_IMPLICIT_STD_GIT`, `FORC_IMPLICIT_STD_GIT_TAG` and/or `FORC_IMPLICIT_STD_GIT_BRANCH` to configure
831///   the git repo of the std-lib.
832fn implicit_std_dep() -> Dependency {
833    if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
834        return Dependency::Detailed(DependencyDetails {
835            path: Some(path),
836            ..Default::default()
837        });
838    }
839
840    // Here, we use the `forc-pkg` crate version formatted with the `v` prefix (e.g. "v1.2.3"),
841    // or the revision commit hash (e.g. "abcdefg").
842    //
843    // This git tag or revision is used during `PackageManifest` construction to pin the version of the
844    // implicit `std` dependency to the `forc-pkg` version.
845    //
846    // This is important to ensure that the version of `sway-core` that is baked into `forc-pkg` is
847    // compatible with the version of the `std` lib.
848    let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG")
849        .ok()
850        .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
851    const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
852
853    // only use tag/rev if the branch is None
854    let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
855    let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
856
857    let mut det = DependencyDetails {
858        git: std::env::var("FORC_IMPLICIT_STD_GIT")
859            .ok()
860            .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
861        tag,
862        branch,
863        ..Default::default()
864    };
865
866    if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
867        // Nightlies are in the format v<version>+nightly.<date>.<hash>
868        let rev = build_metadata.split('.').last().map(|r| r.to_string());
869
870        // If some revision is available and parsed from the 'nightly' build metadata,
871        // we always prefer the revision over the tag.
872        det.tag = None;
873        det.rev = rev;
874    };
875
876    Dependency::Detailed(det)
877}
878
879fn default_entry() -> String {
880    PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
881}
882
883fn default_url() -> String {
884    constants::DEFAULT_NODE_URL.into()
885}
886
887/// A [WorkspaceManifest] that was deserialized from a file at a particular path.
888#[derive(Clone, Debug)]
889pub struct WorkspaceManifestFile {
890    /// The derserialized `Forc.toml`
891    manifest: WorkspaceManifest,
892    /// The path from which the `Forc.toml` file was read.
893    path: PathBuf,
894}
895
896/// A direct mapping to `Forc.toml` if it is a WorkspaceManifest
897#[derive(Serialize, Deserialize, Clone, Debug)]
898#[serde(rename_all = "kebab-case")]
899pub struct WorkspaceManifest {
900    workspace: Workspace,
901    patch: Option<BTreeMap<String, PatchMap>>,
902}
903
904#[derive(Serialize, Deserialize, Clone, Debug)]
905#[serde(rename_all = "kebab-case")]
906pub struct Workspace {
907    pub members: Vec<PathBuf>,
908    pub metadata: Option<toml::Value>,
909}
910
911impl WorkspaceManifestFile {
912    /// Produce an iterator yielding all listed patches.
913    pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
914        self.patch
915            .as_ref()
916            .into_iter()
917            .flat_map(|patches| patches.iter())
918    }
919
920    /// Returns an iterator over relative paths of workspace members.
921    pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
922        self.workspace.members.iter()
923    }
924
925    /// Returns an iterator over workspace member root directories.
926    ///
927    /// This will always return canonical paths.
928    pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
929        Ok(self
930            .workspace
931            .members
932            .iter()
933            .map(|member| self.dir().join(member)))
934    }
935
936    /// Returns an iterator over workspace member package manifests.
937    pub fn member_pkg_manifests(
938        &self,
939    ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
940        let member_paths = self.member_paths()?;
941        let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
942        Ok(member_pkg_manifests)
943    }
944
945    /// Check if given path corresponds to any workspace member's path
946    pub fn is_member_path(&self, path: &Path) -> Result<bool> {
947        Ok(self.member_paths()?.any(|member_path| member_path == path))
948    }
949}
950
951impl GenericManifestFile for WorkspaceManifestFile {
952    /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`
953    ///
954    /// This also `validate`s the manifest, returning an `Err` in the case that given members are
955    /// not present in the manifest dir.
956    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
957        let path = path.as_ref().canonicalize()?;
958        let parent = path
959            .parent()
960            .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
961        let manifest = WorkspaceManifest::from_file(&path)?;
962        manifest.validate(parent)?;
963        Ok(Self { manifest, path })
964    }
965
966    /// Read the manifest from the `Forc.toml` in the directory specified by the given `path` or
967    /// any of its parent directories.
968    ///
969    /// This is short for `PackageManifest::from_file`, but takes care of constructing the path to the
970    /// file.
971    fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
972        let manifest_dir = manifest_dir.as_ref();
973        let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
974            // Check if the found manifest file is a workspace manifest file or a standalone
975            // package manifest file.
976            let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
977            // We should not continue to search if the given manifest is a workspace manifest with
978            // some issues.
979            //
980            // If the error is missing field `workspace` (which happens when trying to read a
981            // package manifest as a workspace manifest), look into the parent directories for a
982            // legitimate workspace manifest. If the error returned is something else this is a
983            // workspace manifest with errors, classify this as a workspace manifest but with
984            // errors so that the errors will be displayed to the user.
985            Self::from_file(possible_path)
986                .err()
987                .map(|e| !e.to_string().contains("missing field `workspace`"))
988                .unwrap_or_else(|| true)
989        })
990        .ok_or_else(|| manifest_file_missing(manifest_dir))?;
991        let path = dir.join(constants::MANIFEST_FILE_NAME);
992        Self::from_file(path)
993    }
994
995    fn path(&self) -> &Path {
996        &self.path
997    }
998
999    /// Returns the location of the lock file for `WorkspaceManifestFile`.
1000    ///
1001    /// This will always be a canonical path.
1002    fn lock_path(&self) -> Result<PathBuf> {
1003        Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1004    }
1005
1006    fn member_manifests(&self) -> Result<MemberManifestFiles> {
1007        let mut member_manifest_files = BTreeMap::new();
1008        for member_manifest in self.member_pkg_manifests()? {
1009            let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1010            member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1011        }
1012
1013        Ok(member_manifest_files)
1014    }
1015}
1016
1017impl WorkspaceManifest {
1018    /// Given a path to a `Forc.toml`, read it and construct a `WorkspaceManifest`.
1019    pub fn from_file(path: &Path) -> Result<Self> {
1020        // While creating a `ManifestFile` we need to check if the given path corresponds to a
1021        // package or a workspace. While doing so, we should be printing the warnings if the given
1022        // file parses so that we only see warnings for the correct type of manifest.
1023        let mut warnings = vec![];
1024        let manifest_str = std::fs::read_to_string(path)
1025            .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1026        let toml_de = toml::de::Deserializer::new(&manifest_str);
1027        let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1028            let warning = format!("unused manifest key: {path}");
1029            warnings.push(warning);
1030        })
1031        .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1032        for warning in warnings {
1033            println_warning(&warning);
1034        }
1035        Ok(manifest)
1036    }
1037
1038    /// Validate the `WorkspaceManifest`
1039    ///
1040    /// This checks if the listed members in the `WorkspaceManifest` are indeed in the given `Forc.toml`'s directory.
1041    pub fn validate(&self, path: &Path) -> Result<()> {
1042        let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1043        for member in &self.workspace.members {
1044            let member_path = path.join(member).join("Forc.toml");
1045            if !member_path.exists() {
1046                bail!(
1047                    "{:?} is listed as a member of the workspace but {:?} does not exists",
1048                    &member,
1049                    member_path
1050                );
1051            }
1052            if Self::from_file(&member_path).is_ok() {
1053                bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1054            };
1055
1056            let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1057            let pkg_name = member_manifest_file.manifest.project.name;
1058            pkg_name_to_paths
1059                .entry(pkg_name)
1060                .or_default()
1061                .push(member_path);
1062        }
1063
1064        // Check for duplicate pkg name entries in member manifests of this workspace.
1065        let duplicate_pkg_lines = pkg_name_to_paths
1066            .iter()
1067            .filter_map(|(pkg_name, paths)| {
1068                if paths.len() > 1 {
1069                    let duplicate_paths = pkg_name_to_paths
1070                        .get(pkg_name)
1071                        .expect("missing duplicate paths");
1072                    Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1073                } else {
1074                    None
1075                }
1076            })
1077            .collect::<Vec<_>>();
1078
1079        if !duplicate_pkg_lines.is_empty() {
1080            let error_message = duplicate_pkg_lines.join("\n");
1081            bail!(
1082                "Duplicate package names detected in the workspace:\n\n{}",
1083                error_message
1084            );
1085        }
1086        Ok(())
1087    }
1088}
1089
1090impl std::ops::Deref for WorkspaceManifestFile {
1091    type Target = WorkspaceManifest;
1092    fn deref(&self) -> &Self::Target {
1093        &self.manifest
1094    }
1095}
1096
1097/// Attempt to find a `Forc.toml` with the given project name within the given directory.
1098///
1099/// Returns the path to the package on success, or `None` in the case it could not be found.
1100pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1101    use sway_types::constants::STD;
1102    const SWAY_STD_FOLDER: &str = "sway-lib-std";
1103    walkdir::WalkDir::new(dir)
1104        .into_iter()
1105        .filter_map(|entry| {
1106            entry
1107                .ok()
1108                .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1109        })
1110        .find_map(|entry| {
1111            let path = entry.path();
1112            let manifest = PackageManifest::from_file(path).ok()?;
1113            // If the package is STD, make sure it is coming from correct folder.
1114            if (manifest.project.name == pkg_name && pkg_name != STD)
1115                || (manifest.project.name == STD
1116                    && path
1117                        .components()
1118                        .any(|comp| comp.as_os_str() == SWAY_STD_FOLDER))
1119            {
1120                Some(path.to_path_buf())
1121            } else {
1122                None
1123            }
1124        })
1125}
1126
1127/// The same as [find_within], but returns the package's project directory.
1128pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1129    find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use std::str::FromStr;
1135
1136    use super::*;
1137
1138    #[test]
1139    fn deserialize_contract_dependency() {
1140        let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1141
1142        let contract_dep_expected: ContractDependency =
1143            serde_json::from_str(contract_dep_str).unwrap();
1144
1145        let dependency_det = DependencyDetails {
1146            path: Some("../".to_owned()),
1147            ..Default::default()
1148        };
1149        let dependency = Dependency::Detailed(dependency_det);
1150        let contract_dep = ContractDependency {
1151            dependency,
1152            salt: HexSalt::from_str(
1153                "0x1111111111111111111111111111111111111111111111111111111111111111",
1154            )
1155            .unwrap(),
1156        };
1157        assert_eq!(contract_dep, contract_dep_expected)
1158    }
1159    #[test]
1160    fn test_invalid_dependency_details_mixed_together() {
1161        let dependency_details_path_branch = DependencyDetails {
1162            version: None,
1163            path: Some("example_path/".to_string()),
1164            git: None,
1165            branch: Some("test_branch".to_string()),
1166            tag: None,
1167            package: None,
1168            rev: None,
1169            ipfs: None,
1170        };
1171
1172        let dependency_details_branch = DependencyDetails {
1173            path: None,
1174            ..dependency_details_path_branch.clone()
1175        };
1176
1177        let dependency_details_ipfs_branch = DependencyDetails {
1178            path: None,
1179            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1180            ..dependency_details_path_branch.clone()
1181        };
1182
1183        let dependency_details_path_tag = DependencyDetails {
1184            version: None,
1185            path: Some("example_path/".to_string()),
1186            git: None,
1187            branch: None,
1188            tag: Some("v0.1.0".to_string()),
1189            package: None,
1190            rev: None,
1191            ipfs: None,
1192        };
1193
1194        let dependency_details_tag = DependencyDetails {
1195            path: None,
1196            ..dependency_details_path_tag.clone()
1197        };
1198
1199        let dependency_details_ipfs_tag = DependencyDetails {
1200            path: None,
1201            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1202            ..dependency_details_path_branch.clone()
1203        };
1204
1205        let dependency_details_path_rev = DependencyDetails {
1206            version: None,
1207            path: Some("example_path/".to_string()),
1208            git: None,
1209            branch: None,
1210            tag: None,
1211            package: None,
1212            ipfs: None,
1213            rev: Some("9f35b8e".to_string()),
1214        };
1215
1216        let dependency_details_rev = DependencyDetails {
1217            path: None,
1218            ..dependency_details_path_rev.clone()
1219        };
1220
1221        let dependency_details_ipfs_rev = DependencyDetails {
1222            path: None,
1223            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1224            ..dependency_details_path_branch.clone()
1225        };
1226
1227        let expected_mismatch_error = "Details reserved for git sources used without a git field";
1228        assert_eq!(
1229            dependency_details_path_branch
1230                .validate()
1231                .err()
1232                .map(|e| e.to_string()),
1233            Some(expected_mismatch_error.to_string())
1234        );
1235        assert_eq!(
1236            dependency_details_ipfs_branch
1237                .validate()
1238                .err()
1239                .map(|e| e.to_string()),
1240            Some(expected_mismatch_error.to_string())
1241        );
1242        assert_eq!(
1243            dependency_details_path_tag
1244                .validate()
1245                .err()
1246                .map(|e| e.to_string()),
1247            Some(expected_mismatch_error.to_string())
1248        );
1249        assert_eq!(
1250            dependency_details_ipfs_tag
1251                .validate()
1252                .err()
1253                .map(|e| e.to_string()),
1254            Some(expected_mismatch_error.to_string())
1255        );
1256        assert_eq!(
1257            dependency_details_path_rev
1258                .validate()
1259                .err()
1260                .map(|e| e.to_string()),
1261            Some(expected_mismatch_error.to_string())
1262        );
1263        assert_eq!(
1264            dependency_details_ipfs_rev
1265                .validate()
1266                .err()
1267                .map(|e| e.to_string()),
1268            Some(expected_mismatch_error.to_string())
1269        );
1270        assert_eq!(
1271            dependency_details_branch
1272                .validate()
1273                .err()
1274                .map(|e| e.to_string()),
1275            Some(expected_mismatch_error.to_string())
1276        );
1277        assert_eq!(
1278            dependency_details_tag
1279                .validate()
1280                .err()
1281                .map(|e| e.to_string()),
1282            Some(expected_mismatch_error.to_string())
1283        );
1284        assert_eq!(
1285            dependency_details_rev
1286                .validate()
1287                .err()
1288                .map(|e| e.to_string()),
1289            Some(expected_mismatch_error.to_string())
1290        );
1291    }
1292
1293    #[test]
1294    #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1295    fn test_error_duplicate_deps_definition() {
1296        PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1297    }
1298
1299    #[test]
1300    fn test_error_duplicate_deps_definition_in_workspace() {
1301        // Load each project inside a workspace and load their patches
1302        // definition. There should be zero, because the file workspace file has
1303        // no patches
1304        //
1305        // The code also prints a warning to the stdout
1306        let workspace =
1307            WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1308        let projects: Vec<_> = workspace
1309            .member_pkg_manifests()
1310            .unwrap()
1311            .collect::<Result<Vec<_>, _>>()
1312            .unwrap();
1313        assert_eq!(projects.len(), 1);
1314        let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1315        assert_eq!(patches.len(), 0);
1316
1317        // Load the same Forc.toml file but outside of a workspace. There should
1318        // be a single entry in the patch
1319        let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1320            .unwrap()
1321            .resolve_patches()
1322            .unwrap()
1323            .collect();
1324        assert_eq!(patches.len(), 1);
1325    }
1326
1327    #[test]
1328    fn test_valid_dependency_details() {
1329        let dependency_details_path = DependencyDetails {
1330            version: None,
1331            path: Some("example_path/".to_string()),
1332            git: None,
1333            branch: None,
1334            tag: None,
1335            package: None,
1336            rev: None,
1337            ipfs: None,
1338        };
1339
1340        let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1341        let dependency_details_git_tag = DependencyDetails {
1342            version: None,
1343            path: None,
1344            git: Some(git_source_string.clone()),
1345            branch: None,
1346            tag: Some("v0.1.0".to_string()),
1347            package: None,
1348            rev: None,
1349            ipfs: None,
1350        };
1351        let dependency_details_git_branch = DependencyDetails {
1352            version: None,
1353            path: None,
1354            git: Some(git_source_string.clone()),
1355            branch: Some("test_branch".to_string()),
1356            tag: None,
1357            package: None,
1358            rev: None,
1359            ipfs: None,
1360        };
1361        let dependency_details_git_rev = DependencyDetails {
1362            version: None,
1363            path: None,
1364            git: Some(git_source_string),
1365            branch: Some("test_branch".to_string()),
1366            tag: None,
1367            package: None,
1368            rev: Some("9f35b8e".to_string()),
1369            ipfs: None,
1370        };
1371
1372        let dependency_details_ipfs = DependencyDetails {
1373            version: None,
1374            path: None,
1375            git: None,
1376            branch: None,
1377            tag: None,
1378            package: None,
1379            rev: None,
1380            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1381        };
1382
1383        assert!(dependency_details_path.validate().is_ok());
1384        assert!(dependency_details_git_tag.validate().is_ok());
1385        assert!(dependency_details_git_branch.validate().is_ok());
1386        assert!(dependency_details_git_rev.validate().is_ok());
1387        assert!(dependency_details_ipfs.validate().is_ok());
1388    }
1389
1390    #[test]
1391    fn test_project_with_null_metadata() {
1392        let project = Project {
1393            authors: Some(vec!["Test Author".to_string()]),
1394            name: "test-project".to_string(),
1395            version: Some(Version::parse("0.1.0").unwrap()),
1396            description: Some("test description".to_string()),
1397            homepage: None,
1398            documentation: None,
1399            categories: None,
1400            keywords: None,
1401            repository: None,
1402            organization: None,
1403            license: "Apache-2.0".to_string(),
1404            entry: "main.sw".to_string(),
1405            implicit_std: None,
1406            forc_version: None,
1407            experimental: HashMap::new(),
1408            metadata: Some(toml::Value::from(toml::value::Table::new())),
1409        };
1410
1411        let serialized = toml::to_string(&project).unwrap();
1412        let deserialized: Project = toml::from_str(&serialized).unwrap();
1413
1414        assert_eq!(project.name, deserialized.name);
1415        assert_eq!(project.metadata, deserialized.metadata);
1416    }
1417
1418    #[test]
1419    fn test_project_without_metadata() {
1420        let project = Project {
1421            authors: Some(vec!["Test Author".to_string()]),
1422            name: "test-project".to_string(),
1423            version: Some(Version::parse("0.1.0").unwrap()),
1424            description: Some("test description".to_string()),
1425            homepage: Some(Url::parse("https://example.com").unwrap()),
1426            documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1427            categories: Some(vec!["test-category".to_string()]),
1428            keywords: Some(vec!["test-keyword".to_string()]),
1429            repository: Some(Url::parse("https://example.com").unwrap()),
1430            organization: None,
1431            license: "Apache-2.0".to_string(),
1432            entry: "main.sw".to_string(),
1433            implicit_std: None,
1434            forc_version: None,
1435            experimental: HashMap::new(),
1436            metadata: None,
1437        };
1438
1439        let serialized = toml::to_string(&project).unwrap();
1440        let deserialized: Project = toml::from_str(&serialized).unwrap();
1441
1442        assert_eq!(project.name, deserialized.name);
1443        assert_eq!(project.version, deserialized.version);
1444        assert_eq!(project.description, deserialized.description);
1445        assert_eq!(project.homepage, deserialized.homepage);
1446        assert_eq!(project.documentation, deserialized.documentation);
1447        assert_eq!(project.repository, deserialized.repository);
1448        assert_eq!(project.metadata, deserialized.metadata);
1449        assert_eq!(project.metadata, None);
1450        assert_eq!(project.categories, deserialized.categories);
1451        assert_eq!(project.keywords, deserialized.keywords);
1452    }
1453
1454    #[test]
1455    fn test_project_metadata_from_toml() {
1456        let toml_str = r#"
1457            name = "test-project"
1458            license = "Apache-2.0"
1459            entry = "main.sw"
1460            authors = ["Test Author"]
1461            description = "A test project"
1462            version = "1.0.0"
1463            keywords = ["test", "project"]
1464            categories = ["test"]
1465
1466            [metadata]
1467            mykey = "https://example.com"
1468        "#;
1469
1470        let project: Project = toml::from_str(toml_str).unwrap();
1471        assert!(project.metadata.is_some());
1472
1473        let metadata = project.metadata.unwrap();
1474        let table = metadata.as_table().unwrap();
1475
1476        assert_eq!(
1477            table.get("mykey").unwrap().as_str().unwrap(),
1478            "https://example.com"
1479        );
1480    }
1481
1482    #[test]
1483    fn test_project_with_invalid_metadata() {
1484        // Test with invalid TOML syntax - unclosed table
1485        let invalid_toml = r#"
1486            name = "test-project"
1487            license = "Apache-2.0"
1488            entry = "main.sw"
1489            
1490            [metadata
1491            description = "Invalid TOML"
1492        "#;
1493
1494        let result: Result<Project, _> = toml::from_str(invalid_toml);
1495        assert!(result.is_err());
1496
1497        // Test with invalid TOML syntax - invalid key
1498        let invalid_toml = r#"
1499            name = "test-project"
1500            license = "Apache-2.0"
1501            entry = "main.sw"
1502            
1503            [metadata]
1504            ] = "Invalid key"
1505        "#;
1506
1507        let result: Result<Project, _> = toml::from_str(invalid_toml);
1508        assert!(result.is_err());
1509
1510        // Test with duplicate keys
1511        let invalid_toml = r#"
1512            name = "test-project"
1513            license = "Apache-2.0"
1514            entry = "main.sw"
1515            
1516            [metadata]
1517            nested = { key = "value1" }
1518
1519            [metadata.nested]
1520            key = "value2"
1521        "#;
1522
1523        let result: Result<Project, _> = toml::from_str(invalid_toml);
1524        assert!(result.is_err());
1525        assert!(result
1526            .err()
1527            .unwrap()
1528            .to_string()
1529            .contains("duplicate key `nested` in table `metadata`"));
1530    }
1531
1532    #[test]
1533    fn test_metadata_roundtrip() {
1534        let original_toml = r#"
1535            name = "test-project"
1536            license = "Apache-2.0"
1537            entry = "main.sw"
1538            
1539            [metadata]
1540            boolean = true
1541            integer = 42
1542            float = 3.12
1543            string = "value"
1544            array = [1, 2, 3]
1545            mixed_array = [1, "two", true]
1546
1547            [metadata.nested]
1548            key = "value2"
1549        "#;
1550
1551        let project: Project = toml::from_str(original_toml).unwrap();
1552        let serialized = toml::to_string(&project).unwrap();
1553        let deserialized: Project = toml::from_str(&serialized).unwrap();
1554
1555        // Verify that the metadata is preserved
1556        assert_eq!(project.metadata, deserialized.metadata);
1557
1558        // Verify all types were preserved
1559        let table_val = project.metadata.unwrap();
1560        let table = table_val.as_table().unwrap();
1561        assert!(table.get("boolean").unwrap().as_bool().unwrap());
1562        assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1563        assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1564        assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1565        assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1566        assert!(table.get("nested").unwrap().as_table().is_some());
1567    }
1568
1569    #[test]
1570    fn test_workspace_with_metadata() {
1571        let toml_str = r#"
1572            [workspace]
1573            members = ["package1", "package2"]
1574            
1575            [workspace.metadata]
1576            description = "A test workspace"
1577            version = "1.0.0"
1578            authors = ["Test Author"]
1579            homepage = "https://example.com"
1580            
1581            [workspace.metadata.ci]
1582            workflow = "main"
1583            timeout = 3600
1584        "#;
1585
1586        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1587        assert!(manifest.workspace.metadata.is_some());
1588
1589        let metadata = manifest.workspace.metadata.unwrap();
1590        let table = metadata.as_table().unwrap();
1591
1592        assert_eq!(
1593            table.get("description").unwrap().as_str().unwrap(),
1594            "A test workspace"
1595        );
1596        assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1597
1598        let ci = table.get("ci").unwrap().as_table().unwrap();
1599        assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1600        assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1601    }
1602
1603    #[test]
1604    fn test_workspace_without_metadata() {
1605        let toml_str = r#"
1606            [workspace]
1607            members = ["package1", "package2"]
1608        "#;
1609
1610        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1611        assert!(manifest.workspace.metadata.is_none());
1612    }
1613
1614    #[test]
1615    fn test_workspace_empty_metadata() {
1616        let toml_str = r#"
1617            [workspace]
1618            members = ["package1", "package2"]
1619            
1620            [workspace.metadata]
1621        "#;
1622
1623        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1624        assert!(manifest.workspace.metadata.is_some());
1625        let metadata = manifest.workspace.metadata.unwrap();
1626        assert!(metadata.as_table().unwrap().is_empty());
1627    }
1628
1629    #[test]
1630    fn test_workspace_complex_metadata() {
1631        let toml_str = r#"
1632            [workspace]
1633            members = ["package1", "package2"]
1634            
1635            [workspace.metadata]
1636            numbers = [1, 2, 3]
1637            strings = ["a", "b", "c"]
1638            mixed = [1, "two", true]
1639            
1640            [workspace.metadata.nested]
1641            key = "value"
1642            
1643            [workspace.metadata.nested.deep]
1644            another = "value"
1645        "#;
1646
1647        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1648        let metadata = manifest.workspace.metadata.unwrap();
1649        let table = metadata.as_table().unwrap();
1650
1651        assert!(table.get("numbers").unwrap().as_array().is_some());
1652        assert!(table.get("strings").unwrap().as_array().is_some());
1653        assert!(table.get("mixed").unwrap().as_array().is_some());
1654
1655        let nested = table.get("nested").unwrap().as_table().unwrap();
1656        assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1657
1658        let deep = nested.get("deep").unwrap().as_table().unwrap();
1659        assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1660    }
1661
1662    #[test]
1663    fn test_workspace_metadata_roundtrip() {
1664        let original = WorkspaceManifest {
1665            workspace: Workspace {
1666                members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1667                metadata: Some(toml::Value::Table({
1668                    let mut table = toml::value::Table::new();
1669                    table.insert("key".to_string(), toml::Value::String("value".to_string()));
1670                    table
1671                })),
1672            },
1673            patch: None,
1674        };
1675
1676        let serialized = toml::to_string(&original).unwrap();
1677        let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1678
1679        assert_eq!(original.workspace.members, deserialized.workspace.members);
1680        assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1681    }
1682
1683    #[test]
1684    fn test_dependency_alias_project_name_collision() {
1685        let original_toml = r#"
1686        [project]
1687        authors = ["Fuel Labs <contact@fuel.sh>"]
1688        entry = "main.sw"
1689        license = "Apache-2.0"
1690        name = "lib_contract_abi"
1691
1692        [dependencies]
1693        lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1694        "#;
1695
1696        let project = PackageManifest::from_string(original_toml.to_string());
1697        let err = project.unwrap_err();
1698        assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1699    }
1700
1701    #[test]
1702    fn test_dependency_name_project_name_collision() {
1703        let original_toml = r#"
1704        [project]
1705        authors = ["Fuel Labs <contact@fuel.sh>"]
1706        entry = "main.sw"
1707        license = "Apache-2.0"
1708        name = "lib_contract"
1709
1710        [dependencies]
1711        lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1712        "#;
1713
1714        let project = PackageManifest::from_string(original_toml.to_string());
1715        let err = project.unwrap_err();
1716        assert_eq!(
1717            err.to_string(),
1718            format!("Dependency \"lib_contract\" collides with project name.")
1719        )
1720    }
1721}