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 `core` and `std` are 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 `core` and `std` are 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 `core` and `std` are 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 `core` and `std` packages under `[dependencies]`. If both are 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 `core`/`std` deps.
737    ///
738    /// Note: If only `core` is specified, we are unable to implicitly add `std` as we cannot
739    /// guarantee that the user's `core` is compatible with the implicit `std`.
740    fn implicitly_include_std_if_missing(&mut self) {
741        use sway_types::constants::{CORE, STD};
742        // Don't include `std` if:
743        // - this *is* `core` or `std`.
744        // - either `core` or `std` packages are already specified.
745        // - a dependency already exists with the name "std".
746        if self.project.name == CORE
747            || self.project.name == STD
748            || self.pkg_dep(CORE).is_some()
749            || self.pkg_dep(STD).is_some()
750            || self.dep(STD).is_some()
751            || !self.project.implicit_std.unwrap_or(true)
752        {
753            return;
754        }
755        // Add a `[dependencies]` table if there isn't one.
756        let deps = self.dependencies.get_or_insert_with(Default::default);
757        // Add the missing dependency.
758        let std_dep = implicit_std_dep();
759        deps.insert(STD.to_string(), std_dep);
760    }
761
762    /// Check for the `debug` and `release` packages under `[build-profile]`. If they are missing add them.
763    /// If they are provided, use the provided `debug` or `release` so that they override the default `debug`
764    /// and `release`.
765    fn implicitly_include_default_build_profiles_if_missing(&mut self) {
766        let build_profiles = self.build_profile.get_or_insert_with(Default::default);
767
768        if build_profiles.get(BuildProfile::DEBUG).is_none() {
769            build_profiles.insert(BuildProfile::DEBUG.into(), BuildProfile::debug());
770        }
771        if build_profiles.get(BuildProfile::RELEASE).is_none() {
772            build_profiles.insert(BuildProfile::RELEASE.into(), BuildProfile::release());
773        }
774    }
775
776    /// Retrieve a reference to the dependency with the given name.
777    pub fn dep(&self, dep_name: &str) -> Option<&Dependency> {
778        self.dependencies
779            .as_ref()
780            .and_then(|deps| deps.get(dep_name))
781    }
782
783    /// Retrieve a reference to the dependency with the given name.
784    pub fn dep_detailed(&self, dep_name: &str) -> Option<&DependencyDetails> {
785        self.dep(dep_name).and_then(|dep| match dep {
786            Dependency::Simple(_) => None,
787            Dependency::Detailed(detailed) => Some(detailed),
788        })
789    }
790
791    /// Retrieve a reference to the contract dependency with the given name.
792    pub fn contract_dep(&self, contract_dep_name: &str) -> Option<&ContractDependency> {
793        self.contract_dependencies
794            .as_ref()
795            .and_then(|contract_dependencies| contract_dependencies.get(contract_dep_name))
796    }
797
798    /// Retrieve a reference to the contract dependency with the given name.
799    pub fn contract_dependency_detailed(
800        &self,
801        contract_dep_name: &str,
802    ) -> Option<&DependencyDetails> {
803        self.contract_dep(contract_dep_name)
804            .and_then(|contract_dep| match &contract_dep.dependency {
805                Dependency::Simple(_) => None,
806                Dependency::Detailed(detailed) => Some(detailed),
807            })
808    }
809
810    /// Finds and returns the name of the dependency associated with a package of the specified
811    /// name if there is one.
812    ///
813    /// Returns `None` in the case that no dependencies associate with a package of the given name.
814    fn pkg_dep<'a>(&'a self, pkg_name: &str) -> Option<&'a str> {
815        for (dep_name, dep) in self.deps() {
816            if dep.package().unwrap_or(dep_name) == pkg_name {
817                return Some(dep_name);
818            }
819        }
820        None
821    }
822}
823
824impl std::ops::Deref for PackageManifestFile {
825    type Target = PackageManifest;
826    fn deref(&self) -> &Self::Target {
827        &self.manifest
828    }
829}
830
831/// The definition for the implicit `std` dependency.
832///
833/// This can be configured using environment variables:
834/// - use `FORC_IMPLICIT_STD_PATH` for the path for the std-lib;
835/// - use `FORC_IMPLICIT_STD_GIT`, `FORC_IMPLICIT_STD_GIT_TAG` and/or `FORC_IMPLICIT_STD_GIT_BRANCH` to configure
836///   the git repo of the std-lib.
837fn implicit_std_dep() -> Dependency {
838    if let Ok(path) = std::env::var("FORC_IMPLICIT_STD_PATH") {
839        return Dependency::Detailed(DependencyDetails {
840            path: Some(path),
841            ..Default::default()
842        });
843    }
844
845    // Here, we use the `forc-pkg` crate version formatted with the `v` prefix (e.g. "v1.2.3"),
846    // or the revision commit hash (e.g. "abcdefg").
847    //
848    // This git tag or revision is used during `PackageManifest` construction to pin the version of the
849    // implicit `std` dependency to the `forc-pkg` version.
850    //
851    // This is important to ensure that the version of `sway-core` that is baked into `forc-pkg` is
852    // compatible with the version of the `std` lib.
853    let tag = std::env::var("FORC_IMPLICIT_STD_GIT_TAG")
854        .ok()
855        .unwrap_or_else(|| format!("v{}", env!("CARGO_PKG_VERSION")));
856    const SWAY_GIT_REPO_URL: &str = "https://github.com/fuellabs/sway";
857
858    // only use tag/rev if the branch is None
859    let branch = std::env::var("FORC_IMPLICIT_STD_GIT_BRANCH").ok();
860    let tag = branch.as_ref().map_or_else(|| Some(tag), |_| None);
861
862    let mut det = DependencyDetails {
863        git: std::env::var("FORC_IMPLICIT_STD_GIT")
864            .ok()
865            .or_else(|| Some(SWAY_GIT_REPO_URL.to_string())),
866        tag,
867        branch,
868        ..Default::default()
869    };
870
871    if let Some((_, build_metadata)) = det.tag.as_ref().and_then(|tag| tag.split_once('+')) {
872        // Nightlies are in the format v<version>+nightly.<date>.<hash>
873        let rev = build_metadata.split('.').last().map(|r| r.to_string());
874
875        // If some revision is available and parsed from the 'nightly' build metadata,
876        // we always prefer the revision over the tag.
877        det.tag = None;
878        det.rev = rev;
879    };
880
881    Dependency::Detailed(det)
882}
883
884fn default_entry() -> String {
885    PackageManifest::DEFAULT_ENTRY_FILE_NAME.to_string()
886}
887
888fn default_url() -> String {
889    constants::DEFAULT_NODE_URL.into()
890}
891
892/// A [WorkspaceManifest] that was deserialized from a file at a particular path.
893#[derive(Clone, Debug)]
894pub struct WorkspaceManifestFile {
895    /// The derserialized `Forc.toml`
896    manifest: WorkspaceManifest,
897    /// The path from which the `Forc.toml` file was read.
898    path: PathBuf,
899}
900
901/// A direct mapping to `Forc.toml` if it is a WorkspaceManifest
902#[derive(Serialize, Deserialize, Clone, Debug)]
903#[serde(rename_all = "kebab-case")]
904pub struct WorkspaceManifest {
905    workspace: Workspace,
906    patch: Option<BTreeMap<String, PatchMap>>,
907}
908
909#[derive(Serialize, Deserialize, Clone, Debug)]
910#[serde(rename_all = "kebab-case")]
911pub struct Workspace {
912    pub members: Vec<PathBuf>,
913    pub metadata: Option<toml::Value>,
914}
915
916impl WorkspaceManifestFile {
917    /// Produce an iterator yielding all listed patches.
918    pub fn patches(&self) -> impl Iterator<Item = (&String, &PatchMap)> {
919        self.patch
920            .as_ref()
921            .into_iter()
922            .flat_map(|patches| patches.iter())
923    }
924
925    /// Returns an iterator over relative paths of workspace members.
926    pub fn members(&self) -> impl Iterator<Item = &PathBuf> + '_ {
927        self.workspace.members.iter()
928    }
929
930    /// Returns an iterator over workspace member root directories.
931    ///
932    /// This will always return canonical paths.
933    pub fn member_paths(&self) -> Result<impl Iterator<Item = PathBuf> + '_> {
934        Ok(self
935            .workspace
936            .members
937            .iter()
938            .map(|member| self.dir().join(member)))
939    }
940
941    /// Returns an iterator over workspace member package manifests.
942    pub fn member_pkg_manifests(
943        &self,
944    ) -> Result<impl Iterator<Item = Result<PackageManifestFile>> + '_> {
945        let member_paths = self.member_paths()?;
946        let member_pkg_manifests = member_paths.map(PackageManifestFile::from_dir);
947        Ok(member_pkg_manifests)
948    }
949
950    /// Check if given path corresponds to any workspace member's path
951    pub fn is_member_path(&self, path: &Path) -> Result<bool> {
952        Ok(self.member_paths()?.any(|member_path| member_path == path))
953    }
954}
955
956impl GenericManifestFile for WorkspaceManifestFile {
957    /// Given a path to a `Forc.toml`, read it and construct a `PackageManifest`
958    ///
959    /// This also `validate`s the manifest, returning an `Err` in the case that given members are
960    /// not present in the manifest dir.
961    fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
962        let path = path.as_ref().canonicalize()?;
963        let parent = path
964            .parent()
965            .ok_or_else(|| anyhow!("Cannot get parent dir of {:?}", path))?;
966        let manifest = WorkspaceManifest::from_file(&path)?;
967        manifest.validate(parent)?;
968        Ok(Self { manifest, path })
969    }
970
971    /// Read the manifest from the `Forc.toml` in the directory specified by the given `path` or
972    /// any of its parent directories.
973    ///
974    /// This is short for `PackageManifest::from_file`, but takes care of constructing the path to the
975    /// file.
976    fn from_dir<P: AsRef<Path>>(manifest_dir: P) -> Result<Self> {
977        let manifest_dir = manifest_dir.as_ref();
978        let dir = find_parent_manifest_dir_with_check(manifest_dir, |possible_manifest_dir| {
979            // Check if the found manifest file is a workspace manifest file or a standalone
980            // package manifest file.
981            let possible_path = possible_manifest_dir.join(constants::MANIFEST_FILE_NAME);
982            // We should not continue to search if the given manifest is a workspace manifest with
983            // some issues.
984            //
985            // If the error is missing field `workspace` (which happens when trying to read a
986            // package manifest as a workspace manifest), look into the parent directories for a
987            // legitimate workspace manifest. If the error returned is something else this is a
988            // workspace manifest with errors, classify this as a workspace manifest but with
989            // errors so that the errors will be displayed to the user.
990            Self::from_file(possible_path)
991                .err()
992                .map(|e| !e.to_string().contains("missing field `workspace`"))
993                .unwrap_or_else(|| true)
994        })
995        .ok_or_else(|| manifest_file_missing(manifest_dir))?;
996        let path = dir.join(constants::MANIFEST_FILE_NAME);
997        Self::from_file(path)
998    }
999
1000    fn path(&self) -> &Path {
1001        &self.path
1002    }
1003
1004    /// Returns the location of the lock file for `WorkspaceManifestFile`.
1005    ///
1006    /// This will always be a canonical path.
1007    fn lock_path(&self) -> Result<PathBuf> {
1008        Ok(self.dir().to_path_buf().join(constants::LOCK_FILE_NAME))
1009    }
1010
1011    fn member_manifests(&self) -> Result<MemberManifestFiles> {
1012        let mut member_manifest_files = BTreeMap::new();
1013        for member_manifest in self.member_pkg_manifests()? {
1014            let member_manifest = member_manifest.with_context(|| "Invalid member manifest")?;
1015            member_manifest_files.insert(member_manifest.project.name.clone(), member_manifest);
1016        }
1017
1018        Ok(member_manifest_files)
1019    }
1020}
1021
1022impl WorkspaceManifest {
1023    /// Given a path to a `Forc.toml`, read it and construct a `WorkspaceManifest`.
1024    pub fn from_file(path: &Path) -> Result<Self> {
1025        // While creating a `ManifestFile` we need to check if the given path corresponds to a
1026        // package or a workspace. While doing so, we should be printing the warnings if the given
1027        // file parses so that we only see warnings for the correct type of manifest.
1028        let mut warnings = vec![];
1029        let manifest_str = std::fs::read_to_string(path)
1030            .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?;
1031        let toml_de = toml::de::Deserializer::new(&manifest_str);
1032        let manifest: Self = serde_ignored::deserialize(toml_de, |path| {
1033            let warning = format!("unused manifest key: {path}");
1034            warnings.push(warning);
1035        })
1036        .map_err(|e| anyhow!("failed to parse manifest: {}.", e))?;
1037        for warning in warnings {
1038            println_warning(&warning);
1039        }
1040        Ok(manifest)
1041    }
1042
1043    /// Validate the `WorkspaceManifest`
1044    ///
1045    /// This checks if the listed members in the `WorkspaceManifest` are indeed in the given `Forc.toml`'s directory.
1046    pub fn validate(&self, path: &Path) -> Result<()> {
1047        let mut pkg_name_to_paths: HashMap<String, Vec<PathBuf>> = HashMap::new();
1048        for member in &self.workspace.members {
1049            let member_path = path.join(member).join("Forc.toml");
1050            if !member_path.exists() {
1051                bail!(
1052                    "{:?} is listed as a member of the workspace but {:?} does not exists",
1053                    &member,
1054                    member_path
1055                );
1056            }
1057            if Self::from_file(&member_path).is_ok() {
1058                bail!("Unexpected nested workspace '{}'. Workspaces are currently only allowed in the project root.", member.display());
1059            };
1060
1061            let member_manifest_file = PackageManifestFile::from_file(member_path.clone())?;
1062            let pkg_name = member_manifest_file.manifest.project.name;
1063            pkg_name_to_paths
1064                .entry(pkg_name)
1065                .or_default()
1066                .push(member_path);
1067        }
1068
1069        // Check for duplicate pkg name entries in member manifests of this workspace.
1070        let duplicate_pkg_lines = pkg_name_to_paths
1071            .iter()
1072            .filter_map(|(pkg_name, paths)| {
1073                if paths.len() > 1 {
1074                    let duplicate_paths = pkg_name_to_paths
1075                        .get(pkg_name)
1076                        .expect("missing duplicate paths");
1077                    Some(format!("{pkg_name}: {duplicate_paths:#?}"))
1078                } else {
1079                    None
1080                }
1081            })
1082            .collect::<Vec<_>>();
1083
1084        if !duplicate_pkg_lines.is_empty() {
1085            let error_message = duplicate_pkg_lines.join("\n");
1086            bail!(
1087                "Duplicate package names detected in the workspace:\n\n{}",
1088                error_message
1089            );
1090        }
1091        Ok(())
1092    }
1093}
1094
1095impl std::ops::Deref for WorkspaceManifestFile {
1096    type Target = WorkspaceManifest;
1097    fn deref(&self) -> &Self::Target {
1098        &self.manifest
1099    }
1100}
1101
1102/// Attempt to find a `Forc.toml` with the given project name within the given directory.
1103///
1104/// Returns the path to the package on success, or `None` in the case it could not be found.
1105pub fn find_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1106    use sway_types::constants::STD;
1107    const SWAY_STD_FOLDER: &str = "sway-lib-std";
1108    walkdir::WalkDir::new(dir)
1109        .into_iter()
1110        .filter_map(|entry| {
1111            entry
1112                .ok()
1113                .filter(|entry| entry.path().ends_with(constants::MANIFEST_FILE_NAME))
1114        })
1115        .find_map(|entry| {
1116            let path = entry.path();
1117            let manifest = PackageManifest::from_file(path).ok()?;
1118            // If the package is STD, make sure it is coming from correct folder.
1119            if (manifest.project.name == pkg_name && pkg_name != STD)
1120                || (manifest.project.name == STD
1121                    && path
1122                        .components()
1123                        .any(|comp| comp.as_os_str() == SWAY_STD_FOLDER))
1124            {
1125                Some(path.to_path_buf())
1126            } else {
1127                None
1128            }
1129        })
1130}
1131
1132/// The same as [find_within], but returns the package's project directory.
1133pub fn find_dir_within(dir: &Path, pkg_name: &str) -> Option<PathBuf> {
1134    find_within(dir, pkg_name).and_then(|path| path.parent().map(Path::to_path_buf))
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use std::str::FromStr;
1140
1141    use super::*;
1142
1143    #[test]
1144    fn deserialize_contract_dependency() {
1145        let contract_dep_str = r#"{"path": "../", "salt": "0x1111111111111111111111111111111111111111111111111111111111111111" }"#;
1146
1147        let contract_dep_expected: ContractDependency =
1148            serde_json::from_str(contract_dep_str).unwrap();
1149
1150        let dependency_det = DependencyDetails {
1151            path: Some("../".to_owned()),
1152            ..Default::default()
1153        };
1154        let dependency = Dependency::Detailed(dependency_det);
1155        let contract_dep = ContractDependency {
1156            dependency,
1157            salt: HexSalt::from_str(
1158                "0x1111111111111111111111111111111111111111111111111111111111111111",
1159            )
1160            .unwrap(),
1161        };
1162        assert_eq!(contract_dep, contract_dep_expected)
1163    }
1164    #[test]
1165    fn test_invalid_dependency_details_mixed_together() {
1166        let dependency_details_path_branch = DependencyDetails {
1167            version: None,
1168            path: Some("example_path/".to_string()),
1169            git: None,
1170            branch: Some("test_branch".to_string()),
1171            tag: None,
1172            package: None,
1173            rev: None,
1174            ipfs: None,
1175        };
1176
1177        let dependency_details_branch = DependencyDetails {
1178            path: None,
1179            ..dependency_details_path_branch.clone()
1180        };
1181
1182        let dependency_details_ipfs_branch = DependencyDetails {
1183            path: None,
1184            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1185            ..dependency_details_path_branch.clone()
1186        };
1187
1188        let dependency_details_path_tag = DependencyDetails {
1189            version: None,
1190            path: Some("example_path/".to_string()),
1191            git: None,
1192            branch: None,
1193            tag: Some("v0.1.0".to_string()),
1194            package: None,
1195            rev: None,
1196            ipfs: None,
1197        };
1198
1199        let dependency_details_tag = DependencyDetails {
1200            path: None,
1201            ..dependency_details_path_tag.clone()
1202        };
1203
1204        let dependency_details_ipfs_tag = DependencyDetails {
1205            path: None,
1206            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1207            ..dependency_details_path_branch.clone()
1208        };
1209
1210        let dependency_details_path_rev = DependencyDetails {
1211            version: None,
1212            path: Some("example_path/".to_string()),
1213            git: None,
1214            branch: None,
1215            tag: None,
1216            package: None,
1217            ipfs: None,
1218            rev: Some("9f35b8e".to_string()),
1219        };
1220
1221        let dependency_details_rev = DependencyDetails {
1222            path: None,
1223            ..dependency_details_path_rev.clone()
1224        };
1225
1226        let dependency_details_ipfs_rev = DependencyDetails {
1227            path: None,
1228            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1229            ..dependency_details_path_branch.clone()
1230        };
1231
1232        let expected_mismatch_error = "Details reserved for git sources used without a git field";
1233        assert_eq!(
1234            dependency_details_path_branch
1235                .validate()
1236                .err()
1237                .map(|e| e.to_string()),
1238            Some(expected_mismatch_error.to_string())
1239        );
1240        assert_eq!(
1241            dependency_details_ipfs_branch
1242                .validate()
1243                .err()
1244                .map(|e| e.to_string()),
1245            Some(expected_mismatch_error.to_string())
1246        );
1247        assert_eq!(
1248            dependency_details_path_tag
1249                .validate()
1250                .err()
1251                .map(|e| e.to_string()),
1252            Some(expected_mismatch_error.to_string())
1253        );
1254        assert_eq!(
1255            dependency_details_ipfs_tag
1256                .validate()
1257                .err()
1258                .map(|e| e.to_string()),
1259            Some(expected_mismatch_error.to_string())
1260        );
1261        assert_eq!(
1262            dependency_details_path_rev
1263                .validate()
1264                .err()
1265                .map(|e| e.to_string()),
1266            Some(expected_mismatch_error.to_string())
1267        );
1268        assert_eq!(
1269            dependency_details_ipfs_rev
1270                .validate()
1271                .err()
1272                .map(|e| e.to_string()),
1273            Some(expected_mismatch_error.to_string())
1274        );
1275        assert_eq!(
1276            dependency_details_branch
1277                .validate()
1278                .err()
1279                .map(|e| e.to_string()),
1280            Some(expected_mismatch_error.to_string())
1281        );
1282        assert_eq!(
1283            dependency_details_tag
1284                .validate()
1285                .err()
1286                .map(|e| e.to_string()),
1287            Some(expected_mismatch_error.to_string())
1288        );
1289        assert_eq!(
1290            dependency_details_rev
1291                .validate()
1292                .err()
1293                .map(|e| e.to_string()),
1294            Some(expected_mismatch_error.to_string())
1295        );
1296    }
1297
1298    #[test]
1299    #[should_panic(expected = "duplicate key `foo` in table `dependencies`")]
1300    fn test_error_duplicate_deps_definition() {
1301        PackageManifest::from_dir("./tests/invalid/duplicate_keys").unwrap();
1302    }
1303
1304    #[test]
1305    fn test_error_duplicate_deps_definition_in_workspace() {
1306        // Load each project inside a workspace and load their patches
1307        // definition. There should be zero, because the file workspace file has
1308        // no patches
1309        //
1310        // The code also prints a warning to the stdout
1311        let workspace =
1312            WorkspaceManifestFile::from_dir("./tests/invalid/patch_workspace_and_package").unwrap();
1313        let projects: Vec<_> = workspace
1314            .member_pkg_manifests()
1315            .unwrap()
1316            .collect::<Result<Vec<_>, _>>()
1317            .unwrap();
1318        assert_eq!(projects.len(), 1);
1319        let patches: Vec<_> = projects[0].resolve_patches().unwrap().collect();
1320        assert_eq!(patches.len(), 0);
1321
1322        // Load the same Forc.toml file but outside of a workspace. There should
1323        // be a single entry in the patch
1324        let patches: Vec<_> = PackageManifestFile::from_dir("./tests/test_package")
1325            .unwrap()
1326            .resolve_patches()
1327            .unwrap()
1328            .collect();
1329        assert_eq!(patches.len(), 1);
1330    }
1331
1332    #[test]
1333    fn test_valid_dependency_details() {
1334        let dependency_details_path = DependencyDetails {
1335            version: None,
1336            path: Some("example_path/".to_string()),
1337            git: None,
1338            branch: None,
1339            tag: None,
1340            package: None,
1341            rev: None,
1342            ipfs: None,
1343        };
1344
1345        let git_source_string = "https://github.com/FuelLabs/sway".to_string();
1346        let dependency_details_git_tag = DependencyDetails {
1347            version: None,
1348            path: None,
1349            git: Some(git_source_string.clone()),
1350            branch: None,
1351            tag: Some("v0.1.0".to_string()),
1352            package: None,
1353            rev: None,
1354            ipfs: None,
1355        };
1356        let dependency_details_git_branch = DependencyDetails {
1357            version: None,
1358            path: None,
1359            git: Some(git_source_string.clone()),
1360            branch: Some("test_branch".to_string()),
1361            tag: None,
1362            package: None,
1363            rev: None,
1364            ipfs: None,
1365        };
1366        let dependency_details_git_rev = DependencyDetails {
1367            version: None,
1368            path: None,
1369            git: Some(git_source_string),
1370            branch: Some("test_branch".to_string()),
1371            tag: None,
1372            package: None,
1373            rev: Some("9f35b8e".to_string()),
1374            ipfs: None,
1375        };
1376
1377        let dependency_details_ipfs = DependencyDetails {
1378            version: None,
1379            path: None,
1380            git: None,
1381            branch: None,
1382            tag: None,
1383            package: None,
1384            rev: None,
1385            ipfs: Some("QmVxgEbiDDdHpG9AesCpZAqNvHYp1P3tWLFdrpUBWPMBcc".to_string()),
1386        };
1387
1388        assert!(dependency_details_path.validate().is_ok());
1389        assert!(dependency_details_git_tag.validate().is_ok());
1390        assert!(dependency_details_git_branch.validate().is_ok());
1391        assert!(dependency_details_git_rev.validate().is_ok());
1392        assert!(dependency_details_ipfs.validate().is_ok());
1393    }
1394
1395    #[test]
1396    fn test_project_with_null_metadata() {
1397        let project = Project {
1398            authors: Some(vec!["Test Author".to_string()]),
1399            name: "test-project".to_string(),
1400            version: Some(Version::parse("0.1.0").unwrap()),
1401            description: Some("test description".to_string()),
1402            homepage: None,
1403            documentation: None,
1404            categories: None,
1405            keywords: None,
1406            repository: None,
1407            organization: None,
1408            license: "Apache-2.0".to_string(),
1409            entry: "main.sw".to_string(),
1410            implicit_std: None,
1411            forc_version: None,
1412            experimental: HashMap::new(),
1413            metadata: Some(toml::Value::from(toml::value::Table::new())),
1414        };
1415
1416        let serialized = toml::to_string(&project).unwrap();
1417        let deserialized: Project = toml::from_str(&serialized).unwrap();
1418
1419        assert_eq!(project.name, deserialized.name);
1420        assert_eq!(project.metadata, deserialized.metadata);
1421    }
1422
1423    #[test]
1424    fn test_project_without_metadata() {
1425        let project = Project {
1426            authors: Some(vec!["Test Author".to_string()]),
1427            name: "test-project".to_string(),
1428            version: Some(Version::parse("0.1.0").unwrap()),
1429            description: Some("test description".to_string()),
1430            homepage: Some(Url::parse("https://example.com").unwrap()),
1431            documentation: Some(Url::parse("https://docs.example.com").unwrap()),
1432            categories: Some(vec!["test-category".to_string()]),
1433            keywords: Some(vec!["test-keyword".to_string()]),
1434            repository: Some(Url::parse("https://example.com").unwrap()),
1435            organization: None,
1436            license: "Apache-2.0".to_string(),
1437            entry: "main.sw".to_string(),
1438            implicit_std: None,
1439            forc_version: None,
1440            experimental: HashMap::new(),
1441            metadata: None,
1442        };
1443
1444        let serialized = toml::to_string(&project).unwrap();
1445        let deserialized: Project = toml::from_str(&serialized).unwrap();
1446
1447        assert_eq!(project.name, deserialized.name);
1448        assert_eq!(project.version, deserialized.version);
1449        assert_eq!(project.description, deserialized.description);
1450        assert_eq!(project.homepage, deserialized.homepage);
1451        assert_eq!(project.documentation, deserialized.documentation);
1452        assert_eq!(project.repository, deserialized.repository);
1453        assert_eq!(project.metadata, deserialized.metadata);
1454        assert_eq!(project.metadata, None);
1455        assert_eq!(project.categories, deserialized.categories);
1456        assert_eq!(project.keywords, deserialized.keywords);
1457    }
1458
1459    #[test]
1460    fn test_project_metadata_from_toml() {
1461        let toml_str = r#"
1462            name = "test-project"
1463            license = "Apache-2.0"
1464            entry = "main.sw"
1465            authors = ["Test Author"]
1466            description = "A test project"
1467            version = "1.0.0"
1468            keywords = ["test", "project"]
1469            categories = ["test"]
1470
1471            [metadata]
1472            mykey = "https://example.com"
1473        "#;
1474
1475        let project: Project = toml::from_str(toml_str).unwrap();
1476        assert!(project.metadata.is_some());
1477
1478        let metadata = project.metadata.unwrap();
1479        let table = metadata.as_table().unwrap();
1480
1481        assert_eq!(
1482            table.get("mykey").unwrap().as_str().unwrap(),
1483            "https://example.com"
1484        );
1485    }
1486
1487    #[test]
1488    fn test_project_with_invalid_metadata() {
1489        // Test with invalid TOML syntax - unclosed table
1490        let invalid_toml = r#"
1491            name = "test-project"
1492            license = "Apache-2.0"
1493            entry = "main.sw"
1494            
1495            [metadata
1496            description = "Invalid TOML"
1497        "#;
1498
1499        let result: Result<Project, _> = toml::from_str(invalid_toml);
1500        assert!(result.is_err());
1501
1502        // Test with invalid TOML syntax - invalid key
1503        let invalid_toml = r#"
1504            name = "test-project"
1505            license = "Apache-2.0"
1506            entry = "main.sw"
1507            
1508            [metadata]
1509            ] = "Invalid key"
1510        "#;
1511
1512        let result: Result<Project, _> = toml::from_str(invalid_toml);
1513        assert!(result.is_err());
1514
1515        // Test with duplicate keys
1516        let invalid_toml = r#"
1517            name = "test-project"
1518            license = "Apache-2.0"
1519            entry = "main.sw"
1520            
1521            [metadata]
1522            nested = { key = "value1" }
1523
1524            [metadata.nested]
1525            key = "value2"
1526        "#;
1527
1528        let result: Result<Project, _> = toml::from_str(invalid_toml);
1529        assert!(result.is_err());
1530        assert!(result
1531            .err()
1532            .unwrap()
1533            .to_string()
1534            .contains("duplicate key `nested` in table `metadata`"));
1535    }
1536
1537    #[test]
1538    fn test_metadata_roundtrip() {
1539        let original_toml = r#"
1540            name = "test-project"
1541            license = "Apache-2.0"
1542            entry = "main.sw"
1543            
1544            [metadata]
1545            boolean = true
1546            integer = 42
1547            float = 3.12
1548            string = "value"
1549            array = [1, 2, 3]
1550            mixed_array = [1, "two", true]
1551
1552            [metadata.nested]
1553            key = "value2"
1554        "#;
1555
1556        let project: Project = toml::from_str(original_toml).unwrap();
1557        let serialized = toml::to_string(&project).unwrap();
1558        let deserialized: Project = toml::from_str(&serialized).unwrap();
1559
1560        // Verify that the metadata is preserved
1561        assert_eq!(project.metadata, deserialized.metadata);
1562
1563        // Verify all types were preserved
1564        let table_val = project.metadata.unwrap();
1565        let table = table_val.as_table().unwrap();
1566        assert!(table.get("boolean").unwrap().as_bool().unwrap());
1567        assert_eq!(table.get("integer").unwrap().as_integer().unwrap(), 42);
1568        assert_eq!(table.get("float").unwrap().as_float().unwrap(), 3.12);
1569        assert_eq!(table.get("string").unwrap().as_str().unwrap(), "value");
1570        assert_eq!(table.get("array").unwrap().as_array().unwrap().len(), 3);
1571        assert!(table.get("nested").unwrap().as_table().is_some());
1572    }
1573
1574    #[test]
1575    fn test_workspace_with_metadata() {
1576        let toml_str = r#"
1577            [workspace]
1578            members = ["package1", "package2"]
1579            
1580            [workspace.metadata]
1581            description = "A test workspace"
1582            version = "1.0.0"
1583            authors = ["Test Author"]
1584            homepage = "https://example.com"
1585            
1586            [workspace.metadata.ci]
1587            workflow = "main"
1588            timeout = 3600
1589        "#;
1590
1591        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1592        assert!(manifest.workspace.metadata.is_some());
1593
1594        let metadata = manifest.workspace.metadata.unwrap();
1595        let table = metadata.as_table().unwrap();
1596
1597        assert_eq!(
1598            table.get("description").unwrap().as_str().unwrap(),
1599            "A test workspace"
1600        );
1601        assert_eq!(table.get("version").unwrap().as_str().unwrap(), "1.0.0");
1602
1603        let ci = table.get("ci").unwrap().as_table().unwrap();
1604        assert_eq!(ci.get("workflow").unwrap().as_str().unwrap(), "main");
1605        assert_eq!(ci.get("timeout").unwrap().as_integer().unwrap(), 3600);
1606    }
1607
1608    #[test]
1609    fn test_workspace_without_metadata() {
1610        let toml_str = r#"
1611            [workspace]
1612            members = ["package1", "package2"]
1613        "#;
1614
1615        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1616        assert!(manifest.workspace.metadata.is_none());
1617    }
1618
1619    #[test]
1620    fn test_workspace_empty_metadata() {
1621        let toml_str = r#"
1622            [workspace]
1623            members = ["package1", "package2"]
1624            
1625            [workspace.metadata]
1626        "#;
1627
1628        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1629        assert!(manifest.workspace.metadata.is_some());
1630        let metadata = manifest.workspace.metadata.unwrap();
1631        assert!(metadata.as_table().unwrap().is_empty());
1632    }
1633
1634    #[test]
1635    fn test_workspace_complex_metadata() {
1636        let toml_str = r#"
1637            [workspace]
1638            members = ["package1", "package2"]
1639            
1640            [workspace.metadata]
1641            numbers = [1, 2, 3]
1642            strings = ["a", "b", "c"]
1643            mixed = [1, "two", true]
1644            
1645            [workspace.metadata.nested]
1646            key = "value"
1647            
1648            [workspace.metadata.nested.deep]
1649            another = "value"
1650        "#;
1651
1652        let manifest: WorkspaceManifest = toml::from_str(toml_str).unwrap();
1653        let metadata = manifest.workspace.metadata.unwrap();
1654        let table = metadata.as_table().unwrap();
1655
1656        assert!(table.get("numbers").unwrap().as_array().is_some());
1657        assert!(table.get("strings").unwrap().as_array().is_some());
1658        assert!(table.get("mixed").unwrap().as_array().is_some());
1659
1660        let nested = table.get("nested").unwrap().as_table().unwrap();
1661        assert_eq!(nested.get("key").unwrap().as_str().unwrap(), "value");
1662
1663        let deep = nested.get("deep").unwrap().as_table().unwrap();
1664        assert_eq!(deep.get("another").unwrap().as_str().unwrap(), "value");
1665    }
1666
1667    #[test]
1668    fn test_workspace_metadata_roundtrip() {
1669        let original = WorkspaceManifest {
1670            workspace: Workspace {
1671                members: vec![PathBuf::from("package1"), PathBuf::from("package2")],
1672                metadata: Some(toml::Value::Table({
1673                    let mut table = toml::value::Table::new();
1674                    table.insert("key".to_string(), toml::Value::String("value".to_string()));
1675                    table
1676                })),
1677            },
1678            patch: None,
1679        };
1680
1681        let serialized = toml::to_string(&original).unwrap();
1682        let deserialized: WorkspaceManifest = toml::from_str(&serialized).unwrap();
1683
1684        assert_eq!(original.workspace.members, deserialized.workspace.members);
1685        assert_eq!(original.workspace.metadata, deserialized.workspace.metadata);
1686    }
1687
1688    #[test]
1689    fn test_dependency_alias_project_name_collision() {
1690        let original_toml = r#"
1691        [project]
1692        authors = ["Fuel Labs <contact@fuel.sh>"]
1693        entry = "main.sw"
1694        license = "Apache-2.0"
1695        name = "lib_contract_abi"
1696
1697        [dependencies]
1698        lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1699        "#;
1700
1701        let project = PackageManifest::from_string(original_toml.to_string());
1702        let err = project.unwrap_err();
1703        assert_eq!(err.to_string(), format!("Dependency \"lib_contract\" declares an alias (\"package\" field) that is the same as project name"))
1704    }
1705
1706    #[test]
1707    fn test_dependency_name_project_name_collision() {
1708        let original_toml = r#"
1709        [project]
1710        authors = ["Fuel Labs <contact@fuel.sh>"]
1711        entry = "main.sw"
1712        license = "Apache-2.0"
1713        name = "lib_contract"
1714
1715        [dependencies]
1716        lib_contract = { path = "../lib_contract_abi/", package = "lib_contract_abi" }
1717        "#;
1718
1719        let project = PackageManifest::from_string(original_toml.to_string());
1720        let err = project.unwrap_err();
1721        assert_eq!(
1722            err.to_string(),
1723            format!("Dependency \"lib_contract\" collides with project name.")
1724        )
1725    }
1726}