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