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

DevOps: Implement seamare, seamare-lint, cargo-seamare to catch direct duplicate dependencies #1907

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -181,3 +181,27 @@ jobs:
with:
command: fmt
args: --all -- --check

seamare:
name: Seamare
timeout-minutes: 30
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true

- name: install cargo seamare
uses: actions-rs/cargo@v1
with:
command: install
# For now seamare is not published, so just install locally
args: --path cargo-seamare

- name: Run cargo seamare to catch direct duplicate dependencies
uses: actions-rs/cargo@v1
with:
command: seamare
3 changes: 3 additions & 0 deletions Cargo.toml
Expand Up @@ -12,6 +12,9 @@ members = [
"zebra-utils",
"tower-batch",
"tower-fallback",
"seamare",
"seamare-lints",
"cargo-seamare",
]

[profile.dev]
Expand Down
14 changes: 14 additions & 0 deletions cargo-seamare/Cargo.toml
@@ -0,0 +1,14 @@
[package]
name = "cargo-seamare"
version = "0.1.0"
authors = ["Zcash Foundation <zebra@zfnd.org>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
seamare = { path = "../seamare" }
seamare-lints = { path = "../seamare-lints" }
guppy = "0.8.0"
camino = "1.0.3"
anyhow = "1.0.38"
119 changes: 119 additions & 0 deletions cargo-seamare/src/lint_engine.rs
@@ -0,0 +1,119 @@
use seamare::prelude::*;

/// Configuration for the lint engine.
#[derive(Clone, Debug)]
pub struct LintEngineConfig<'cfg> {
core: &'cfg CoreContext<'cfg>,
project_linters: &'cfg [&'cfg dyn ProjectLinter],
package_linters: &'cfg [&'cfg dyn PackageLinter],
file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
content_linters: &'cfg [&'cfg dyn ContentLinter],
fail_fast: bool,
}

impl<'cfg> LintEngineConfig<'cfg> {
pub fn new(core: &'cfg CoreContext) -> Self {
Self {
core,
project_linters: &[],
package_linters: &[],
file_path_linters: &[],
content_linters: &[],
fail_fast: false,
}
}

pub fn with_project_linters(
&mut self,
project_linters: &'cfg [&'cfg dyn ProjectLinter],
) -> &mut Self {
self.project_linters = project_linters;
self
}

#[allow(dead_code)]
pub fn with_package_linters(
&mut self,
package_linters: &'cfg [&'cfg dyn PackageLinter],
) -> &mut Self {
self.package_linters = package_linters;
self
}

#[allow(dead_code)]
pub fn with_file_path_linters(
&mut self,
file_path_linters: &'cfg [&'cfg dyn FilePathLinter],
) -> &mut Self {
self.file_path_linters = file_path_linters;
self
}

#[allow(dead_code)]
pub fn with_content_linters(
&mut self,
content_linters: &'cfg [&'cfg dyn ContentLinter],
) -> &mut Self {
self.content_linters = content_linters;
self
}

#[allow(dead_code)]
pub fn fail_fast(&mut self, fail_fast: bool) -> &mut Self {
self.fail_fast = fail_fast;
self
}

pub fn build(&self) -> LintEngine<'cfg> {
LintEngine::new(self.clone())
}
}

pub struct LintEngine<'cfg> {
config: LintEngineConfig<'cfg>,
project_ctx: ProjectContext<'cfg>,
}

impl<'cfg> LintEngine<'cfg> {
pub fn new(config: LintEngineConfig<'cfg>) -> Self {
let project_ctx = ProjectContext::new(config.core);
Self {
config,
project_ctx,
}
}

pub fn run(&self) -> Result<LintResults> {
let mut skipped = vec![];
let mut messages = vec![];

// Just run project linters.
if !self.config.project_linters.is_empty() {
for linter in self.config.project_linters {
let source = self.project_ctx.source(linter.name());
let mut formatter = LintFormatter::new(source, &mut messages);
match linter.run(&self.project_ctx, &mut formatter)? {
RunStatus::Executed => {
// Lint ran successfully.
}
RunStatus::Skipped(reason) => {
skipped.push((source, reason));
}
}

if self.config.fail_fast && !messages.is_empty() {
// At least one issue was found.
return Ok(LintResults { skipped, messages });
}
}
}
Ok(LintResults { skipped, messages })
}
}

#[derive(Debug)]
#[non_exhaustive]
pub struct LintResults<'l> {
pub skipped: Vec<(LintSource<'l>, SkipReason<'l>)>,
pub messages: Vec<(LintSource<'l>, LintMessage)>,
}
44 changes: 44 additions & 0 deletions cargo-seamare/src/main.rs
@@ -0,0 +1,44 @@
mod lint_engine;

use anyhow::anyhow;
use camino::Utf8PathBuf;
use lint_engine::LintEngineConfig;
use seamare::prelude::{CoreContext, SeamareError};
use seamare_lints::DirectDepDups;
use std::env;

type Result<T> = anyhow::Result<T>;

fn main() -> Result<()> {
run_lint_engine()
}

fn run_lint_engine() -> Result<()> {
// basic progress:
// check if it's running inside a cargo project, build context can handle this.
let path = env::current_dir()?;
let path = Utf8PathBuf::from_path_buf(path).map_err(SeamareError::NotValidUtf8Path)?;
let context = CoreContext::new(path.as_path())?;
// Initialize a lint engine, load linter.
let engine = LintEngineConfig::new(&context)
.with_project_linters(&[&DirectDepDups])
.build();

// Run this lint engine.
let results = engine.run()?;
for (source, message) in &results.messages {
println!(
"[{}] [{}] [{}]: {}\n",
message.level(),
source.name(),
source.kind(),
message.message()
);
}

if !results.messages.is_empty() {
Err(anyhow!("there were lint errors"))
} else {
Ok(())
}
}
11 changes: 11 additions & 0 deletions seamare-lints/Cargo.toml
@@ -0,0 +1,11 @@
[package]
name = "seamare-lints"
version = "0.1.0"
authors = ["Zcash Foundation <zebra@zfnd.org>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
seamare = { path = "../seamare" }
guppy = "0.8.0"
54 changes: 54 additions & 0 deletions seamare-lints/src/duplicate_deps.rs
@@ -0,0 +1,54 @@
use guppy::Version;
use seamare::prelude::*;
use std::collections::BTreeMap;

/// Ensure that packages within the workspace only depend on one version of a third-party crate.
#[derive(Debug)]
pub struct DirectDepDups;

impl Linter for DirectDepDups {
fn name(&self) -> &'static str {
"direct-dep-dups"
}
}

impl ProjectLinter for DirectDepDups {
fn run<'l>(
&self,
ctx: &ProjectContext<'l>,
out: &mut LintFormatter<'l, '_>,
) -> Result<RunStatus<'l>> {
let package_graph = ctx.package_graph();

// This is a map of direct deps by name -> version -> packages that depend on it.
let mut direct_deps: BTreeMap<&str, BTreeMap<&Version, Vec<&str>>> = BTreeMap::new();
package_graph.query_workspace().resolve_with_fn(|_, link| {
// Collect direct dependencies of workspace packages.
let (from, to) = link.endpoints();
if from.in_workspace() && !to.in_workspace() {
direct_deps
.entry(to.name())
.or_default()
.entry(to.version())
.or_default()
.push(from.name());
}
// query_workspace + preventing further traversals will mean that only direct
// dependencies are considered.
false
});
for (direct_dep, versions) in direct_deps {
if versions.len() > 1 {
let mut msg = format!("duplicate direct dependency '{}':\n", direct_dep);
for (version, packages) in versions {
msg.push_str(&format!(" * {} (", version));
msg.push_str(&packages.join(", "));
msg.push_str(")\n");
}
out.write(LintLevel::Error, msg);
}
}

Ok(RunStatus::Executed)
}
}
3 changes: 3 additions & 0 deletions seamare-lints/src/lib.rs
@@ -0,0 +1,3 @@
mod duplicate_deps;

pub use duplicate_deps::DirectDepDups;
13 changes: 13 additions & 0 deletions seamare/Cargo.toml
@@ -0,0 +1,13 @@
[package]
name = "seamare"
version = "0.1.0"
authors = ["Zcash Foundation <zebra@zfnd.org>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
guppy = "0.8.0"
camino = "1.0.3"
once_cell = "1.7.2"
thiserror = "1.0"