Skip to content

Commit bdfb744

Browse files
authored
Add forc check command (#2026)
* 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
1 parent 44a16af commit bdfb744

File tree

14 files changed

+231
-125
lines changed

14 files changed

+231
-125
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
- [Commands](./forc/commands/index.md)
5757
- [forc addr2line](./forc/commands/forc_addr2line.md)
5858
- [forc build](./forc/commands/forc_build.md)
59+
- [forc check](./forc/commands/forc_check.md)
5960
- [forc clean](./forc/commands/forc_clean.md)
6061
- [forc completions](./forc/commands/forc_completions.md)
6162
- [forc deploy](./forc/commands/forc_deploy.md)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# forc check

forc-pkg/src/pkg.rs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use petgraph::{
1616
use serde::{Deserialize, Serialize};
1717
use std::{
1818
collections::{hash_map, BTreeSet, HashMap, HashSet},
19-
fmt,
19+
fmt, fs,
2020
hash::{Hash, Hasher},
2121
path::{Path, PathBuf},
2222
str::FromStr,
@@ -235,6 +235,68 @@ impl BuildPlan {
235235
})
236236
}
237237

238+
/// Create a new build plan taking into account the state of both the Manifest and the existing lock file if there is one.
239+
///
240+
/// 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.
241+
///
242+
/// This includes checking if the [dependencies] or [patch] tables have changed and checking the validity of the local path dependencies.
243+
/// If any changes are detected, the graph is updated and any new packages that require fetching are fetched.
244+
///
245+
/// The resulting build plan should always be in a valid state that is ready for building or checking.
246+
pub fn load_from_manifest(
247+
manifest: &ManifestFile,
248+
locked: bool,
249+
offline: bool,
250+
sway_git_tag: &str,
251+
) -> Result<Self> {
252+
let lock_path = forc_util::lock_path(manifest.dir());
253+
let plan_result = BuildPlan::from_lock_file(&lock_path, sway_git_tag);
254+
255+
// Retrieve the old lock file state so we can produce a diff.
256+
let old_lock = plan_result
257+
.as_ref()
258+
.ok()
259+
.map(|plan| Lock::from_graph(plan.graph()))
260+
.unwrap_or_default();
261+
262+
// Check if there are any errors coming from the BuildPlan generation from the lock file
263+
// If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything
264+
let mut new_lock_cause = None;
265+
let mut plan = plan_result.or_else(|e| -> Result<BuildPlan> {
266+
if locked {
267+
bail!(
268+
"The lock file {} needs to be updated but --locked was passed to prevent this.",
269+
lock_path.to_string_lossy()
270+
);
271+
}
272+
new_lock_cause = if e.to_string().contains("No such file or directory") {
273+
Some(anyhow!("lock file did not exist"))
274+
} else {
275+
Some(e)
276+
};
277+
let plan = BuildPlan::new(manifest, sway_git_tag, offline)?;
278+
Ok(plan)
279+
})?;
280+
281+
// If there are no issues with the BuildPlan generated from the lock file
282+
// Check and apply the diff.
283+
if new_lock_cause.is_none() {
284+
let diff = plan.validate(manifest, sway_git_tag)?;
285+
if !diff.added.is_empty() || !diff.removed.is_empty() {
286+
new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`"));
287+
plan = plan.apply_pkg_diff(diff, sway_git_tag, offline)?;
288+
}
289+
}
290+
291+
if let Some(cause) = new_lock_cause {
292+
info!(" Creating a new `Forc.lock` file. (Cause: {})", cause);
293+
create_new_lock(&plan, &old_lock, manifest, &lock_path)?;
294+
info!(" Created new lock file at {}", lock_path.display());
295+
}
296+
297+
Ok(plan)
298+
}
299+
238300
/// Create a new build plan from an existing one. Needs the difference with the existing plan with the lock.
239301
pub fn apply_pkg_diff(
240302
&self,
@@ -1316,6 +1378,19 @@ pub fn dependency_namespace(
13161378
namespace
13171379
}
13181380

1381+
/// Compiles the package to an AST.
1382+
pub fn compile_ast(
1383+
manifest: &ManifestFile,
1384+
build_config: &BuildConfig,
1385+
namespace: namespace::Module,
1386+
) -> Result<CompileAstResult> {
1387+
let source = manifest.entry_string()?;
1388+
let sway_build_config =
1389+
sway_build_config(manifest.dir(), &manifest.entry_path(), build_config)?;
1390+
let ast_res = sway_core::compile_to_ast(source, namespace, Some(&sway_build_config));
1391+
Ok(ast_res)
1392+
}
1393+
13191394
/// Compiles the given package.
13201395
///
13211396
/// ## Program Types
@@ -1342,12 +1417,11 @@ pub fn compile(
13421417
source_map: &mut SourceMap,
13431418
) -> Result<(Compiled, Option<namespace::Root>)> {
13441419
let entry_path = manifest.entry_path();
1345-
let source = manifest.entry_string()?;
13461420
let sway_build_config = sway_build_config(manifest.dir(), &entry_path, build_config)?;
13471421
let silent_mode = build_config.silent;
13481422

13491423
// First, compile to an AST. We'll update the namespace and check for JSON ABI output.
1350-
let ast_res = sway_core::compile_to_ast(source, namespace, Some(&sway_build_config));
1424+
let ast_res = compile_ast(manifest, build_config, namespace)?;
13511425
match &ast_res {
13521426
CompileAstResult::Failure { warnings, errors } => {
13531427
print_on_failure(silent_mode, warnings, errors);
@@ -1444,6 +1518,43 @@ pub fn build(
14441518
Ok((compiled, source_map))
14451519
}
14461520

1521+
/// Compile the entire forc package and return a CompileAstResult.
1522+
pub fn check(
1523+
plan: &BuildPlan,
1524+
silent_mode: bool,
1525+
sway_git_tag: &str,
1526+
) -> anyhow::Result<CompileAstResult> {
1527+
let conf = &BuildConfig {
1528+
print_ir: false,
1529+
print_finalized_asm: false,
1530+
print_intermediate_asm: false,
1531+
silent: silent_mode,
1532+
};
1533+
1534+
let mut namespace_map = Default::default();
1535+
let mut source_map = SourceMap::new();
1536+
for (i, &node) in plan.compilation_order.iter().enumerate() {
1537+
let dep_namespace =
1538+
dependency_namespace(&namespace_map, &plan.graph, &plan.compilation_order, node);
1539+
let pkg = &plan.graph[node];
1540+
let path = &plan.path_map[&pkg.id()];
1541+
let manifest = ManifestFile::from_dir(path, sway_git_tag)?;
1542+
let ast_res = compile_ast(&manifest, conf, dep_namespace)?;
1543+
if let CompileAstResult::Success { typed_program, .. } = &ast_res {
1544+
if let TreeType::Library { .. } = typed_program.kind.tree_type() {
1545+
namespace_map.insert(node, typed_program.root.namespace.clone());
1546+
}
1547+
}
1548+
source_map.insert_dependency(path.clone());
1549+
1550+
// We only need to return the final CompileAstResult
1551+
if i == plan.compilation_order.len() - 1 {
1552+
return Ok(ast_res);
1553+
}
1554+
}
1555+
bail!("unable to check sway program: build plan contains no packages")
1556+
}
1557+
14471558
/// Attempt to find a `Forc.toml` with the given project name within the given directory.
14481559
///
14491560
/// Returns the path to the package on success, or `None` in the case it could not be found.
@@ -1565,3 +1676,18 @@ pub fn fuel_core_not_running(node_url: &str) -> anyhow::Error {
15651676
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);
15661677
Error::msg(message)
15671678
}
1679+
1680+
fn create_new_lock(
1681+
plan: &BuildPlan,
1682+
old_lock: &Lock,
1683+
manifest: &ManifestFile,
1684+
lock_path: &Path,
1685+
) -> Result<()> {
1686+
let lock = Lock::from_graph(plan.graph());
1687+
let diff = lock.diff(old_lock);
1688+
super::lock::print_diff(&manifest.project.name, &diff);
1689+
let string = toml::ser::to_string_pretty(&lock)
1690+
.map_err(|e| anyhow!("failed to serialize lock file: {}", e))?;
1691+
fs::write(&lock_path, &string).map_err(|e| anyhow!("failed to write lock file: {}", e))?;
1692+
Ok(())
1693+
}

forc/src/cli/commands/check.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use crate::ops::forc_check;
2+
use anyhow::Result;
3+
use clap::Parser;
4+
5+
/// Check the current or target project and all of its dependencies for errors.
6+
///
7+
/// This will essentially compile the packages without performing the final step of code generation,
8+
/// which is faster than running forc build.
9+
#[derive(Debug, Default, Parser)]
10+
pub struct Command {
11+
/// Path to the project, if not specified, current working directory will be used.
12+
#[clap(short, long)]
13+
pub path: Option<String>,
14+
/// Offline mode, prevents Forc from using the network when managing dependencies.
15+
/// Meaning it will only try to use previously downloaded dependencies.
16+
#[clap(long = "offline")]
17+
pub offline_mode: bool,
18+
/// Silent mode. Don't output any warnings or errors to the command line.
19+
#[clap(long = "silent", short = 's')]
20+
pub silent_mode: bool,
21+
/// Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it
22+
/// needs to be updated, Forc will exit with an error
23+
#[clap(long)]
24+
pub locked: bool,
25+
}
26+
27+
pub(crate) fn exec(command: Command) -> Result<()> {
28+
forc_check::check(command)?;
29+
Ok(())
30+
}

forc/src/cli/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod addr2line;
22
pub mod build;
3+
pub mod check;
34
pub mod clean;
45
pub mod completions;
56
pub mod deploy;

forc/src/cli/mod.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use self::commands::{
2-
addr2line, build, clean, completions, deploy, init, json_abi, parse_bytecode, plugins, run,
3-
template, test, update,
2+
addr2line, build, check, clean, completions, deploy, init, json_abi, parse_bytecode, plugins,
3+
run, template, test, update,
44
};
55
use addr2line::Command as Addr2LineCommand;
66
use anyhow::{anyhow, Result};
77
pub use build::Command as BuildCommand;
8+
pub use check::Command as CheckCommand;
89
use clap::Parser;
910
pub use clean::Command as CleanCommand;
1011
pub use completions::Command as CompletionsCommand;
@@ -35,6 +36,7 @@ enum Forc {
3536
Addr2Line(Addr2LineCommand),
3637
#[clap(visible_alias = "b")]
3738
Build(BuildCommand),
39+
Check(CheckCommand),
3840
Clean(CleanCommand),
3941
Completions(CompletionsCommand),
4042
Deploy(DeployCommand),
@@ -64,6 +66,7 @@ pub async fn run_cli() -> Result<()> {
6466
match opt.command {
6567
Forc::Addr2Line(command) => addr2line::exec(command),
6668
Forc::Build(command) => build::exec(command),
69+
Forc::Check(command) => check::exec(command),
6770
Forc::Clean(command) => clean::exec(command),
6871
Forc::Completions(command) => completions::exec(command),
6972
Forc::Deploy(command) => deploy::exec(command).await,

forc/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
pub mod cli;
22
mod ops;
3-
mod utils;
3+
pub mod utils;
44

55
#[cfg(feature = "test")]
66
pub mod test {

forc/src/ops/forc_build.rs

Lines changed: 6 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ use crate::{
22
cli::BuildCommand,
33
utils::{SWAY_BIN_HASH_SUFFIX, SWAY_BIN_ROOT_SUFFIX, SWAY_GIT_TAG},
44
};
5-
use anyhow::{anyhow, bail, Result};
6-
use forc_pkg::{self as pkg, lock, Lock, ManifestFile};
7-
use forc_util::{default_output_directory, lock_path};
5+
use anyhow::Result;
6+
use forc_pkg::{self as pkg, ManifestFile};
7+
use forc_util::default_output_directory;
88
use fuel_tx::Contract;
99
use std::{
1010
fs::{self, File},
11-
path::{Path, PathBuf},
11+
path::PathBuf,
1212
};
1313
use sway_core::TreeType;
1414
use tracing::{info, warn};
@@ -56,6 +56,8 @@ pub fn build(command: BuildCommand) -> Result<pkg::Compiled> {
5656

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

59+
let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?;
60+
5961
// If any cli parameter is passed by the user it overrides the selected build profile.
6062
let mut config = &pkg::BuildConfig {
6163
print_ir,
@@ -80,52 +82,6 @@ pub fn build(command: BuildCommand) -> Result<pkg::Compiled> {
8082
});
8183
}
8284

83-
let lock_path = lock_path(manifest.dir());
84-
85-
let plan_result = pkg::BuildPlan::from_lock_file(&lock_path, SWAY_GIT_TAG);
86-
87-
// Retrieve the old lock file state so we can produce a diff.
88-
let old_lock = plan_result
89-
.as_ref()
90-
.ok()
91-
.map(|plan| Lock::from_graph(plan.graph()))
92-
.unwrap_or_default();
93-
94-
// Check if there are any errors coming from the BuildPlan generation from the lock file
95-
// If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything
96-
let mut new_lock_cause = None;
97-
let mut plan = plan_result.or_else(|e| -> Result<pkg::BuildPlan> {
98-
if locked {
99-
bail!(
100-
"The lock file {} needs to be updated but --locked was passed to prevent this.",
101-
lock_path.to_string_lossy()
102-
);
103-
}
104-
new_lock_cause = if e.to_string().contains("No such file or directory") {
105-
Some(anyhow!("lock file did not exist"))
106-
} else {
107-
Some(e)
108-
};
109-
let plan = pkg::BuildPlan::new(&manifest, SWAY_GIT_TAG, offline)?;
110-
Ok(plan)
111-
})?;
112-
113-
// If there are no issues with the BuildPlan generated from the lock file
114-
// Check and apply the diff.
115-
if new_lock_cause.is_none() {
116-
let diff = plan.validate(&manifest, SWAY_GIT_TAG)?;
117-
if !diff.added.is_empty() || !diff.removed.is_empty() {
118-
new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`"));
119-
plan = plan.apply_pkg_diff(diff, SWAY_GIT_TAG, offline)?;
120-
}
121-
}
122-
123-
if let Some(cause) = new_lock_cause {
124-
info!(" Creating a new `Forc.lock` file. (Cause: {})", cause);
125-
create_new_lock(&plan, &old_lock, &manifest, &lock_path)?;
126-
info!(" Created new lock file at {}", lock_path.display());
127-
}
128-
12985
// Build it!
13086
let (compiled, source_map) = pkg::build(&plan, config, SWAY_GIT_TAG)?;
13187

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

188144
Ok(compiled)
189145
}
190-
191-
fn create_new_lock(
192-
plan: &pkg::BuildPlan,
193-
old_lock: &Lock,
194-
manifest: &ManifestFile,
195-
lock_path: &Path,
196-
) -> Result<()> {
197-
let lock = Lock::from_graph(plan.graph());
198-
let diff = lock.diff(old_lock);
199-
lock::print_diff(&manifest.project.name, &diff);
200-
let string = toml::ser::to_string_pretty(&lock)
201-
.map_err(|e| anyhow!("failed to serialize lock file: {}", e))?;
202-
fs::write(&lock_path, &string).map_err(|e| anyhow!("failed to write lock file: {}", e))?;
203-
Ok(())
204-
}

forc/src/ops/forc_check.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use crate::{cli::CheckCommand, utils::SWAY_GIT_TAG};
2+
use anyhow::Result;
3+
use forc_pkg::{self as pkg, ManifestFile};
4+
use std::path::PathBuf;
5+
6+
pub fn check(command: CheckCommand) -> Result<sway_core::CompileAstResult> {
7+
let CheckCommand {
8+
path,
9+
offline_mode: offline,
10+
silent_mode,
11+
locked,
12+
} = command;
13+
14+
let this_dir = if let Some(ref path) = path {
15+
PathBuf::from(path)
16+
} else {
17+
std::env::current_dir()?
18+
};
19+
let manifest = ManifestFile::from_dir(&this_dir, SWAY_GIT_TAG)?;
20+
let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?;
21+
22+
pkg::check(&plan, silent_mode, SWAY_GIT_TAG)
23+
}

0 commit comments

Comments
 (0)