Skip to content

Commit

Permalink
Add forc check command (#2026)
Browse files Browse the repository at this point in the history
* wip

* moving back to PC computer

* adding check function to forc pkg

* have ast returning from forc pkg

* can now successfully parse all sway examples

* fmt

* added forc check

* tidy up lsp tests

* add forc check command

* forc ops doesnt need to be public

* tidy up lsp tests

* remove non relevant code

* rebase on master

* add Cargo.lock file

* add forc check to mdbook
  • Loading branch information
JoshuaBatty authored Jun 22, 2022
1 parent 44a16af commit bdfb744
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 125 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
- [Commands](./forc/commands/index.md)
- [forc addr2line](./forc/commands/forc_addr2line.md)
- [forc build](./forc/commands/forc_build.md)
- [forc check](./forc/commands/forc_check.md)
- [forc clean](./forc/commands/forc_clean.md)
- [forc completions](./forc/commands/forc_completions.md)
- [forc deploy](./forc/commands/forc_deploy.md)
Expand Down
1 change: 1 addition & 0 deletions docs/src/forc/commands/forc_check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# forc check
132 changes: 129 additions & 3 deletions forc-pkg/src/pkg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use petgraph::{
use serde::{Deserialize, Serialize};
use std::{
collections::{hash_map, BTreeSet, HashMap, HashSet},
fmt,
fmt, fs,
hash::{Hash, Hasher},
path::{Path, PathBuf},
str::FromStr,
Expand Down Expand Up @@ -235,6 +235,68 @@ impl BuildPlan {
})
}

/// Create a new build plan taking into account the state of both the Manifest and the existing lock file if there is one.
///
/// This will first attempt to load a build plan from the lock file and validate the resulting graph using the current state of the Manifest.
///
/// This includes checking if the [dependencies] or [patch] tables have changed and checking the validity of the local path dependencies.
/// If any changes are detected, the graph is updated and any new packages that require fetching are fetched.
///
/// The resulting build plan should always be in a valid state that is ready for building or checking.
pub fn load_from_manifest(
manifest: &ManifestFile,
locked: bool,
offline: bool,
sway_git_tag: &str,
) -> Result<Self> {
let lock_path = forc_util::lock_path(manifest.dir());
let plan_result = BuildPlan::from_lock_file(&lock_path, sway_git_tag);

// Retrieve the old lock file state so we can produce a diff.
let old_lock = plan_result
.as_ref()
.ok()
.map(|plan| Lock::from_graph(plan.graph()))
.unwrap_or_default();

// Check if there are any errors coming from the BuildPlan generation from the lock file
// If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything
let mut new_lock_cause = None;
let mut plan = plan_result.or_else(|e| -> Result<BuildPlan> {
if locked {
bail!(
"The lock file {} needs to be updated but --locked was passed to prevent this.",
lock_path.to_string_lossy()
);
}
new_lock_cause = if e.to_string().contains("No such file or directory") {
Some(anyhow!("lock file did not exist"))
} else {
Some(e)
};
let plan = BuildPlan::new(manifest, sway_git_tag, offline)?;
Ok(plan)
})?;

// If there are no issues with the BuildPlan generated from the lock file
// Check and apply the diff.
if new_lock_cause.is_none() {
let diff = plan.validate(manifest, sway_git_tag)?;
if !diff.added.is_empty() || !diff.removed.is_empty() {
new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`"));
plan = plan.apply_pkg_diff(diff, sway_git_tag, offline)?;
}
}

if let Some(cause) = new_lock_cause {
info!(" Creating a new `Forc.lock` file. (Cause: {})", cause);
create_new_lock(&plan, &old_lock, manifest, &lock_path)?;
info!(" Created new lock file at {}", lock_path.display());
}

Ok(plan)
}

/// Create a new build plan from an existing one. Needs the difference with the existing plan with the lock.
pub fn apply_pkg_diff(
&self,
Expand Down Expand Up @@ -1316,6 +1378,19 @@ pub fn dependency_namespace(
namespace
}

/// Compiles the package to an AST.
pub fn compile_ast(
manifest: &ManifestFile,
build_config: &BuildConfig,
namespace: namespace::Module,
) -> Result<CompileAstResult> {
let source = manifest.entry_string()?;
let sway_build_config =
sway_build_config(manifest.dir(), &manifest.entry_path(), build_config)?;
let ast_res = sway_core::compile_to_ast(source, namespace, Some(&sway_build_config));
Ok(ast_res)
}

/// Compiles the given package.
///
/// ## Program Types
Expand All @@ -1342,12 +1417,11 @@ pub fn compile(
source_map: &mut SourceMap,
) -> Result<(Compiled, Option<namespace::Root>)> {
let entry_path = manifest.entry_path();
let source = manifest.entry_string()?;
let sway_build_config = sway_build_config(manifest.dir(), &entry_path, 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, Some(&sway_build_config));
let ast_res = compile_ast(manifest, build_config, namespace)?;
match &ast_res {
CompileAstResult::Failure { warnings, errors } => {
print_on_failure(silent_mode, warnings, errors);
Expand Down Expand Up @@ -1444,6 +1518,43 @@ pub fn build(
Ok((compiled, source_map))
}

/// Compile the entire forc package and return a CompileAstResult.
pub fn check(
plan: &BuildPlan,
silent_mode: bool,
sway_git_tag: &str,
) -> anyhow::Result<CompileAstResult> {
let conf = &BuildConfig {
print_ir: false,
print_finalized_asm: false,
print_intermediate_asm: false,
silent: silent_mode,
};

let mut namespace_map = Default::default();
let mut source_map = SourceMap::new();
for (i, &node) in plan.compilation_order.iter().enumerate() {
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 manifest = ManifestFile::from_dir(path, sway_git_tag)?;
let ast_res = compile_ast(&manifest, conf, dep_namespace)?;
if let CompileAstResult::Success { typed_program, .. } = &ast_res {
if let TreeType::Library { .. } = typed_program.kind.tree_type() {
namespace_map.insert(node, typed_program.root.namespace.clone());
}
}
source_map.insert_dependency(path.clone());

// We only need to return the final CompileAstResult
if i == plan.compilation_order.len() - 1 {
return Ok(ast_res);
}
}
bail!("unable to check sway program: build plan contains no packages")
}

/// Attempt to find a `Forc.toml` with the given project name within the given directory.
///
/// Returns the path to the package on success, or `None` in the case it could not be found.
Expand Down Expand Up @@ -1565,3 +1676,18 @@ pub fn fuel_core_not_running(node_url: &str) -> anyhow::Error {
let message = format!("could not get a response from node at the URL {}. Start a node with `fuel-core`. See https://github.com/FuelLabs/fuel-core#running for more information", node_url);
Error::msg(message)
}

fn create_new_lock(
plan: &BuildPlan,
old_lock: &Lock,
manifest: &ManifestFile,
lock_path: &Path,
) -> Result<()> {
let lock = Lock::from_graph(plan.graph());
let diff = lock.diff(old_lock);
super::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))?;
Ok(())
}
30 changes: 30 additions & 0 deletions forc/src/cli/commands/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use crate::ops::forc_check;
use anyhow::Result;
use clap::Parser;

/// Check the current or target project and all of its dependencies for errors.
///
/// This will essentially compile the packages without performing the final step of code generation,
/// which is faster than running forc build.
#[derive(Debug, Default, Parser)]
pub struct Command {
/// Path to the project, if not specified, current working directory will be used.
#[clap(short, long)]
pub path: Option<String>,
/// Offline mode, prevents Forc from using the network when managing dependencies.
/// Meaning it will only try to use previously downloaded dependencies.
#[clap(long = "offline")]
pub offline_mode: bool,
/// Silent mode. Don't output any warnings or errors to the command line.
#[clap(long = "silent", short = 's')]
pub silent_mode: bool,
/// Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it
/// needs to be updated, Forc will exit with an error
#[clap(long)]
pub locked: bool,
}

pub(crate) fn exec(command: Command) -> Result<()> {
forc_check::check(command)?;
Ok(())
}
1 change: 1 addition & 0 deletions forc/src/cli/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod addr2line;
pub mod build;
pub mod check;
pub mod clean;
pub mod completions;
pub mod deploy;
Expand Down
7 changes: 5 additions & 2 deletions forc/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use self::commands::{
addr2line, build, clean, completions, deploy, init, json_abi, parse_bytecode, plugins, run,
template, test, update,
addr2line, build, check, clean, completions, deploy, init, json_abi, parse_bytecode, plugins,
run, template, test, update,
};
use addr2line::Command as Addr2LineCommand;
use anyhow::{anyhow, Result};
pub use build::Command as BuildCommand;
pub use check::Command as CheckCommand;
use clap::Parser;
pub use clean::Command as CleanCommand;
pub use completions::Command as CompletionsCommand;
Expand Down Expand Up @@ -35,6 +36,7 @@ enum Forc {
Addr2Line(Addr2LineCommand),
#[clap(visible_alias = "b")]
Build(BuildCommand),
Check(CheckCommand),
Clean(CleanCommand),
Completions(CompletionsCommand),
Deploy(DeployCommand),
Expand Down Expand Up @@ -64,6 +66,7 @@ pub async fn run_cli() -> Result<()> {
match opt.command {
Forc::Addr2Line(command) => addr2line::exec(command),
Forc::Build(command) => build::exec(command),
Forc::Check(command) => check::exec(command),
Forc::Clean(command) => clean::exec(command),
Forc::Completions(command) => completions::exec(command),
Forc::Deploy(command) => deploy::exec(command).await,
Expand Down
2 changes: 1 addition & 1 deletion forc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod cli;
mod ops;
mod utils;
pub mod utils;

#[cfg(feature = "test")]
pub mod test {
Expand Down
71 changes: 6 additions & 65 deletions forc/src/ops/forc_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ use crate::{
cli::BuildCommand,
utils::{SWAY_BIN_HASH_SUFFIX, SWAY_BIN_ROOT_SUFFIX, SWAY_GIT_TAG},
};
use anyhow::{anyhow, bail, Result};
use forc_pkg::{self as pkg, lock, Lock, ManifestFile};
use forc_util::{default_output_directory, lock_path};
use anyhow::Result;
use forc_pkg::{self as pkg, ManifestFile};
use forc_util::default_output_directory;
use fuel_tx::Contract;
use std::{
fs::{self, File},
path::{Path, PathBuf},
path::PathBuf,
};
use sway_core::TreeType;
use tracing::{info, warn};
Expand Down Expand Up @@ -56,6 +56,8 @@ pub fn build(command: BuildCommand) -> Result<pkg::Compiled> {

let manifest = ManifestFile::from_dir(&this_dir, SWAY_GIT_TAG)?;

let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?;

// If any cli parameter is passed by the user it overrides the selected build profile.
let mut config = &pkg::BuildConfig {
print_ir,
Expand All @@ -80,52 +82,6 @@ pub fn build(command: BuildCommand) -> Result<pkg::Compiled> {
});
}

let lock_path = lock_path(manifest.dir());

let plan_result = pkg::BuildPlan::from_lock_file(&lock_path, SWAY_GIT_TAG);

// Retrieve the old lock file state so we can produce a diff.
let old_lock = plan_result
.as_ref()
.ok()
.map(|plan| Lock::from_graph(plan.graph()))
.unwrap_or_default();

// Check if there are any errors coming from the BuildPlan generation from the lock file
// If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything
let mut new_lock_cause = None;
let mut plan = plan_result.or_else(|e| -> Result<pkg::BuildPlan> {
if locked {
bail!(
"The lock file {} needs to be updated but --locked was passed to prevent this.",
lock_path.to_string_lossy()
);
}
new_lock_cause = if e.to_string().contains("No such file or directory") {
Some(anyhow!("lock file did not exist"))
} else {
Some(e)
};
let plan = pkg::BuildPlan::new(&manifest, SWAY_GIT_TAG, offline)?;
Ok(plan)
})?;

// If there are no issues with the BuildPlan generated from the lock file
// Check and apply the diff.
if new_lock_cause.is_none() {
let diff = plan.validate(&manifest, SWAY_GIT_TAG)?;
if !diff.added.is_empty() || !diff.removed.is_empty() {
new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`"));
plan = plan.apply_pkg_diff(diff, SWAY_GIT_TAG, offline)?;
}
}

if let Some(cause) = new_lock_cause {
info!(" Creating a new `Forc.lock` file. (Cause: {})", cause);
create_new_lock(&plan, &old_lock, &manifest, &lock_path)?;
info!(" Created new lock file at {}", lock_path.display());
}

// Build it!
let (compiled, source_map) = pkg::build(&plan, config, SWAY_GIT_TAG)?;

Expand Down Expand Up @@ -187,18 +143,3 @@ pub fn build(command: BuildCommand) -> Result<pkg::Compiled> {

Ok(compiled)
}

fn create_new_lock(
plan: &pkg::BuildPlan,
old_lock: &Lock,
manifest: &ManifestFile,
lock_path: &Path,
) -> Result<()> {
let lock = Lock::from_graph(plan.graph());
let diff = lock.diff(old_lock);
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))?;
Ok(())
}
23 changes: 23 additions & 0 deletions forc/src/ops/forc_check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use crate::{cli::CheckCommand, utils::SWAY_GIT_TAG};
use anyhow::Result;
use forc_pkg::{self as pkg, ManifestFile};
use std::path::PathBuf;

pub fn check(command: CheckCommand) -> Result<sway_core::CompileAstResult> {
let CheckCommand {
path,
offline_mode: offline,
silent_mode,
locked,
} = command;

let this_dir = if let Some(ref path) = path {
PathBuf::from(path)
} else {
std::env::current_dir()?
};
let manifest = ManifestFile::from_dir(&this_dir, SWAY_GIT_TAG)?;
let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?;

pkg::check(&plan, silent_mode, SWAY_GIT_TAG)
}
1 change: 1 addition & 0 deletions forc/src/ops/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod forc_abi_json;
pub mod forc_build;
pub mod forc_check;
pub mod forc_clean;
pub mod forc_deploy;
pub mod forc_init;
Expand Down
Loading

0 comments on commit bdfb744

Please sign in to comment.