forc_pkg/manifest/
mod.rs

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