From a500e2d58db1aa9c6d5cc6a51a7a892fc21e4a82 Mon Sep 17 00:00:00 2001 From: Joshua Nelson Date: Fri, 30 Oct 2020 17:14:36 -0400 Subject: [PATCH 1/3] Split cargo-deadlinks and deadlinks into two binaries - Expose public `walk_dir` function, which prints all missing files - `impl Clone for CheckContext` - Put cargo dependencies behind a feature gate - Switch from `Into` to `From` to help type inference --- Cargo.toml | 16 ++++++- src/{main.rs => bin/cargo-deadlinks.rs} | 64 +++++-------------------- src/bin/deadlinks.rs | 61 +++++++++++++++++++++++ src/bin/shared.rs | 16 +++++++ src/check.rs | 9 +++- src/lib.rs | 30 +++++++++++- 6 files changed, 139 insertions(+), 57 deletions(-) rename src/{main.rs => bin/cargo-deadlinks.rs} (75%) create mode 100644 src/bin/deadlinks.rs create mode 100644 src/bin/shared.rs diff --git a/Cargo.toml b/Cargo.toml index 0794900..8f3af60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,22 @@ edition = "2018" repository = "https://github.com/deadlinks/cargo-deadlinks" readme = "README.md" license = "MIT OR Apache-2.0" +autobins = false + +[[bin]] +name = "cargo-deadlinks" +required-features = ["cargo"] + +[[bin]] +name = "deadlinks" + +[features] +cargo = ["cargo_metadata", "serde_json"] +default = ["cargo"] [dependencies] -cargo_metadata = "0.9" +cargo_metadata = { version = "0.9", optional = true } +serde_json = { version = "1.0.34", optional = true } docopt = "1" env_logger = "0.8" lol_html = "0.2" @@ -21,7 +34,6 @@ serde = "1.0" serde_derive = "1.0" url = "2" walkdir = "2.1" -serde_json = "1.0.34" [dev-dependencies] assert_cmd = "1.0" diff --git a/src/main.rs b/src/bin/cargo-deadlinks.rs similarity index 75% rename from src/main.rs rename to src/bin/cargo-deadlinks.rs index 87b0a81..15b92a3 100644 --- a/src/main.rs +++ b/src/bin/cargo-deadlinks.rs @@ -1,15 +1,13 @@ -use serde_derive::Deserialize; - -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process; use cargo_metadata::MetadataCommand; use docopt::Docopt; -use log::LevelFilter; +use serde_derive::Deserialize; -use rayon::{prelude::*, ThreadPoolBuilder}; +use cargo_deadlinks::{walk_dir, CheckContext}; -use cargo_deadlinks::{unavailable_urls, CheckContext}; +mod shared; const MAIN_USAGE: &str = " Check your package's documentation for dead links. @@ -34,10 +32,11 @@ struct MainArgs { flag_check_http: bool, } -impl Into for MainArgs { - fn into(self) -> CheckContext { +impl From for CheckContext { + fn from(args: MainArgs) -> CheckContext { CheckContext { - check_http: self.flag_check_http, + check_http: args.flag_check_http, + verbose: args.flag_debug, } } } @@ -50,13 +49,14 @@ fn main() { }) .unwrap_or_else(|e| e.exit()); - init_logger(&args); + shared::init_logger(args.flag_debug, args.flag_verbose, "cargo_deadlinks"); let dirs = args .arg_directory .as_ref() .map_or_else(determine_dir, |dir| vec![dir.into()]); + let ctx = CheckContext::from(args); let mut errors = false; for dir in dirs { let dir = match dir.canonicalize() { @@ -69,7 +69,7 @@ fn main() { } }; log::info!("checking directory {:?}", dir); - if walk_dir(&dir, &args) { + if walk_dir(&dir, ctx.clone()) { errors = true; } } @@ -78,21 +78,6 @@ fn main() { } } -/// Initalizes the logger according to the provided config flags. -fn init_logger(args: &MainArgs) { - let mut builder = env_logger::Builder::new(); - match (args.flag_debug, args.flag_verbose) { - (true, _) => { - builder.filter(Some("cargo_deadlinks"), LevelFilter::Debug); - } - (false, true) => { - builder.filter(Some("cargo_deadlinks"), LevelFilter::Info); - } - _ => {} - } - builder.parse_default_env().init(); -} - /// Returns the directory to use as root of the documentation. /// /// If an directory has been provided as CLI argument that one is used. @@ -143,33 +128,6 @@ fn has_docs(target: &cargo_metadata::Target) -> bool { } } -/// Traverses a given path recursively, checking all *.html files found. -/// -/// Returns whether an error occurred. -fn walk_dir(dir_path: &Path, args: &MainArgs) -> bool { - let pool = ThreadPoolBuilder::new() - .num_threads(num_cpus::get()) - .build() - .unwrap(); - - let ctx = CheckContext { - check_http: args.flag_check_http, - }; - pool.install(|| { - unavailable_urls(dir_path, &ctx) - .map(|err| { - if args.flag_debug { - println!("{}", err); - } else { - println!("{}", err.print_shortened(Some(dir_path))); - } - true - }) - // |||||| - .reduce(|| false, |initial, new| initial || new) - }) -} - #[cfg(test)] mod test { use super::has_docs; diff --git a/src/bin/deadlinks.rs b/src/bin/deadlinks.rs new file mode 100644 index 0000000..1267b57 --- /dev/null +++ b/src/bin/deadlinks.rs @@ -0,0 +1,61 @@ +use std::path::PathBuf; +use std::process; + +use cargo_deadlinks::{walk_dir, CheckContext}; +use docopt::Docopt; +use serde_derive::Deserialize; + +mod shared; + +const MAIN_USAGE: &str = " +Check your package's documentation for dead links. + +Usage: + deadlinks [options] + +Options: + -h --help Print this message + --check-http Check 'http' and 'https' scheme links + --debug Use debug output + -v --verbose Use verbose output + -V --version Print version info and exit. +"; + +#[derive(Debug, Deserialize)] +struct MainArgs { + arg_directory: PathBuf, + flag_verbose: bool, + flag_debug: bool, + flag_check_http: bool, +} + +impl From for CheckContext { + fn from(args: MainArgs) -> CheckContext { + CheckContext { + check_http: args.flag_check_http, + verbose: args.flag_debug, + } + } +} + +fn main() { + let args: MainArgs = Docopt::new(MAIN_USAGE) + .and_then(|d| { + d.version(Some(env!("CARGO_PKG_VERSION").to_owned())) + .deserialize() + }) + .unwrap_or_else(|e| e.exit()); + shared::init_logger(args.flag_debug, args.flag_verbose, "deadlinks"); + + let dir = match args.arg_directory.canonicalize() { + Ok(dir) => dir, + Err(_) => { + println!("Could not find directory {:?}.", args.arg_directory); + process::exit(1); + } + }; + log::info!("checking directory {:?}", dir); + if walk_dir(&dir, args.into()) { + process::exit(1); + } +} diff --git a/src/bin/shared.rs b/src/bin/shared.rs new file mode 100644 index 0000000..7eb9fc8 --- /dev/null +++ b/src/bin/shared.rs @@ -0,0 +1,16 @@ +use log::LevelFilter; + +/// Initalizes the logger according to the provided config flags. +pub fn init_logger(debug: bool, verbose: bool, krate: &str) { + let mut builder = env_logger::Builder::new(); + match (debug, verbose) { + (true, _) => { + builder.filter(Some(krate), LevelFilter::Debug); + } + (false, true) => { + builder.filter(Some(krate), LevelFilter::Info); + } + _ => {} + } + builder.parse_default_env().init(); +} diff --git a/src/check.rs b/src/check.rs index 8fb2e9a..594e1e7 100644 --- a/src/check.rs +++ b/src/check.rs @@ -120,7 +120,14 @@ mod test { let cwd = env::current_dir().unwrap(); let url = Url::from_file_path(cwd.join(path)).unwrap(); - check_file_url(&url, &CheckContext { check_http: false }).unwrap(); + check_file_url( + &url, + &CheckContext { + verbose: false, + check_http: false, + }, + ) + .unwrap(); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 1690dc1..61c6a84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ use std::{ }; use rayon::prelude::*; +use rayon::ThreadPoolBuilder; use walkdir::{DirEntry, WalkDir}; use check::is_available; @@ -14,9 +15,11 @@ pub use check::{CheckError, HttpError}; mod check; mod parse; -#[derive(Debug)] +// NOTE: this could be Copy, but we intentionally choose not to guarantee that. +#[derive(Clone, Debug)] pub struct CheckContext { pub check_http: bool, + pub verbose: bool, } #[derive(Debug)] @@ -32,6 +35,31 @@ impl fmt::Display for FileError { } } +/// Traverses a given path recursively, checking all *.html files found. +/// +/// For each error that occurred, print an error message. +/// Returns whether an error occurred. +pub fn walk_dir(dir_path: &Path, ctx: CheckContext) -> bool { + let pool = ThreadPoolBuilder::new() + .num_threads(num_cpus::get()) + .build() + .unwrap(); + + pool.install(|| { + unavailable_urls(dir_path, &ctx) + .map(|err| { + if ctx.verbose { + println!("{}", err); + } else { + println!("{}", err.print_shortened(Some(dir_path))); + } + true + }) + // |||||| + .reduce(|| false, |initial, new| initial || new) + }) +} + impl FileError { pub fn print_shortened(&self, prefix: Option<&Path>) -> String { let prefix = prefix.unwrap_or_else(|| Path::new("")); From aaf773ac940cf91fa2eed036c246a6067299079b Mon Sep 17 00:00:00 2001 From: Joshua Nelson Date: Tue, 17 Nov 2020 20:29:48 -0500 Subject: [PATCH 2/3] Update deploy.yml to also upload `deadlinks` binary --- .github/workflows/deploy.yml | 26 +++++++++++++++++--------- CHANGELOG.md | 2 ++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 555bea7..bad4133 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -16,16 +16,16 @@ jobs: include: - name: linux os: ubuntu-latest - artifact_name: cargo-deadlinks - asset_name: deadlinks-linux + suffix: "" + asset_suffix: -linux - name: windows os: windows-latest - artifact_name: cargo-deadlinks.exe - asset_name: deadlinks-windows + suffix: .exe + asset_suffix: -windows - name: macos os: macos-latest - artifact_name: cargo-deadlinks - asset_name: deadlinks-macos + suffix: "" + asset_suffix: -macos steps: - uses: actions/checkout@v1 @@ -38,10 +38,18 @@ jobs: - name: Build run: cargo build --release - - name: Upload binaries to release + - name: Upload `deadlinks` binaries uses: svenstaro/upload-release-action@v1-release with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: target/release/${{ matrix.artifact_name }} - asset_name: ${{ matrix.asset_name }} + file: target/release/deadlinks${{ matrix.suffix }} + asset_name: deadlinks${{ matrix.asset_suffix }} + tag: ${{ github.ref }} + + - name: Upload `cargo-deadlinks` binaries + uses: svenstaro/upload-release-action@v1-release + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: target/release/cargo-deadlinks${{ matrix.suffix }} + asset_name: cargo-deadlinks${{ matrix.asset_suffix }} tag: ${{ github.ref }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f33f91e..cbff953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ #### Added * `RUST_LOG` is now read, and controls logging. [PR#100] +* There is now a separate `deadlinks` binary which doesn't depend on cargo in any way. [PR#87] #### Changes @@ -12,6 +13,7 @@ * Logging now follows the standard `env_logger` format. [PR#100] * `--debug` and `--verbose` are deprecated in favor of `RUST_LOG`. [PR#100] +[PR#87]: https://github.com/deadlinks/cargo-deadlinks/pull/87 [PR#100]: https://github.com/deadlinks/cargo-deadlinks/pull/100 From d1a55defa2131cd0a58c630acb3107e5ac312960 Mon Sep 17 00:00:00 2001 From: Joshua Nelson Date: Tue, 17 Nov 2020 20:41:32 -0500 Subject: [PATCH 3/3] Make `deadlinks` the default binary for `cargo run` This shouldn't affect the end-user at all, it just makes things easier for development. --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8f3af60..73cfc17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ repository = "https://github.com/deadlinks/cargo-deadlinks" readme = "README.md" license = "MIT OR Apache-2.0" autobins = false +default-run = "deadlinks" [[bin]] name = "cargo-deadlinks"