diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95159b8f4c5..b7b64d5fe7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,8 @@ jobs: run: | cargo install toml-cli ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc/Cargo.toml + ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-pkg/Cargo.toml + ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} forc-util/Cargo.toml ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} sway-core/Cargo.toml ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} sway-fmt/Cargo.toml ./.github/workflows/scripts/verify_tag.sh ${{ github.ref_name }} sway-ir/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index fded668f577..c7a0b6f3a9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1006,22 +1006,19 @@ name = "forc" version = "0.6.0" dependencies = [ "annotate-snippets", - "ansi_term", "anyhow", "clap 3.1.2", "clap_complete", - "dirs 3.0.2", + "forc-pkg", + "forc-util", "fuel-asm", "fuel-gql-client", "fuel-tx", "fuel-vm", "futures", - "git2", "hex", - "petgraph", "prettydiff", "reqwest", - "semver 1.0.4", "serde", "serde_json", "sway-core", @@ -1032,11 +1029,9 @@ dependencies = [ "taplo", "tar", "term-table", - "termcolor", "tokio", "toml", "toml_edit", - "unicode-xid", "ureq", "url", "uwuify", @@ -1044,6 +1039,36 @@ dependencies = [ "whoami", ] +[[package]] +name = "forc-pkg" +version = "0.6.0" +dependencies = [ + "anyhow", + "forc-util", + "git2", + "petgraph", + "semver 1.0.4", + "serde", + "sway-core", + "sway-types", + "sway-utils", + "toml", + "url", +] + +[[package]] +name = "forc-util" +version = "0.6.0" +dependencies = [ + "annotate-snippets", + "anyhow", + "dirs 3.0.2", + "sway-core", + "sway-utils", + "termcolor", + "unicode-xid", +] + [[package]] name = "form_urlencoded" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 7a226cf86e8..a40cec1f4f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "docstrings", "examples/build-all-examples", "forc", + "forc-pkg", + "forc-util", "parser", "sway-core", "sway-fmt", diff --git a/forc-pkg/Cargo.toml b/forc-pkg/Cargo.toml new file mode 100644 index 00000000000..1fb21b4c2aa --- /dev/null +++ b/forc-pkg/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "forc-pkg" +version = "0.6.0" +authors = ["Fuel Labs "] +edition = "2021" +homepage = "https://fuel.network/" +license = "Apache-2.0" +repository = "https://github.com/FuelLabs/sway" +description = "Building, locking, fetching and updating sway projects as Forc packages." + +[dependencies] +anyhow = "1" +forc-util = { version = "0.6.0", path = "../forc-util" } +git2 = "0.14" +petgraph = { version = "0.6", features = ["serde-1"] } +semver = { version = "1.0", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +sway-core = { version = "0.6.0", path = "../sway-core" } +sway-types = { version = "0.6.0", path = "../sway-types" } +sway-utils = { version = "0.6.0", path = "../sway-utils" } +toml = "0.5" +url = { version = "2.2", features = ["serde"] } diff --git a/forc-pkg/src/lib.rs b/forc-pkg/src/lib.rs new file mode 100644 index 00000000000..00f97d2cc3f --- /dev/null +++ b/forc-pkg/src/lib.rs @@ -0,0 +1,14 @@ +//! Building, locking, fetching and updating sway projects as Forc packages. +//! +//! A forc package represents a Sway project with a `Forc.toml` manifest file declared at its root. +//! The project should consist of one or more Sway modules under a `src` directory. It may also +//! declare a set of forc package dependencies within its manifest. + +pub mod lock; +pub mod manifest; +mod pkg; + +pub use lock::Lock; +pub use manifest::Manifest; +#[doc(inline)] +pub use pkg::*; diff --git a/forc/src/lock.rs b/forc-pkg/src/lock.rs similarity index 87% rename from forc/src/lock.rs rename to forc-pkg/src/lock.rs index ef9bae9e668..4115f3072e1 100644 --- a/forc/src/lock.rs +++ b/forc-pkg/src/lock.rs @@ -1,5 +1,6 @@ use crate::pkg; use anyhow::{anyhow, Result}; +use forc_util::{println_green, println_red}; use petgraph::{visit::EdgeRef, Direction}; use serde::{Deserialize, Serialize}; use std::{ @@ -11,7 +12,7 @@ use std::{ /// The graph of pinned packages represented as a toml-serialization-friendly structure. #[derive(Debug, Default, Deserialize, Serialize)] -pub(crate) struct Lock { +pub struct Lock { // Named `package` so that each entry serializes to lock file under `[[package]]` like cargo. pub(crate) package: BTreeSet, } @@ -19,13 +20,13 @@ pub(crate) struct Lock { /// Packages that have been removed and added between two `Lock` instances. /// /// The result of `new_lock.diff(&old_lock)`. -pub(crate) struct Diff<'a> { - pub(crate) removed: BTreeSet<&'a PkgLock>, - pub(crate) added: BTreeSet<&'a PkgLock>, +pub struct Diff<'a> { + pub removed: BTreeSet<&'a PkgLock>, + pub added: BTreeSet<&'a PkgLock>, } #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)] -pub(crate) struct PkgLock { +pub struct PkgLock { pub(crate) name: String, // TODO: Cargo *always* includes version, whereas we don't even parse it when reading a // project's `Manifest` yet. If we decide to enforce versions, we'll want to remove the @@ -169,3 +170,30 @@ fn pkg_unique_string(name: &str, source: Option<&str>) -> String { Some(s) => format!("{} {}", name, s), } } + +pub fn print_diff(proj_name: &str, diff: &Diff) { + print_removed_pkgs(proj_name, diff.removed.iter().cloned()); + print_added_pkgs(proj_name, diff.added.iter().cloned()); +} + +pub fn print_removed_pkgs<'a, I>(proj_name: &str, removed: I) +where + I: IntoIterator, +{ + for pkg in removed { + if pkg.name != proj_name { + let _ = println_red(&format!(" Removing {}", pkg.unique_string())); + } + } +} + +pub fn print_added_pkgs<'a, I>(proj_name: &str, removed: I) +where + I: IntoIterator, +{ + for pkg in removed { + if pkg.name != proj_name { + let _ = println_green(&format!(" Adding {}", pkg.unique_string())); + } + } +} diff --git a/forc-pkg/src/manifest.rs b/forc-pkg/src/manifest.rs new file mode 100644 index 00000000000..915fe30d32b --- /dev/null +++ b/forc-pkg/src/manifest.rs @@ -0,0 +1,136 @@ +use anyhow::anyhow; +use forc_util::validate_name; +use serde::{Deserialize, Serialize}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, + sync::Arc, +}; +use sway_utils::constants; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Manifest { + pub project: Project, + pub network: Option, + pub dependencies: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Project { + #[deprecated = "use the authors field instead, the author field will be removed soon."] + pub author: Option, + pub authors: Option>, + pub name: String, + pub organization: Option, + pub license: String, + #[serde(default = "default_entry")] + pub entry: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct Network { + #[serde(default = "default_url")] + pub url: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum Dependency { + /// In the simple format, only a version is specified, eg. + /// `package = ""` + Simple(String), + /// The simple format is equivalent to a detailed dependency + /// specifying only a version, eg. + /// `package = { version = "" }` + Detailed(DependencyDetails), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub struct DependencyDetails { + pub(crate) version: Option, + pub(crate) path: Option, + pub(crate) git: Option, + pub(crate) branch: Option, + pub(crate) tag: Option, +} + +impl Manifest { + pub const DEFAULT_ENTRY_FILE_NAME: &'static str = "main.sw"; + + /// Given a path to a `Forc.toml`, read it and construct a `Manifest`. + /// + /// This also `validate`s the manifest, returning an `Err` in the case that invalid names, + /// fields were used. + pub fn from_file(path: &Path) -> anyhow::Result { + let manifest = std::fs::read_to_string(path) + .map_err(|e| anyhow!("failed to read manifest at {:?}: {}", path, e))?; + let manifest: Self = + toml::from_str(&manifest).map_err(|e| anyhow!("failed to parse manifest: {}.", e))?; + manifest.validate()?; + Ok(manifest) + } + + /// Given a directory to a forc project containing a `Forc.toml`, read the manifest. + /// + /// This is short for `Manifest::from_file`, but takes care of constructing the path to the + /// file. + pub fn from_dir(manifest_dir: &Path) -> anyhow::Result { + let file_path = manifest_dir.join(constants::MANIFEST_FILE_NAME); + Self::from_file(&file_path) + } + + /// Validate the `Manifest`. + /// + /// This checks the project and organization names against a set of reserved/restricted + /// keywords and patterns. + pub fn validate(&self) -> anyhow::Result<()> { + validate_name(&self.project.name, "package name")?; + if let Some(ref org) = self.project.organization { + validate_name(org, "organization name")?; + } + Ok(()) + } + + /// Given the directory in which the file associated with this `Manifest` resides, produce the + /// path to the entry file as specified in the manifest. + pub fn entry_path(&self, manifest_dir: &Path) -> PathBuf { + manifest_dir + .join(constants::SRC_DIR) + .join(&self.project.entry) + } + + /// Produces the string of the entry point file. + pub fn entry_string(&self, manifest_dir: &Path) -> anyhow::Result> { + let entry_path = self.entry_path(manifest_dir); + let entry_string = std::fs::read_to_string(&entry_path)?; + Ok(Arc::from(entry_string)) + } + + /// Produce an iterator yielding all listed dependencies. + pub fn deps(&self) -> impl Iterator { + self.dependencies + .as_ref() + .into_iter() + .flat_map(|deps| deps.iter()) + } + + /// Produce an iterator yielding all `Detailed` dependencies. + pub fn deps_detailed(&self) -> impl Iterator { + self.deps().filter_map(|(name, dep)| match dep { + Dependency::Detailed(ref det) => Some((name, det)), + Dependency::Simple(_) => None, + }) + } +} + +fn default_entry() -> String { + Manifest::DEFAULT_ENTRY_FILE_NAME.to_string() +} + +fn default_url() -> String { + constants::DEFAULT_NODE_URL.into() +} diff --git a/forc/src/pkg.rs b/forc-pkg/src/pkg.rs similarity index 89% rename from forc/src/pkg.rs rename to forc-pkg/src/pkg.rs index 568f3578631..147e0ff2fac 100644 --- a/forc/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -1,15 +1,12 @@ use crate::{ lock::Lock, - utils::{ - dependency::Dependency, - helpers::{ - find_file_name, find_main_path, get_main_file, git_checkouts_directory, - print_on_failure, print_on_success, print_on_success_library, read_manifest, - }, - manifest::Manifest, - }, + manifest::{Dependency, Manifest}, }; use anyhow::{anyhow, bail, Result}; +use forc_util::{ + find_file_name, git_checkouts_directory, print_on_failure, print_on_success, + print_on_success_library, println_yellow_err, +}; use petgraph::{self, visit::EdgeRef, Directed, Direction}; use serde::{Deserialize, Serialize}; use std::{ @@ -19,7 +16,7 @@ use std::{ str::FromStr, }; use sway_core::{ - source_map::SourceMap, BuildConfig, BytecodeCompilationResult, CompileAstResult, NamespaceRef, + source_map::SourceMap, BytecodeCompilationResult, CompileAstResult, NamespaceRef, NamespaceWrapper, TreeType, TypedParseTree, }; use sway_types::JsonABI; @@ -124,18 +121,19 @@ pub enum SourcePinned { /// Represents the full build plan for a project. #[derive(Clone)] -pub(crate) struct BuildPlan { - pub(crate) graph: Graph, - pub(crate) path_map: PathMap, - pub(crate) compilation_order: Vec, +pub struct BuildPlan { + graph: Graph, + path_map: PathMap, + compilation_order: Vec, } -// Parameters to pass through to the `BuildConfig` during compilation. -pub(crate) struct BuildConf { - pub(crate) use_ir: bool, - pub(crate) print_ir: bool, - pub(crate) print_finalized_asm: bool, - pub(crate) print_intermediate_asm: bool, +/// Parameters to pass through to the `sway_core::BuildConfig` during compilation. +pub struct BuildConfig { + pub use_ir: bool, + pub print_ir: bool, + pub print_finalized_asm: bool, + pub print_intermediate_asm: bool, + pub silent: bool, } /// Error returned upon failed parsing of `SourceGitPinned::from_str`. @@ -150,7 +148,7 @@ pub enum SourceGitPinnedParseError { impl BuildPlan { /// Create a new build plan for the project by fetching and pinning dependenies. pub fn new(manifest_dir: &Path, offline: bool) -> Result { - let manifest = read_manifest(manifest_dir)?; + let manifest = Manifest::from_dir(manifest_dir)?; let (graph, path_map) = fetch_deps(manifest_dir.to_path_buf(), &manifest, offline)?; let compilation_order = compilation_order(&graph)?; Ok(Self { @@ -206,7 +204,7 @@ impl BuildPlan { // NOTE: Temporarily warn about `version` until we have support for registries. if let Dependency::Detailed(det) = dep { if det.version.is_some() { - crate::utils::helpers::println_yellow_err(&format!( + println_yellow_err(&format!( " WARNING! Dependency \"{}\" specifies the unused `version` field: \ consider using `branch` or `tag` instead", name @@ -228,6 +226,22 @@ impl BuildPlan { Ok(()) } + + /// View the build plan's compilation graph. + pub fn graph(&self) -> &Graph { + &self.graph + } + + /// View the build plan's map of pinned package IDs to the path containing a local copy of + /// their source. + pub fn path_map(&self) -> &PathMap { + &self.path_map + } + + /// The order in which nodes are compiled, determined via a toposort of the package graph. + pub fn compilation_order(&self) -> &[NodeIx] { + &self.compilation_order + } } impl Pinned { @@ -362,7 +376,7 @@ pub fn graph_to_path_map( .source(); let parent = &graph[parent_node]; let parent_path = &path_map[&parent.id()]; - let parent_manifest = read_manifest(parent_path)?; + let parent_manifest = Manifest::from_dir(parent_path)?; let detailed = parent_manifest .dependencies .as_ref() @@ -442,7 +456,7 @@ fn fetch_children( ) -> Result<()> { let parent = &graph[node]; let parent_path = path_map[&parent.id()].clone(); - let manifest = read_manifest(&parent_path)?; + let manifest = Manifest::from_dir(&parent_path)?; let deps = match &manifest.dependencies { None => return Ok(()), Some(deps) => deps, @@ -677,15 +691,17 @@ fn dep_to_source(pkg_path: &Path, dep: &Dependency) -> Result { Ok(source) } -pub(crate) fn build_config( +/// Given a `forc_pkg::BuildConfig`, produce the necessary `sway_core::BuildConfig` required for +/// compilation. +pub fn sway_build_config( path: PathBuf, manifest: &Manifest, - build_conf: &BuildConf, -) -> Result { + build_conf: &BuildConfig, +) -> Result { // Prepare the build config to pass through to the compiler. - let main_path = find_main_path(&path, manifest); - let file_name = find_file_name(&path, &main_path)?; - let build_config = BuildConfig::root_from_file_name_and_manifest_path( + let entry_path = manifest.entry_path(&path); + let file_name = find_file_name(&path, &entry_path)?; + let build_config = sway_core::BuildConfig::root_from_file_name_and_manifest_path( file_name.to_path_buf(), path.to_path_buf(), ) @@ -699,7 +715,7 @@ pub(crate) fn build_config( /// Builds the dependency namespace for the package at the given node index within the graph. /// /// This function is designed to be called for each node in order of compilation. -pub(crate) fn dependency_namespace( +pub fn dependency_namespace( namespace_map: &HashMap, graph: &Graph, compilation_order: &[NodeIx], @@ -740,20 +756,20 @@ pub(crate) fn dependency_namespace( /// ### Script, Predicate /// /// Scripts and Predicates will be compiled to bytecode and will not emit any JSON ABI. -pub(crate) fn compile( +pub fn compile( pkg: &Pinned, pkg_path: &Path, - build_conf: &BuildConf, + build_config: &BuildConfig, namespace: NamespaceRef, source_map: &mut SourceMap, - silent_mode: bool, ) -> Result<(Compiled, Option)> { - let manifest = read_manifest(pkg_path)?; - let source = get_main_file(&manifest, pkg_path)?; - let build_config = build_config(pkg_path.to_path_buf(), &manifest, build_conf)?; + let manifest = Manifest::from_dir(pkg_path)?; + let source = manifest.entry_string(pkg_path)?; + let sway_build_config = sway_build_config(pkg_path.to_path_buf(), &manifest, build_config)?; + let silent_mode = build_config.silent; // First, compile to an AST. We'll update the namespace and check for JSON ABI output. - let ast_res = sway_core::compile_to_ast(source, namespace, &build_config); + let ast_res = sway_core::compile_to_ast(source, namespace, &sway_build_config); match &ast_res { CompileAstResult::Failure { warnings, errors } => { print_on_failure(silent_mode, warnings, errors); @@ -779,7 +795,7 @@ pub(crate) fn compile( // For all other program types, we'll compile the bytecode. TreeType::Contract | TreeType::Predicate | TreeType::Script => { let tree_type = tree_type.clone(); - let asm_res = sway_core::ast_to_asm(ast_res, &build_config); + let asm_res = sway_core::ast_to_asm(ast_res, &sway_build_config); let bc_res = sway_core::asm_to_bytecode(asm_res, source_map); match bc_res { BytecodeCompilationResult::Success { bytes, warnings } => { @@ -802,6 +818,34 @@ pub(crate) fn compile( } } +/// Build an entire forc package and return the compiled output. +/// +/// This compiles all packages (including dependencies) in the order specified by the `BuildPlan`. +/// +/// Also returns the resulting `sway_core::SourceMap` which may be useful for debugging purposes. +pub fn build(plan: &BuildPlan, conf: &BuildConfig) -> anyhow::Result<(Compiled, SourceMap)> { + let mut namespace_map = Default::default(); + let mut source_map = SourceMap::new(); + let mut json_abi = vec![]; + let mut bytecode = vec![]; + for &node in &plan.compilation_order { + let dep_namespace = + dependency_namespace(&namespace_map, &plan.graph, &plan.compilation_order, node); + let pkg = &plan.graph[node]; + let path = &plan.path_map[&pkg.id()]; + let res = compile(pkg, path, conf, dep_namespace, &mut source_map)?; + let (compiled, maybe_namespace) = res; + if let Some(namespace) = maybe_namespace { + namespace_map.insert(node, namespace); + } + json_abi.extend(compiled.json_abi); + bytecode = compiled.bytecode; + source_map.insert_dependency(path.clone()); + } + let compiled = Compiled { bytecode, json_abi }; + Ok((compiled, source_map)) +} + // TODO: Update this to match behaviour described in the `compile` doc comment above. fn generate_json_abi(ast: &TypedParseTree) -> JsonABI { match ast { diff --git a/forc-util/Cargo.toml b/forc-util/Cargo.toml new file mode 100644 index 00000000000..5b1e25b4098 --- /dev/null +++ b/forc-util/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "forc-util" +version = "0.6.0" +authors = ["Fuel Labs "] +edition = "2021" +homepage = "https://fuel.network/" +license = "Apache-2.0" +repository = "https://github.com/FuelLabs/sway" +description = "Utility items shared between forc crates." + +[dependencies] +annotate-snippets = { version = "0.9", features = ["color"] } +anyhow = "1" +dirs = "3.0.2" +sway-core = { version = "0.6.0", path = "../sway-core" } +sway-utils = { version = "0.6.0", path = "../sway-utils" } +termcolor = "1.1" +unicode-xid = "0.2.2" diff --git a/forc/src/utils/helpers.rs b/forc-util/src/lib.rs similarity index 77% rename from forc/src/utils/helpers.rs rename to forc-util/src/lib.rs index 31a4502f32a..c1600bc7f66 100644 --- a/forc/src/utils/helpers.rs +++ b/forc-util/src/lib.rs @@ -1,19 +1,20 @@ -use super::manifest::Manifest; -use crate::utils::restricted_names; +//! Utility items shared between forc crates. + use annotate_snippets::{ display_list::{DisplayList, FormatOptions}, snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation}, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{bail, Result}; use std::ffi::OsStr; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::str; -use std::sync::Arc; use sway_core::{error::LineCol, CompileError, CompileWarning, TreeType}; use sway_utils::constants; use termcolor::{self, Color as TermColor, ColorChoice, ColorSpec, StandardStream, WriteColor}; +pub mod restricted; + pub const DEFAULT_OUTPUT_DIRECTORY: &str = "out"; pub fn is_sway_file(file: &Path) -> bool { @@ -21,17 +22,10 @@ pub fn is_sway_file(file: &Path) -> bool { Some(OsStr::new(constants::SWAY_EXTENSION)) == res } -pub fn find_main_path(manifest_dir: &Path, manifest: &Manifest) -> PathBuf { - let mut code_dir = manifest_dir.to_path_buf(); - code_dir.push(constants::SRC_DIR); - code_dir.push(&manifest.project.entry); - code_dir -} - -pub fn find_file_name<'sc>(manifest_dir: &Path, main_path: &'sc Path) -> Result<&'sc Path> { +pub fn find_file_name<'sc>(manifest_dir: &Path, entry_path: &'sc Path) -> Result<&'sc Path> { let mut file_path = manifest_dir.to_path_buf(); file_path.pop(); - let file_name = match main_path.strip_prefix(file_path.clone()) { + let file_name = match entry_path.strip_prefix(file_path.clone()) { Ok(o) => o, Err(err) => bail!(err), }; @@ -42,39 +36,17 @@ pub fn lock_path(manifest_dir: &Path) -> PathBuf { manifest_dir.join(constants::LOCK_FILE_NAME) } -pub fn read_manifest(manifest_dir: &Path) -> Result { - let manifest_path = { - let mut man = PathBuf::from(manifest_dir); - man.push(constants::MANIFEST_FILE_NAME); - man - }; - let manifest_path_str = format!("{:?}", manifest_path); - let manifest = match std::fs::read_to_string(manifest_path) { - Ok(o) => o, - Err(e) => { - bail!("failed to read manifest at {:?}: {}", manifest_path_str, e) - } - }; - - let manifest = match toml::from_str(&manifest) { - Ok(o) => Ok(o), - Err(e) => Err(anyhow!("Error parsing manifest: {}.", e)), - }?; - - validate_manifest(manifest) -} - // Using (https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/util/toml/mod.rs + // https://github.com/rust-lang/cargo/blob/489b66f2e458404a10d7824194d3ded94bc1f4e4/src/cargo/ops/cargo_new.rs) for reference -fn validate_name(name: &str, use_case: &str) -> Result<()> { +pub fn validate_name(name: &str, use_case: &str) -> Result<()> { // if true returns formatted error - restricted_names::contains_invalid_char(name, use_case)?; + restricted::contains_invalid_char(name, use_case)?; - if restricted_names::is_keyword(name) { + if restricted::is_keyword(name) { bail!("the name `{name}` cannot be used as a package name, it is a Sway keyword"); } - if restricted_names::is_conflicting_artifact_name(name) { + if restricted::is_conflicting_artifact_name(name) { bail!( "the name `{name}` cannot be used as a package name, \ it conflicts with Forc's build directory names" @@ -86,13 +58,13 @@ fn validate_name(name: &str, use_case: &str) -> Result<()> { it conflicts with Sway's built-in test library" ); } - if restricted_names::is_conflicting_suffix(name) { + if restricted::is_conflicting_suffix(name) { bail!( "the name `{name}` is part of Sway's standard library\n\ It is recommended to use a different name to avoid problems." ); } - if restricted_names::is_windows_reserved(name) { + if restricted::is_windows_reserved(name) { if cfg!(windows) { bail!("cannot use name `{name}`, it is a reserved Windows filename"); } else { @@ -102,35 +74,12 @@ fn validate_name(name: &str, use_case: &str) -> Result<()> { ); } } - if restricted_names::is_non_ascii_name(name) { + if restricted::is_non_ascii_name(name) { bail!("the name `{name}` contains non-ASCII characters which are unsupported"); } Ok(()) } -fn validate_manifest(manifest: Manifest) -> Result { - validate_name(&manifest.project.name, "package name")?; - if let Some(ref org) = manifest.project.organization { - validate_name(org, "organization name")?; - } - - Ok(manifest) -} - -pub fn get_main_file(manifest_of_dep: &Manifest, manifest_dir: &Path) -> Result> { - let main_path = { - let mut code_dir = PathBuf::from(manifest_dir); - code_dir.push(constants::SRC_DIR); - code_dir.push(&manifest_of_dep.project.entry); - code_dir - }; - - // some hackery to get around lifetimes for now, until the AST returns a non-lifetime-bound AST - let main_file = std::fs::read_to_string(&main_path).map_err(|e| e)?; - let main_file = Arc::from(main_file); - Ok(main_file) -} - pub fn default_output_directory(manifest_dir: &Path) -> PathBuf { manifest_dir.join(DEFAULT_OUTPUT_DIRECTORY) } @@ -218,33 +167,6 @@ pub fn print_on_failure(silent_mode: bool, warnings: &[CompileWarning], errors: .unwrap(); } -pub(crate) fn print_lock_diff(proj_name: &str, diff: &crate::lock::Diff) { - print_removed_pkgs(proj_name, diff.removed.iter().cloned()); - print_added_pkgs(proj_name, diff.added.iter().cloned()); -} - -pub(crate) fn print_removed_pkgs<'a, I>(proj_name: &str, removed: I) -where - I: IntoIterator, -{ - for pkg in removed { - if pkg.name != proj_name { - let _ = println_red(&format!(" Removing {}", pkg.unique_string())); - } - } -} - -pub(crate) fn print_added_pkgs<'a, I>(proj_name: &str, removed: I) -where - I: IntoIterator, -{ - for pkg in removed { - if pkg.name != proj_name { - let _ = println_green(&format!(" Adding {}", pkg.unique_string())); - } - } -} - pub fn println_red(txt: &str) -> io::Result<()> { println_std_out(txt, TermColor::Red) } @@ -269,7 +191,7 @@ pub fn println_green_err(txt: &str) -> io::Result<()> { println_std_err(txt, TermColor::Green) } -fn print_std_out(txt: &str, color: TermColor) -> io::Result<()> { +pub fn print_std_out(txt: &str, color: TermColor) -> io::Result<()> { let stdout = StandardStream::stdout(ColorChoice::Always); print_with_color(txt, color, stdout) } diff --git a/forc/src/utils/restricted_names.rs b/forc-util/src/restricted.rs similarity index 100% rename from forc/src/utils/restricted_names.rs rename to forc-util/src/restricted.rs diff --git a/forc/Cargo.toml b/forc/Cargo.toml index 132151ead23..3b06928099b 100644 --- a/forc/Cargo.toml +++ b/forc/Cargo.toml @@ -10,22 +10,19 @@ description = "Fuel Orchestrator." [dependencies] annotate-snippets = { version = "0.9", features = ["color"] } -ansi_term = "0.12" anyhow = "1.0.41" clap = { version = "3.1", features = ["env", "derive"] } clap_complete = "3.1" -dirs = "3.0.2" +forc-pkg = { version = "0.6.0", path = "../forc-pkg" } +forc-util = { version = "0.6.0", path = "../forc-util" } fuel-asm = "0.2" fuel-gql-client = { version = "0.4", default-features = false } fuel-tx = "0.6" fuel-vm = "0.5" futures = "0.3" -git2 = "0.14" hex = "0.4.3" -petgraph = { version = "0.6.0", features = ["serde-1"] } prettydiff = "0.5.0" reqwest = { version = "0.11.4", default-features = false, features = ["json", "rustls-tls"] } -semver = "1.0.3" serde = {version = "1.0", features = ["derive"]} serde_json = "1.0.73" sway-core = { version = "0.6.0", path = "../sway-core" } @@ -36,11 +33,9 @@ sway-types = { version = "0.6.0", path = "../sway-types" } taplo = "0.7" tar = "0.4.35" term-table = "1.3" -termcolor = "1.1" tokio = { version = "1.8.0", features = ["macros", "rt-multi-thread", "process"] } toml = "0.5" toml_edit = "0.13" -unicode-xid = "0.2.2" ureq = { version = "2.4", features = ["json"] } url = "2.2" uwuify = { version = "^0.2", optional = true } diff --git a/forc/src/cli/commands/addr2line.rs b/forc/src/cli/commands/addr2line.rs old mode 100755 new mode 100644 diff --git a/forc/src/lib.rs b/forc/src/lib.rs index 06324a3fc1f..7ef50bf343a 100644 --- a/forc/src/lib.rs +++ b/forc/src/lib.rs @@ -1,8 +1,6 @@ #![allow(dead_code)] mod cli; -mod lock; mod ops; -mod pkg; mod utils; #[cfg(feature = "test")] @@ -14,6 +12,5 @@ pub mod test { #[cfg(feature = "util")] pub mod util { pub use crate::utils::client::start_fuel_core; - pub use crate::utils::helpers; pub use sway_utils::constants; } diff --git a/forc/src/main.rs b/forc/src/main.rs index db2b13c0ac7..c2a1b119259 100644 --- a/forc/src/main.rs +++ b/forc/src/main.rs @@ -1,8 +1,6 @@ #![allow(warnings)] mod cli; -mod lock; mod ops; -mod pkg; mod utils; use anyhow::Result; diff --git a/forc/src/ops/forc_build.rs b/forc/src/ops/forc_build.rs index b302199cc3f..98e60090e2a 100644 --- a/forc/src/ops/forc_build.rs +++ b/forc/src/ops/forc_build.rs @@ -1,16 +1,11 @@ -use crate::{ - cli::BuildCommand, - lock::Lock, - pkg, - utils::helpers::{default_output_directory, lock_path, print_lock_diff, read_manifest}, -}; +use crate::cli::BuildCommand; use anyhow::{anyhow, bail, Result}; +use forc_pkg::{self as pkg, lock, Lock, Manifest}; +use forc_util::{default_output_directory, lock_path}; use std::{ fs::{self, File}, - io::Write, path::PathBuf, }; -use sway_core::source_map::SourceMap; use sway_utils::{find_manifest_dir, MANIFEST_FILE_NAME}; pub fn build(command: BuildCommand) -> Result { @@ -23,16 +18,17 @@ pub fn build(command: BuildCommand) -> Result { print_intermediate_asm, print_ir, offline_mode: offline, - silent_mode: silent, + silent_mode, output_directory, minify_json_abi, } = command; - let build_conf = pkg::BuildConf { + let config = pkg::BuildConfig { use_ir, print_ir, print_finalized_asm, print_intermediate_asm, + silent: silent_mode, }; // find manifest directory, even if in subdirectory @@ -52,7 +48,7 @@ pub fn build(command: BuildCommand) -> Result { ); } }; - let manifest = read_manifest(&manifest_dir)?; + let manifest = Manifest::from_dir(&manifest_dir)?; let lock_path = lock_path(&manifest_dir); // Load the build plan from the lock file. @@ -62,7 +58,7 @@ pub fn build(command: BuildCommand) -> Result { let old_lock = plan_result .as_ref() .ok() - .map(|plan| Lock::from_graph(&plan.graph)) + .map(|plan| Lock::from_graph(plan.graph())) .unwrap_or_default(); // Validate the loaded build plan for the current manifest. @@ -73,9 +69,9 @@ pub fn build(command: BuildCommand) -> Result { println!(" Creating a new `Forc.lock` file"); println!(" Cause: {}", e); let plan = pkg::BuildPlan::new(&manifest_dir, offline)?; - let lock = Lock::from_graph(&plan.graph); + let lock = Lock::from_graph(plan.graph()); let diff = lock.diff(&old_lock); - print_lock_diff(&manifest.project.name, &diff); + lock::print_diff(&manifest.project.name, &diff); let string = toml::ser::to_string_pretty(&lock) .map_err(|e| anyhow!("failed to serialize lock file: {}", e))?; fs::write(&lock_path, &string).map_err(|e| anyhow!("failed to write lock file: {}", e))?; @@ -83,43 +79,16 @@ pub fn build(command: BuildCommand) -> Result { Ok(plan) })?; - // Iterate over and compile all packages. - let mut namespace_map = Default::default(); - let mut source_map = SourceMap::new(); - let mut json_abi = vec![]; - let mut bytecode = vec![]; - for &node in &plan.compilation_order { - let dep_namespace = - pkg::dependency_namespace(&namespace_map, &plan.graph, &plan.compilation_order, node); - let pkg = &plan.graph[node]; - let path = &plan.path_map[&pkg.id()]; - let res = pkg::compile( - pkg, - path, - &build_conf, - dep_namespace, - &mut source_map, - silent, - )?; - let (compiled, maybe_namespace) = res; - if let Some(namespace) = maybe_namespace { - namespace_map.insert(node, namespace); - } - json_abi.extend(compiled.json_abi); - bytecode = compiled.bytecode; - source_map.insert_dependency(path.clone()); - } + // Build it! + let (compiled, source_map) = pkg::build(&plan, &config)?; if let Some(outfile) = binary_outfile { - let mut file = File::create(outfile)?; - file.write_all(bytecode.as_slice())?; + fs::write(&outfile, &compiled.bytecode)?; } if let Some(outfile) = debug_outfile { - fs::write( - outfile, - &serde_json::to_vec(&source_map).expect("JSON serialization failed"), - )?; + let source_map_json = serde_json::to_vec(&source_map).expect("JSON serialization failed"); + fs::write(outfile, &source_map_json)?; } // TODO: We may support custom build profiles in the future. @@ -137,20 +106,20 @@ pub fn build(command: BuildCommand) -> Result { let bin_path = output_dir .join(&manifest.project.name) .with_extension("bin"); - std::fs::write(&bin_path, bytecode.as_slice())?; - if !json_abi.is_empty() { + fs::write(&bin_path, &compiled.bytecode)?; + if !compiled.json_abi.is_empty() { let json_abi_stem = format!("{}-abi", manifest.project.name); let json_abi_path = output_dir.join(&json_abi_stem).with_extension("json"); let file = File::create(json_abi_path)?; let res = if minify_json_abi { - serde_json::to_writer(&file, &json_abi) + serde_json::to_writer(&file, &compiled.json_abi) } else { - serde_json::to_writer_pretty(&file, &json_abi) + serde_json::to_writer_pretty(&file, &compiled.json_abi) }; res?; } - println!(" Bytecode size is {} bytes.", bytecode.len()); + println!(" Bytecode size is {} bytes.", compiled.bytecode.len()); - Ok(pkg::Compiled { bytecode, json_abi }) + Ok(compiled) } diff --git a/forc/src/ops/forc_clean.rs b/forc/src/ops/forc_clean.rs index f6ac3ed4934..2fc0f16a5e7 100644 --- a/forc/src/ops/forc_clean.rs +++ b/forc/src/ops/forc_clean.rs @@ -1,5 +1,6 @@ -use crate::{cli::CleanCommand, utils::helpers::default_output_directory}; +use crate::cli::CleanCommand; use anyhow::{anyhow, bail, Result}; +use forc_util::default_output_directory; use std::{path::PathBuf, process}; use sway_utils::{find_manifest_dir, MANIFEST_FILE_NAME}; diff --git a/forc/src/ops/forc_deploy.rs b/forc/src/ops/forc_deploy.rs index ade1604a828..2590d1be53e 100644 --- a/forc/src/ops/forc_deploy.rs +++ b/forc/src/ops/forc_deploy.rs @@ -1,16 +1,13 @@ -use fuel_gql_client::client::FuelClient; -use fuel_tx::{Output, Salt, Transaction}; -use fuel_vm::prelude::*; -use sway_core::{parse, TreeType}; - use crate::cli::{BuildCommand, DeployCommand}; use crate::ops::forc_build; use crate::utils::cli_error::CliError; use anyhow::Result; - -use crate::utils::helpers; -use helpers::{get_main_file, read_manifest}; +use forc_pkg::Manifest; +use fuel_gql_client::client::FuelClient; +use fuel_tx::{Output, Salt, Transaction}; +use fuel_vm::prelude::*; use std::path::PathBuf; +use sway_core::{parse, TreeType}; use sway_utils::{constants::*, find_manifest_dir}; pub async fn deploy(command: DeployCommand) -> Result { @@ -36,12 +33,12 @@ pub async fn deploy(command: DeployCommand) -> Result { - let manifest = read_manifest(&manifest_dir)?; + let manifest = Manifest::from_dir(&manifest_dir)?; let project_name = &manifest.project.name; - let main_file = get_main_file(&manifest, &manifest_dir)?; + let entry_string = manifest.entry_string(&manifest_dir)?; - // parse the main file and check is it a contract - let parsed_result = parse(main_file, None); + // Parse the main file and check is it a contract. + let parsed_result = parse(entry_string, None); match parsed_result.value { Some(parse_tree) => match parse_tree.tree_type { TreeType::Contract => { diff --git a/forc/src/ops/forc_explorer.rs b/forc/src/ops/forc_explorer.rs index 001ad17838f..e96b3a2619f 100644 --- a/forc/src/ops/forc_explorer.rs +++ b/forc/src/ops/forc_explorer.rs @@ -1,15 +1,13 @@ +use crate::cli::ExplorerCommand; +use forc_util::{println_green, user_forc_directory}; +use reqwest; +use serde::Deserialize; use std::fs::{create_dir_all, remove_dir_all, remove_file, rename, File}; use std::io::Cursor; use std::path::PathBuf; - -use crate::utils::helpers::user_forc_directory; -use ansi_term::Colour; -use reqwest; -use serde::Deserialize; use tar::Archive; use warp::Filter; -use crate::cli::ExplorerCommand; type DownloadResult = std::result::Result>; #[derive(Deserialize, Debug)] @@ -80,7 +78,7 @@ async fn exec_start(command: ExplorerCommand) -> Result<(), reqwest::Error> { let releases = get_github_releases().await?; let version = get_latest_release_name(releases.as_slice()); let message = format!("Fuel Network Explorer {}", version); - println!("{}", Colour::Green.paint(message)); + let _ = println_green(&message); let is_downloaded = check_version_path(version); if !is_downloaded { diff --git a/forc/src/ops/forc_fmt.rs b/forc/src/ops/forc_fmt.rs index d25edf738a7..06fd1cdef6d 100644 --- a/forc/src/ops/forc_fmt.rs +++ b/forc/src/ops/forc_fmt.rs @@ -1,6 +1,6 @@ use crate::cli::{BuildCommand, FormatCommand}; use crate::ops::forc_build; -use crate::utils::helpers::{println_green, println_red}; +use forc_util::{println_green, println_red}; use prettydiff::{basic::DiffOp, diff_lines}; use std::default::Default; use std::{fmt, fs, io, path::Path, sync::Arc}; diff --git a/forc/src/ops/forc_run.rs b/forc/src/ops/forc_run.rs index a9f8fa20189..c5304639711 100644 --- a/forc/src/ops/forc_run.rs +++ b/forc/src/ops/forc_run.rs @@ -1,19 +1,15 @@ +use crate::cli::{BuildCommand, RunCommand}; +use crate::ops::forc_build; +use crate::utils::cli_error::CliError; +use forc_pkg::Manifest; use fuel_gql_client::client::FuelClient; use fuel_tx::Transaction; use futures::TryFutureExt; - use std::path::PathBuf; use std::str::FromStr; use sway_core::{parse, TreeType}; -use tokio::process::Child; - -use crate::cli::{BuildCommand, RunCommand}; -use crate::ops::forc_build; -use crate::utils::cli_error::CliError; - -use crate::utils::helpers; -use helpers::{get_main_file, read_manifest}; use sway_utils::{constants::*, find_manifest_dir}; +use tokio::process::Child; pub async fn run(command: RunCommand) -> Result<(), CliError> { let path_dir = if let Some(path) = &command.path { @@ -24,12 +20,12 @@ pub async fn run(command: RunCommand) -> Result<(), CliError> { match find_manifest_dir(&path_dir) { Some(manifest_dir) => { - let manifest = read_manifest(&manifest_dir)?; + let manifest = Manifest::from_dir(&manifest_dir)?; let project_name = &manifest.project.name; - let main_file = get_main_file(&manifest, &manifest_dir)?; + let entry_string = manifest.entry_string(&manifest_dir)?; - // parse the main file and check is it a script - let parsed_result = parse(main_file, None); + // Parse the entry point string and check is it a script. + let parsed_result = parse(entry_string, None); match parsed_result.value { Some(parse_tree) => match parse_tree.tree_type { TreeType::Script => { diff --git a/forc/src/ops/forc_update.rs b/forc/src/ops/forc_update.rs index b1e84e5b8de..ed119da9d54 100644 --- a/forc/src/ops/forc_update.rs +++ b/forc/src/ops/forc_update.rs @@ -1,10 +1,7 @@ -use crate::{ - cli::UpdateCommand, - lock::Lock, - pkg, - utils::helpers::{lock_path, print_lock_diff, read_manifest}, -}; +use crate::cli::UpdateCommand; use anyhow::{anyhow, Result}; +use forc_pkg::{self as pkg, lock, Lock, Manifest}; +use forc_util::lock_path; use std::{fs, path::PathBuf}; use sway_utils::find_manifest_dir; @@ -44,14 +41,14 @@ pub async fn update(command: UpdateCommand) -> Result<()> { } }; - let manifest = read_manifest(&manifest_dir).map_err(|e| anyhow!("{}", e))?; + let manifest = Manifest::from_dir(&manifest_dir)?; let lock_path = lock_path(&manifest_dir); let old_lock = Lock::from_path(&lock_path).ok().unwrap_or_default(); let offline = false; - let new_plan = pkg::BuildPlan::new(&manifest_dir, offline).map_err(|e| anyhow!("{}", e))?; - let new_lock = Lock::from_graph(&new_plan.graph); + let new_plan = pkg::BuildPlan::new(&manifest_dir, offline)?; + let new_lock = Lock::from_graph(new_plan.graph()); let diff = new_lock.diff(&old_lock); - print_lock_diff(&manifest.project.name, &diff); + lock::print_diff(&manifest.project.name, &diff); // If we're not only `check`ing, write the updated lock file. if !check { diff --git a/forc/src/utils/defaults.rs b/forc/src/utils/defaults.rs index 9157bffe54c..fc8b7f5c9cc 100644 --- a/forc/src/utils/defaults.rs +++ b/forc/src/utils/defaults.rs @@ -25,7 +25,7 @@ pub(crate) fn default_tests_manifest(project_name: &str) -> String { let real_name = whoami::realname(); format!( - r#"[package] + r#"[project] authors = ["{real_name}"] edition = "2021" license = "Apache-2.0" @@ -76,3 +76,19 @@ target "# .into() } + +#[test] +fn parse_default_manifest() { + println!( + "{:#?}", + toml::from_str::(&default_manifest("test_proj")).unwrap() + ) +} + +#[test] +fn parse_default_tests_manifest() { + println!( + "{:#?}", + toml::from_str::(&default_tests_manifest("test_proj")).unwrap() + ) +} diff --git a/forc/src/utils/dependency.rs b/forc/src/utils/dependency.rs deleted file mode 100644 index c2d495f9cec..00000000000 --- a/forc/src/utils/dependency.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::utils::manifest::Manifest; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -// A collection of remote dependency related functions - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum Dependency { - /// In the simple format, only a version is specified, eg. - /// `package = ""` - Simple(String), - /// The simple format is equivalent to a detailed dependency - /// specifying only a version, eg. - /// `package = { version = "" }` - Detailed(DependencyDetails), -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct DependencyDetails { - pub(crate) version: Option, - pub(crate) path: Option, - pub(crate) git: Option, - pub(crate) branch: Option, - pub(crate) tag: Option, -} -pub enum OfflineMode { - Yes, - No, -} - -impl From for OfflineMode { - fn from(v: bool) -> OfflineMode { - match v { - true => OfflineMode::Yes, - false => OfflineMode::No, - } - } -} - -// Helper to get only detailed dependencies (`Dependency::Detailed`). -pub fn get_detailed_dependencies(manifest: &mut Manifest) -> HashMap { - let mut dependencies: HashMap = HashMap::new(); - - if let Some(ref mut deps) = manifest.dependencies { - for (dep_name, dependency_details) in deps.iter_mut() { - match dependency_details { - Dependency::Simple(..) => continue, - Dependency::Detailed(dep_details) => { - dependencies.insert(dep_name.to_owned(), dep_details) - } - }; - } - } - - dependencies -} diff --git a/forc/src/utils/manifest.rs b/forc/src/utils/manifest.rs deleted file mode 100644 index 3a615330097..00000000000 --- a/forc/src/utils/manifest.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::utils::dependency::Dependency; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; - -use sway_utils::constants::DEFAULT_NODE_URL; - -// using https://github.com/rust-lang/cargo/blob/master/src/cargo/util/toml/mod.rs as the source of -// implementation strategy - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct Manifest { - pub project: Project, - pub network: Option, - pub dependencies: Option>, -} - -impl Manifest {} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct Project { - #[deprecated = "use the authors field instead, the author field will be removed soon."] - pub author: Option, - pub authors: Option>, - pub name: String, - pub organization: Option, - pub license: String, - #[serde(default = "default_entry")] - pub entry: String, -} - -fn default_entry() -> String { - "main.sw".into() -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub struct Network { - #[serde(default = "default_url")] - pub url: String, -} - -fn default_url() -> String { - DEFAULT_NODE_URL.into() -} - -#[test] -fn try_parse() { - println!( - "{:#?}", - toml::from_str::(&super::defaults::default_manifest("test_proj")).unwrap() - ) -} diff --git a/forc/src/utils/mod.rs b/forc/src/utils/mod.rs index 90af9b3bfa8..425a05bb2ea 100644 --- a/forc/src/utils/mod.rs +++ b/forc/src/utils/mod.rs @@ -1,7 +1,3 @@ pub mod cli_error; pub mod client; pub mod defaults; -pub mod dependency; -pub mod helpers; -pub mod manifest; -pub mod restricted_names;