Skip to content

Commit 1319a7f

Browse files
authored
Integrate dylint lints to build (#1412)
Integrate dylint lints to `build` Integrate mandatory dylint-based lints into the build process. This change enables us to introduce ink-specific compilation errors more flexibly through the dylint-based `ink_linting` module. It works in the following way: - The manifest of each contract includes a `workspace.metadata.dylint` section - When the `build` is run, cargo downloads and builds the `ink_linting` libraries with a specific toolchain version - The linter is then executed using that specific toolchain There is room for improvement in automating toolchain installation, which will be addressed in future releases.
1 parent 4cb707f commit 1319a7f

File tree

10 files changed

+142
-113
lines changed

10 files changed

+142
-113
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ jobs:
180180
uses: actions-rs/toolchain@v1
181181
with:
182182
profile: minimal
183-
toolchain: stable
183+
toolchain: nightly-2023-12-28
184184
default: true
185185
target: wasm32-unknown-unknown
186186
components: rust-src, clippy

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Add a user-friendly view of contract storage data in the form of a table - [#1414](https://github.com/paritytech/cargo-contract/pull/1414)
1111

12+
### Changed
13+
- Mandatory dylint-based lints - [#1412](https://github.com/paritytech/cargo-contract/pull/1412)
14+
1215
## [4.0.0-rc.1]
1316

1417
### Changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Modern releases of gcc and clang, as well as Visual Studio 2019+ should work.
4242

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

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

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

164164
##### `cargo contract verify-schema`
165165

166-
Verify a metadata file or a contract bundle containing metadata against the schema file.
166+
Verify a metadata file or a contract bundle containing metadata against the schema file.
167167

168168
##### `cargo contract storage`
169169

build-image/Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ RUN apt-get -y update && apt-get -y install gcc=${GCC_VERSION} g++=${G_VERSION}
9191
fi \
9292
&& echo "Executing ${COMMAND}" \
9393
&& eval "${COMMAND}" \
94+
&& echo "Installing linting dependencies" \
95+
&& cargo install cargo-dylint dylint-link \
9496
# Cleanup after `cargo install`
9597
&& rm -rf ${CARGO_HOME}/"registry" ${CARGO_HOME}/"git" /root/.cache/sccache \
9698
# apt clean up

crates/build/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ let args = contract_build::ExecuteArgs {
3232
unstable_flags: UnstableFlags::default(),
3333
optimization_passes: Some(OptimizationPasses::default()),
3434
keep_debug_symbols: false,
35-
dylint: false,
35+
extra_lints: false,
3636
output_type: OutputType::Json,
3737
skip_wasm_validation: false,
3838
target: Target::Wasm,

crates/build/src/lib.rs

Lines changed: 100 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,7 @@ use parity_wasm::elements::{
9494
};
9595
use semver::Version;
9696
use std::{
97-
collections::VecDeque,
9897
fs,
99-
io,
10098
path::{
10199
Path,
102100
PathBuf,
@@ -112,6 +110,19 @@ pub const DEFAULT_MAX_MEMORY_PAGES: u32 = 16;
112110
/// Version of the currently executing `cargo-contract` binary.
113111
const VERSION: &str = env!("CARGO_PKG_VERSION");
114112

113+
/// Configuration of the linting module.
114+
///
115+
/// Ensure it is kept up-to-date when updating `cargo-contract`.
116+
pub(crate) mod linting {
117+
/// Toolchain used to build ink_linting:
118+
/// https://github.com/paritytech/ink/blob/master/linting/rust-toolchain.toml
119+
pub const TOOLCHAIN_VERSION: &str = "nightly-2023-12-28";
120+
/// Git repository with ink_linting libraries
121+
pub const GIT_URL: &str = "https://github.com/paritytech/ink/";
122+
/// Git revision number of the linting crate
123+
pub const GIT_REV: &str = "1c029a153ead15cd0bd76631613967c9e679e0c1";
124+
}
125+
115126
/// Arguments to use when executing `build` or `check` commands.
116127
#[derive(Clone)]
117128
pub struct ExecuteArgs {
@@ -125,7 +136,7 @@ pub struct ExecuteArgs {
125136
pub unstable_flags: UnstableFlags,
126137
pub optimization_passes: Option<OptimizationPasses>,
127138
pub keep_debug_symbols: bool,
128-
pub dylint: bool,
139+
pub extra_lints: bool,
129140
pub output_type: OutputType,
130141
pub skip_wasm_validation: bool,
131142
pub target: Target,
@@ -145,7 +156,7 @@ impl Default for ExecuteArgs {
145156
unstable_flags: Default::default(),
146157
optimization_passes: Default::default(),
147158
keep_debug_symbols: Default::default(),
148-
dylint: Default::default(),
159+
extra_lints: Default::default(),
149160
output_type: Default::default(),
150161
skip_wasm_validation: Default::default(),
151162
target: Default::default(),
@@ -289,14 +300,8 @@ fn exec_cargo_for_onchain_target(
289300
"--target-dir={}",
290301
crate_metadata.target_directory.to_string_lossy()
291302
);
292-
293-
let mut args = vec![
294-
format!("--target={}", target.llvm_target()),
295-
"-Zbuild-std=core,alloc".to_owned(),
296-
"--no-default-features".to_owned(),
297-
"--release".to_owned(),
298-
target_dir,
299-
];
303+
let mut args = vec![target_dir, "--release".to_owned()];
304+
args.extend(onchain_cargo_options(target));
300305
network.append_to_args(&mut args);
301306

302307
let mut features = features.clone();
@@ -344,10 +349,13 @@ fn exec_cargo_for_onchain_target(
344349
env.push(("CARGO_ENCODED_RUSTFLAGS", Some(rustflags)));
345350
};
346351

347-
let cargo =
348-
util::cargo_cmd(command, &args, manifest_path.directory(), *verbosity, env);
349-
350-
invoke_cargo_and_scan_for_error(cargo)
352+
execute_cargo(util::cargo_cmd(
353+
command,
354+
&args,
355+
manifest_path.directory(),
356+
*verbosity,
357+
env,
358+
))
351359
};
352360

353361
if unstable_flags.original_manifest {
@@ -433,7 +441,7 @@ fn check_buffer_size_invoke_cargo_clean(
433441
"Detected a change in the configured buffer size. Rebuilding the project."
434442
.bold()
435443
);
436-
invoke_cargo_and_scan_for_error(cargo)?;
444+
execute_cargo(cargo)?;
437445
}
438446
Err(_) => {
439447
verbose_eprintln!(
@@ -443,7 +451,7 @@ fn check_buffer_size_invoke_cargo_clean(
443451
"Cannot find the previous size of the static buffer. Rebuilding the project."
444452
.bold()
445453
);
446-
invoke_cargo_and_scan_for_error(cargo)?;
454+
execute_cargo(cargo)?;
447455
}
448456
}
449457
}
@@ -452,59 +460,22 @@ fn check_buffer_size_invoke_cargo_clean(
452460

453461
/// Executes the supplied cargo command, reading the output and scanning for known errors.
454462
/// Writes the captured stderr back to stderr and maintains the cargo tty progress bar.
455-
fn invoke_cargo_and_scan_for_error(cargo: duct::Expression) -> Result<()> {
456-
macro_rules! eprintln_red {
457-
($value:expr) => {{
458-
use colored::Colorize as _;
459-
::std::eprintln!("{}", $value.bright_red().bold());
460-
}};
463+
fn execute_cargo(cargo: duct::Expression) -> Result<()> {
464+
match cargo.unchecked().run() {
465+
Ok(out) if out.status.success() => Ok(()),
466+
Ok(out) => anyhow::bail!(String::from_utf8_lossy(&out.stderr).to_string()),
467+
Err(e) => anyhow::bail!("Cannot run `cargo` command: {:?}", e),
461468
}
462-
463-
// unchecked: Even capture output on non exit return status
464-
let cargo = util::cargo_tty_output(cargo).unchecked();
465-
466-
let missing_main_err = "error[E0601]".as_bytes();
467-
let mut err_buf = VecDeque::with_capacity(missing_main_err.len());
468-
469-
let mut reader = cargo.stderr_to_stdout().reader()?;
470-
let mut buffer = [0u8; 1];
471-
472-
loop {
473-
let bytes_read = io::Read::read(&mut reader, &mut buffer)?;
474-
for byte in buffer[0..bytes_read].iter() {
475-
err_buf.push_back(*byte);
476-
if err_buf.len() > missing_main_err.len() {
477-
let byte = err_buf.pop_front().expect("buffer is not empty");
478-
io::Write::write(&mut io::stderr(), &[byte])?;
479-
}
480-
}
481-
if missing_main_err == err_buf.make_contiguous() {
482-
eprintln_red!("\nExited with error: [E0601]");
483-
eprintln_red!(
484-
"Your contract must be annotated with the `no_main` attribute.\n"
485-
);
486-
eprintln_red!("Examples how to do this:");
487-
eprintln_red!(" - `#![cfg_attr(not(feature = \"std\"), no_std, no_main)]`");
488-
eprintln_red!(" - `#[no_main]`\n");
489-
return Err(anyhow::anyhow!("missing `no_main` attribute"))
490-
}
491-
if bytes_read == 0 {
492-
// flush the remaining buffered bytes
493-
io::Write::write(&mut io::stderr(), err_buf.make_contiguous())?;
494-
break
495-
}
496-
buffer = [0u8; 1];
497-
}
498-
Ok(())
499469
}
500470

501-
/// Run linting steps which include `clippy` (mandatory) + `dylint` (optional).
471+
/// Run linting that involves two steps: `clippy` and `dylint`. Both are mandatory as
472+
/// they're part of the compilation process and implement security-critical features.
502473
fn lint(
503-
dylint: bool,
474+
extra_lints: bool,
504475
crate_metadata: &CrateMetadata,
476+
target: &Target,
505477
verbosity: &Verbosity,
506478
) -> Result<()> {
507-
// mandatory: Always run clippy.
508479
verbose_eprintln!(
509480
verbosity,
510481
" {} {}",
@@ -513,16 +484,19 @@ fn lint(
513484
);
514485
exec_cargo_clippy(crate_metadata, *verbosity)?;
515486

516-
// optional: Dylint only on demand (for now).
517-
if dylint {
487+
// TODO (jubnzv): Dylint needs a custom toolchain installed by the user. Currently,
488+
// it's required only for RiscV target. We're working on the toolchain integration
489+
// and will make this step mandatory for all targets in future releases.
490+
if extra_lints || matches!(target, Target::RiscV) {
518491
verbose_eprintln!(
519492
verbosity,
520493
" {} {}",
521494
"[==]".bold(),
522495
"Checking ink! linting rules".bright_green().bold()
523496
);
524-
exec_cargo_dylint(crate_metadata, *verbosity)?;
497+
exec_cargo_dylint(extra_lints, crate_metadata, target, *verbosity)?;
525498
}
499+
526500
Ok(())
527501
}
528502

@@ -537,7 +511,7 @@ fn exec_cargo_clippy(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
537511
"-Dclippy::arithmetic_side_effects",
538512
];
539513
// we execute clippy with the plain manifest no temp dir required
540-
invoke_cargo_and_scan_for_error(util::cargo_cmd(
514+
execute_cargo(util::cargo_cmd(
541515
"clippy",
542516
args,
543517
crate_metadata.manifest_path.directory(),
@@ -546,11 +520,25 @@ fn exec_cargo_clippy(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
546520
))
547521
}
548522

523+
/// Returns a list of cargo options used for on-chain builds
524+
fn onchain_cargo_options(target: &Target) -> Vec<String> {
525+
vec![
526+
format!("--target={}", target.llvm_target()),
527+
"-Zbuild-std=core,alloc".to_owned(),
528+
"--no-default-features".to_owned(),
529+
]
530+
}
531+
549532
/// Inject our custom lints into the manifest and execute `cargo dylint` .
550533
///
551534
/// We create a temporary folder, extract the linting driver there and run
552535
/// `cargo dylint` with it.
553-
fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Result<()> {
536+
fn exec_cargo_dylint(
537+
extra_lints: bool,
538+
crate_metadata: &CrateMetadata,
539+
target: &Target,
540+
verbosity: Verbosity,
541+
) -> Result<()> {
554542
check_dylint_requirements(crate_metadata.manifest_path.directory())?;
555543

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

550+
let mut args = if extra_lints {
551+
vec![
552+
"--lib=ink_linting_mandatory".to_owned(),
553+
"--lib=ink_linting".to_owned(),
554+
]
555+
} else {
556+
vec!["--lib=ink_linting_mandatory".to_owned()]
557+
};
558+
args.push("--".to_owned());
559+
// Pass on-chain build options to ensure the linter expands all conditional `cfg_attr`
560+
// macros, as it does for the release build.
561+
args.extend(onchain_cargo_options(target));
562+
562563
let target_dir = &crate_metadata.target_directory.to_string_lossy();
563-
let args = vec!["--lib=ink_linting"];
564564
let env = vec![
565565
// We need to set the `CARGO_TARGET_DIR` environment variable in
566566
// case `cargo dylint` is invoked.
@@ -578,7 +578,7 @@ fn exec_cargo_dylint(crate_metadata: &CrateMetadata, verbosity: Verbosity) -> Re
578578

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

625+
// Check if the required toolchain is present and is installed with `rustup`.
626+
if let Ok(output) = Command::new("rustup").arg("toolchain").arg("list").output() {
627+
anyhow::ensure!(
628+
String::from_utf8_lossy(&output.stdout).contains(linting::TOOLCHAIN_VERSION),
629+
format!(
630+
"Toolchain `{0}` was not found!\n\
631+
This specific version is required to provide additional source code analysis.\n\n
632+
You can install it by executing `rustup install {0}`.",
633+
linting::TOOLCHAIN_VERSION,
634+
)
635+
.to_string()
636+
.bright_yellow());
637+
} else {
638+
anyhow::bail!(format!(
639+
"Toolchain `{0}` was not found!\n\
640+
This specific version is required to provide additional source code analysis.\n\n
641+
Install `rustup` according to https://rustup.rs/ and then run: `rustup install {0}`.",
642+
linting::TOOLCHAIN_VERSION,
643+
)
644+
.to_string()
645+
.bright_yellow());
646+
}
647+
624648
// when testing this function we should never fall back to a `cargo` specified
625649
// in the env variable, as this would mess with the mocked binaries.
626650
#[cfg(not(test))]
@@ -793,7 +817,7 @@ pub fn execute(args: ExecuteArgs) -> Result<BuildResult> {
793817
build_artifact,
794818
unstable_flags,
795819
optimization_passes,
796-
dylint,
820+
extra_lints,
797821
output_type,
798822
target,
799823
..
@@ -838,7 +862,7 @@ pub fn execute(args: ExecuteArgs) -> Result<BuildResult> {
838862
let (opt_result, metadata_result, dest_wasm) = match build_artifact {
839863
BuildArtifacts::CheckOnly => {
840864
// Check basically means only running our linter without building.
841-
lint(*dylint, &crate_metadata, verbosity)?;
865+
lint(*extra_lints, &crate_metadata, target, verbosity)?;
842866
(None, None, None)
843867
}
844868
BuildArtifacts::CodeOnly => {
@@ -912,7 +936,7 @@ fn local_build(
912936
network,
913937
unstable_flags,
914938
keep_debug_symbols,
915-
dylint,
939+
extra_lints,
916940
skip_wasm_validation,
917941
target,
918942
max_memory_pages,
@@ -921,7 +945,7 @@ fn local_build(
921945

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

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

0 commit comments

Comments
 (0)