forc_pkg/manifest/
mod.rs

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