Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate dylint lints to build #1412

Merged
merged 35 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b1a4d65
feat(build): Always execute dylint-based lints
jubnzv Nov 17, 2023
f736546
feat(build): Don't check `cargo` output since we have a lint for this
jubnzv Nov 21, 2023
ddabef6
chore(build): cleanup
jubnzv Nov 27, 2023
7946de2
feat: Implement `extra_lints`
jubnzv Dec 23, 2023
f2cb3de
feat: Pass on-chain build options for the linter
jubnzv Dec 23, 2023
8f392c4
chore: fmt+clippy
jubnzv Dec 23, 2023
d74f7ca
fix: Unneeded steps when executing `cargo`
jubnzv Dec 23, 2023
1d15601
feat: Print more context on error when running `cargo`
jubnzv Dec 23, 2023
83df3fb
chore: Bump changelog
jubnzv Dec 23, 2023
d197c76
Merge remote-tracking branch 'origin/master' into mandatory-lints
jubnzv Dec 23, 2023
8950df2
feat: Install `cargo-dylint` and `dylint-link` in CI
jubnzv Dec 23, 2023
577fa72
fix(ci): Install missing packages in `template` section
jubnzv Dec 23, 2023
b3cbec9
fix(readme): Mark the `dylint` installation step as mandatory
jubnzv Dec 23, 2023
f21e2b8
feat(docker): Add `dylint` installation to the docker image
jubnzv Dec 23, 2023
8b7c7f3
Merge remote-tracking branch 'origin/master' into mandatory-lints
jubnzv Jan 3, 2024
c7a0626
Merge remote-tracking branch 'origin/master' into mandatory-lints
jubnzv Jan 15, 2024
af380eb
fix(ci): Install linting dependencies
jubnzv Jan 15, 2024
6c4be8d
fix(ci): deps
jubnzv Jan 15, 2024
9fb5222
fix: Install dependencies
jubnzv Jan 15, 2024
4a22459
fix(build): Fix installation for `ink_linting` libraries
jubnzv Jan 19, 2024
a97de2f
chore: Add rust-toolchain.toml
jubnzv Jan 19, 2024
557a696
fix(ci): Use rust-toolchain.toml from the repo
jubnzv Jan 19, 2024
45c531f
fix(ci): Toolchain version
jubnzv Jan 19, 2024
823dac9
fix(ci): Try to run it with the nightly toolchain
jubnzv Jan 19, 2024
3d4af86
fix(ci): Force `stable` toolchain for the `clippy` action
jubnzv Jan 19, 2024
84780ba
fix(tests): Failing test
jubnzv Jan 19, 2024
01dd588
fix(readme)
jubnzv Jan 20, 2024
44cf7b1
fix(ci): Use the `nightly` toolchain for template builds
jubnzv Jan 20, 2024
38c8bcd
chore: Remove the fixed toolchain version
jubnzv Jan 23, 2024
f3212f3
feat(build): Make `dylint` mandatory only for RiscV or `--lint`
jubnzv Jan 23, 2024
284f0ff
feat(build): Check `ink_linting` toolchain
jubnzv Jan 23, 2024
df89a75
fix: `cargo-dylint` test
jubnzv Jan 23, 2024
789087c
fix(ci): Remove `dylint` installation
jubnzv Jan 23, 2024
169da84
fix(test): Run only on mandatory lints which are disabled
jubnzv Jan 23, 2024
a86c38b
fix(test): Fix to check for non-existing toolchain
jubnzv Jan 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: nightly-2023-12-28
default: true
target: wasm32-unknown-unknown
components: rust-src, clippy
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add a user-friendly view of contract storage data in the form of a table - [#1414](https://github.com/paritytech/cargo-contract/pull/1414)

### Changed
- Mandatory dylint-based lints - [#1412](https://github.com/paritytech/cargo-contract/pull/1412)

## [4.0.0-rc.1]

### Changed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Modern releases of gcc and clang, as well as Visual Studio 2019+ should work.

- Step 2: `cargo install --force --locked cargo-contract`.

- Step 3: (**Optional**) Install `dylint` for linting.
- Step 3: Install `dylint` for linting.

- (MacOS) `brew install openssl`
- `cargo install cargo-dylint dylint-link`.
Expand Down Expand Up @@ -163,7 +163,7 @@ Generate schema and print it to STDOUT.

##### `cargo contract verify-schema`

Verify a metadata file or a contract bundle containing metadata against the schema file.
Verify a metadata file or a contract bundle containing metadata against the schema file.

##### `cargo contract storage`

Expand Down
2 changes: 2 additions & 0 deletions build-image/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ RUN apt-get -y update && apt-get -y install gcc=${GCC_VERSION} g++=${G_VERSION}
fi \
&& echo "Executing ${COMMAND}" \
&& eval "${COMMAND}" \
&& echo "Installing linting dependencies" \
&& cargo install cargo-dylint dylint-link \
# Cleanup after `cargo install`
&& rm -rf ${CARGO_HOME}/"registry" ${CARGO_HOME}/"git" /root/.cache/sccache \
# apt clean up
Expand Down
2 changes: 1 addition & 1 deletion crates/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let args = contract_build::ExecuteArgs {
unstable_flags: UnstableFlags::default(),
optimization_passes: Some(OptimizationPasses::default()),
keep_debug_symbols: false,
dylint: false,
extra_lints: false,
output_type: OutputType::Json,
skip_wasm_validation: false,
target: Target::Wasm,
Expand Down
176 changes: 100 additions & 76 deletions crates/build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,7 @@ use parity_wasm::elements::{
};
use semver::Version;
use std::{
collections::VecDeque,
fs,
io,
path::{
Path,
PathBuf,
Expand All @@ -112,6 +110,19 @@ pub const DEFAULT_MAX_MEMORY_PAGES: u32 = 16;
/// Version of the currently executing `cargo-contract` binary.
const VERSION: &str = env!("CARGO_PKG_VERSION");

/// Configuration of the linting module.
///
/// Ensure it is kept up-to-date when updating `cargo-contract`.
pub(crate) mod linting {
/// Toolchain used to build ink_linting:
/// https://github.com/paritytech/ink/blob/master/linting/rust-toolchain.toml
pub const TOOLCHAIN_VERSION: &str = "nightly-2023-12-28";
/// Git repository with ink_linting libraries
pub const GIT_URL: &str = "https://github.com/paritytech/ink/";
/// Git revision number of the linting crate
pub const GIT_REV: &str = "1c029a153ead15cd0bd76631613967c9e679e0c1";
}

/// Arguments to use when executing `build` or `check` commands.
#[derive(Clone)]
pub struct ExecuteArgs {
Expand All @@ -125,7 +136,7 @@ pub struct ExecuteArgs {
pub unstable_flags: UnstableFlags,
pub optimization_passes: Option<OptimizationPasses>,
pub keep_debug_symbols: bool,
pub dylint: bool,
pub extra_lints: bool,
pub output_type: OutputType,
pub skip_wasm_validation: bool,
pub target: Target,
Expand All @@ -145,7 +156,7 @@ impl Default for ExecuteArgs {
unstable_flags: Default::default(),
optimization_passes: Default::default(),
keep_debug_symbols: Default::default(),
dylint: Default::default(),
extra_lints: Default::default(),
output_type: Default::default(),
skip_wasm_validation: Default::default(),
target: Default::default(),
Expand Down Expand Up @@ -289,14 +300,8 @@ fn exec_cargo_for_onchain_target(
"--target-dir={}",
crate_metadata.target_directory.to_string_lossy()
);

let mut args = vec![
format!("--target={}", target.llvm_target()),
"-Zbuild-std=core,alloc".to_owned(),
"--no-default-features".to_owned(),
"--release".to_owned(),
target_dir,
];
let mut args = vec![target_dir, "--release".to_owned()];
args.extend(onchain_cargo_options(target));
network.append_to_args(&mut args);

let mut features = features.clone();
Expand Down Expand Up @@ -344,10 +349,13 @@ fn exec_cargo_for_onchain_target(
env.push(("CARGO_ENCODED_RUSTFLAGS", Some(rustflags)));
};

let cargo =
util::cargo_cmd(command, &args, manifest_path.directory(), *verbosity, env);

invoke_cargo_and_scan_for_error(cargo)
execute_cargo(util::cargo_cmd(
command,
&args,
manifest_path.directory(),
*verbosity,
env,
))
};

if unstable_flags.original_manifest {
Expand Down Expand Up @@ -433,7 +441,7 @@ fn check_buffer_size_invoke_cargo_clean(
"Detected a change in the configured buffer size. Rebuilding the project."
.bold()
);
invoke_cargo_and_scan_for_error(cargo)?;
execute_cargo(cargo)?;
}
Err(_) => {
verbose_eprintln!(
Expand All @@ -443,7 +451,7 @@ fn check_buffer_size_invoke_cargo_clean(
"Cannot find the previous size of the static buffer. Rebuilding the project."
.bold()
);
invoke_cargo_and_scan_for_error(cargo)?;
execute_cargo(cargo)?;
}
}
}
Expand All @@ -452,59 +460,22 @@ fn check_buffer_size_invoke_cargo_clean(

/// Executes the supplied cargo command, reading the output and scanning for known errors.
/// Writes the captured stderr back to stderr and maintains the cargo tty progress bar.
fn invoke_cargo_and_scan_for_error(cargo: duct::Expression) -> Result<()> {
macro_rules! eprintln_red {
($value:expr) => {{
use colored::Colorize as _;
::std::eprintln!("{}", $value.bright_red().bold());
}};
fn execute_cargo(cargo: duct::Expression) -> Result<()> {
match cargo.unchecked().run() {
Ok(out) if out.status.success() => Ok(()),
Ok(out) => anyhow::bail!(String::from_utf8_lossy(&out.stderr).to_string()),
Err(e) => anyhow::bail!("Cannot run `cargo` command: {:?}", e),
}

// unchecked: Even capture output on non exit return status
let cargo = util::cargo_tty_output(cargo).unchecked();

let missing_main_err = "error[E0601]".as_bytes();
let mut err_buf = VecDeque::with_capacity(missing_main_err.len());

let mut reader = cargo.stderr_to_stdout().reader()?;
let mut buffer = [0u8; 1];

loop {
let bytes_read = io::Read::read(&mut reader, &mut buffer)?;
for byte in buffer[0..bytes_read].iter() {
err_buf.push_back(*byte);
if err_buf.len() > missing_main_err.len() {
let byte = err_buf.pop_front().expect("buffer is not empty");
io::Write::write(&mut io::stderr(), &[byte])?;
}
}
if missing_main_err == err_buf.make_contiguous() {
eprintln_red!("\nExited with error: [E0601]");
eprintln_red!(
"Your contract must be annotated with the `no_main` attribute.\n"
);
eprintln_red!("Examples how to do this:");
eprintln_red!(" - `#![cfg_attr(not(feature = \"std\"), no_std, no_main)]`");
eprintln_red!(" - `#[no_main]`\n");
return Err(anyhow::anyhow!("missing `no_main` attribute"))
}
if bytes_read == 0 {
// flush the remaining buffered bytes
io::Write::write(&mut io::stderr(), err_buf.make_contiguous())?;
break
}
buffer = [0u8; 1];
}
Ok(())
jubnzv marked this conversation as resolved.
Show resolved Hide resolved
}

/// Run linting steps which include `clippy` (mandatory) + `dylint` (optional).
/// Run linting that involves two steps: `clippy` and `dylint`. Both are mandatory as
/// they're part of the compilation process and implement security-critical features.
fn lint(
dylint: bool,
extra_lints: bool,
Copy link
Member Author

@jubnzv jubnzv Nov 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that all the lints will be executed on each build, because it slows down the build process and may bother the user with extra noise generated by lints. Ideally, we should execute only mandatory lints needed to check compilation errors (like no_main) and skip the rest.

I think the most simple way to do this is to split our ink_linting in two dylint libraries, like ink_linting and ink_linting_extra. The first one will contain only the mandatory lints, and the second one will run the optional analyses when user requesting it.

There are other ways to suppress lints (like hacking Cargo.toml in contract projects or compilation flags for rustc), but AFAIK these will execute lints anyway, suppressing their output.

WDYT @ascjones?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we only want the mandatory lints to execute every time, extra lints must still be opt-in.

I think the most simple way to do this is to split our ink_linting in two dylint libraries, like ink_linting and ink_linting_extra

Sounds good to me 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged use-ink/ink#2032 now

crate_metadata: &CrateMetadata,
target: &Target,
verbosity: &Verbosity,
) -> Result<()> {
// mandatory: Always run clippy.
verbose_eprintln!(
verbosity,
" {} {}",
Expand All @@ -513,16 +484,19 @@ fn lint(
);
exec_cargo_clippy(crate_metadata, *verbosity)?;

// optional: Dylint only on demand (for now).
if dylint {
// TODO (jubnzv): Dylint needs a custom toolchain installed by the user. Currently,
// it's required only for RiscV target. We're working on the toolchain integration
// and will make this step mandatory for all targets in future releases.
if extra_lints || matches!(target, Target::RiscV) {
verbose_eprintln!(
verbosity,
" {} {}",
"[==]".bold(),
"Checking ink! linting rules".bright_green().bold()
);
exec_cargo_dylint(crate_metadata, *verbosity)?;
exec_cargo_dylint(extra_lints, crate_metadata, target, *verbosity)?;
}

Ok(())
}

Expand All @@ -537,7 +511,7 @@ fn exec_cargo_clippy(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
"-Dclippy::arithmetic_side_effects",
];
// we execute clippy with the plain manifest no temp dir required
invoke_cargo_and_scan_for_error(util::cargo_cmd(
execute_cargo(util::cargo_cmd(
"clippy",
args,
crate_metadata.manifest_path.directory(),
Expand All @@ -546,11 +520,25 @@ fn exec_cargo_clippy(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
))
}

/// Returns a list of cargo options used for on-chain builds
fn onchain_cargo_options(target: &Target) -> Vec<String> {
vec![
format!("--target={}", target.llvm_target()),
"-Zbuild-std=core,alloc".to_owned(),
"--no-default-features".to_owned(),
]
}

/// Inject our custom lints into the manifest and execute `cargo dylint` .
///
/// We create a temporary folder, extract the linting driver there and run
/// `cargo dylint` with it.
fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Result<()> {
fn exec_cargo_dylint(
extra_lints: bool,
crate_metadata: &CrateMetadata,
target: &Target,
verbosity: Verbosity,
) -> Result<()> {
check_dylint_requirements(crate_metadata.manifest_path.directory())?;

// `dylint` is verbose by default, it doesn't have a `--verbose` argument,
Expand All @@ -559,8 +547,20 @@ fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
Verbosity::Default | Verbosity::Quiet => Verbosity::Quiet,
};

let mut args = if extra_lints {
vec![
"--lib=ink_linting_mandatory".to_owned(),
"--lib=ink_linting".to_owned(),
]
} else {
vec!["--lib=ink_linting_mandatory".to_owned()]
};
args.push("--".to_owned());
// Pass on-chain build options to ensure the linter expands all conditional `cfg_attr`
// macros, as it does for the release build.
args.extend(onchain_cargo_options(target));

let target_dir = &crate_metadata.target_directory.to_string_lossy();
let args = vec!["--lib=ink_linting"];
let env = vec![
// We need to set the `CARGO_TARGET_DIR` environment variable in
// case `cargo dylint` is invoked.
Expand All @@ -578,7 +578,7 @@ fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re

Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
.with_root_package_manifest(|manifest| {
manifest.with_dylint()?.with_empty_workspace();
manifest.with_dylint()?;
Ok(())
})?
.using_temp(|manifest_path| {
Expand All @@ -599,7 +599,8 @@ fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
/// Checks if all requirements for `dylint` are installed.
///
/// We require both `cargo-dylint` and `dylint-link` because the driver is being
/// built at runtime on demand.
/// built at runtime on demand. These must be built using a custom version of the
/// toolchain, as the linter utilizes the unstable rustc API.
///
/// This function takes a `_working_dir` which is only used for unit tests.
fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> {
Expand All @@ -621,6 +622,29 @@ fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> {
})
};

// Check if the required toolchain is present and is installed with `rustup`.
if let Ok(output) = Command::new("rustup").arg("toolchain").arg("list").output() {
anyhow::ensure!(
String::from_utf8_lossy(&output.stdout).contains(linting::TOOLCHAIN_VERSION),
format!(
"Toolchain `{0}` was not found!\n\
This specific version is required to provide additional source code analysis.\n\n
You can install it by executing `rustup install {0}`.",
linting::TOOLCHAIN_VERSION,
)
.to_string()
.bright_yellow());
} else {
anyhow::bail!(format!(
"Toolchain `{0}` was not found!\n\
This specific version is required to provide additional source code analysis.\n\n
Install `rustup` according to https://rustup.rs/ and then run: `rustup install {0}`.",
linting::TOOLCHAIN_VERSION,
)
.to_string()
.bright_yellow());
}

// when testing this function we should never fall back to a `cargo` specified
// in the env variable, as this would mess with the mocked binaries.
#[cfg(not(test))]
Expand Down Expand Up @@ -793,7 +817,7 @@ pub fn execute(args: ExecuteArgs) -> Result<BuildResult> {
build_artifact,
unstable_flags,
optimization_passes,
dylint,
extra_lints,
output_type,
target,
..
Expand Down Expand Up @@ -838,7 +862,7 @@ pub fn execute(args: ExecuteArgs) -> Result<BuildResult> {
let (opt_result, metadata_result, dest_wasm) = match build_artifact {
BuildArtifacts::CheckOnly => {
// Check basically means only running our linter without building.
lint(*dylint, &crate_metadata, verbosity)?;
lint(*extra_lints, &crate_metadata, target, verbosity)?;
(None, None, None)
}
BuildArtifacts::CodeOnly => {
Expand Down Expand Up @@ -912,7 +936,7 @@ fn local_build(
network,
unstable_flags,
keep_debug_symbols,
dylint,
extra_lints,
skip_wasm_validation,
target,
max_memory_pages,
Expand All @@ -921,7 +945,7 @@ fn local_build(

// We always want to lint first so we don't suppress any warnings when a build is
// skipped because of a matching fingerprint.
lint(*dylint, crate_metadata, verbosity)?;
lint(*extra_lints, crate_metadata, target, verbosity)?;

let pre_fingerprint = Fingerprint::new(crate_metadata)?;

Expand Down
Loading
Loading