| use core::fmt::{self, Display}; |
| use core::num::NonZero; |
| use core::ops::Range; |
| use core::slice; |
| use core::str::FromStr; |
| use rustc_lexer::{self as lexer, FrontmatterAllowed}; |
| use std::ffi::OsStr; |
| use std::fs::{self, OpenOptions}; |
| use std::io::{self, Read as _, Seek as _, SeekFrom, Write}; |
| use std::path::{Path, PathBuf}; |
| use std::process::{self, Command, Stdio}; |
| use std::{env, thread}; |
| use walkdir::WalkDir; |
| |
| #[derive(Clone, Copy)] |
| pub enum ErrAction { |
| Open, |
| Read, |
| Write, |
| Create, |
| Rename, |
| Delete, |
| Run, |
| } |
| impl ErrAction { |
| fn as_str(self) -> &'static str { |
| match self { |
| Self::Open => "opening", |
| Self::Read => "reading", |
| Self::Write => "writing", |
| Self::Create => "creating", |
| Self::Rename => "renaming", |
| Self::Delete => "deleting", |
| Self::Run => "running", |
| } |
| } |
| } |
| |
| #[cold] |
| #[track_caller] |
| pub fn panic_action(err: &impl Display, action: ErrAction, path: &Path) -> ! { |
| panic!("error {} `{}`: {}", action.as_str(), path.display(), *err) |
| } |
| |
| #[track_caller] |
| pub fn expect_action<T>(res: Result<T, impl Display>, action: ErrAction, path: impl AsRef<Path>) -> T { |
| match res { |
| Ok(x) => x, |
| Err(ref e) => panic_action(e, action, path.as_ref()), |
| } |
| } |
| |
| /// Wrapper around `std::fs::File` which panics with a path on failure. |
| pub struct File<'a> { |
| pub inner: fs::File, |
| pub path: &'a Path, |
| } |
| impl<'a> File<'a> { |
| /// Opens a file panicking on failure. |
| #[track_caller] |
| pub fn open(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Self { |
| let path = path.as_ref(); |
| Self { |
| inner: expect_action(options.open(path), ErrAction::Open, path), |
| path, |
| } |
| } |
| |
| /// Opens a file if it exists, panicking on any other failure. |
| #[track_caller] |
| pub fn open_if_exists(path: &'a (impl AsRef<Path> + ?Sized), options: &mut OpenOptions) -> Option<Self> { |
| let path = path.as_ref(); |
| match options.open(path) { |
| Ok(inner) => Some(Self { inner, path }), |
| Err(e) if e.kind() == io::ErrorKind::NotFound => None, |
| Err(e) => panic_action(&e, ErrAction::Open, path), |
| } |
| } |
| |
| /// Opens and reads a file into a string, panicking of failure. |
| #[track_caller] |
| pub fn open_read_to_cleared_string<'dst>( |
| path: &'a (impl AsRef<Path> + ?Sized), |
| dst: &'dst mut String, |
| ) -> &'dst mut String { |
| Self::open(path, OpenOptions::new().read(true)).read_to_cleared_string(dst) |
| } |
| |
| /// Read the entire contents of a file to the given buffer. |
| #[track_caller] |
| pub fn read_append_to_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String { |
| expect_action(self.inner.read_to_string(dst), ErrAction::Read, self.path); |
| dst |
| } |
| |
| #[track_caller] |
| pub fn read_to_cleared_string<'dst>(&mut self, dst: &'dst mut String) -> &'dst mut String { |
| dst.clear(); |
| self.read_append_to_string(dst) |
| } |
| |
| /// Replaces the entire contents of a file. |
| #[track_caller] |
| pub fn replace_contents(&mut self, data: &[u8]) { |
| let res = match self.inner.seek(SeekFrom::Start(0)) { |
| Ok(_) => match self.inner.write_all(data) { |
| Ok(()) => self.inner.set_len(data.len() as u64), |
| Err(e) => Err(e), |
| }, |
| Err(e) => Err(e), |
| }; |
| expect_action(res, ErrAction::Write, self.path); |
| } |
| } |
| |
| /// Creates a `Command` for running cargo. |
| #[must_use] |
| pub fn cargo_cmd() -> Command { |
| if let Some(path) = env::var_os("CARGO") { |
| Command::new(path) |
| } else { |
| Command::new("cargo") |
| } |
| } |
| |
| #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| pub struct Version { |
| pub major: u16, |
| pub minor: u16, |
| } |
| impl FromStr for Version { |
| type Err = (); |
| fn from_str(s: &str) -> Result<Self, Self::Err> { |
| if let Some(s) = s.strip_prefix("0.") |
| && let Some((major, minor)) = s.split_once('.') |
| && let Ok(major) = major.parse() |
| && let Ok(minor) = minor.parse() |
| { |
| Ok(Self { major, minor }) |
| } else { |
| Err(()) |
| } |
| } |
| } |
| impl Version { |
| /// Displays the version as a rust version. i.e. `x.y.0` |
| #[must_use] |
| pub fn rust_display(self) -> impl Display { |
| struct X(Version); |
| impl Display for X { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "{}.{}.0", self.0.major, self.0.minor) |
| } |
| } |
| X(self) |
| } |
| |
| /// Displays the version as it should appear in clippy's toml files. i.e. `0.x.y` |
| #[must_use] |
| pub fn toml_display(self) -> impl Display { |
| struct X(Version); |
| impl Display for X { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| write!(f, "0.{}.{}", self.0.major, self.0.minor) |
| } |
| } |
| X(self) |
| } |
| } |
| |
| enum TomlPart<'a> { |
| Table(&'a str), |
| Value(&'a str, &'a str), |
| } |
| |
| fn toml_iter(s: &str) -> impl Iterator<Item = (usize, TomlPart<'_>)> { |
| let mut pos = 0; |
| s.split('\n') |
| .map(move |s| { |
| let x = pos; |
| pos += s.len() + 1; |
| (x, s) |
| }) |
| .filter_map(|(pos, s)| { |
| if let Some(s) = s.strip_prefix('[') { |
| s.split_once(']').map(|(name, _)| (pos, TomlPart::Table(name))) |
| } else if matches!(s.bytes().next(), Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) { |
| s.split_once('=').map(|(key, value)| (pos, TomlPart::Value(key, value))) |
| } else { |
| None |
| } |
| }) |
| } |
| |
| pub struct CargoPackage<'a> { |
| pub name: &'a str, |
| pub version_range: Range<usize>, |
| pub not_a_platform_range: Range<usize>, |
| } |
| |
| #[must_use] |
| pub fn parse_cargo_package(s: &str) -> CargoPackage<'_> { |
| let mut in_package = false; |
| let mut in_platform_deps = false; |
| let mut name = ""; |
| let mut version_range = 0..0; |
| let mut not_a_platform_range = 0..0; |
| for (offset, part) in toml_iter(s) { |
| match part { |
| TomlPart::Table(name) => { |
| if in_platform_deps { |
| not_a_platform_range.end = offset; |
| } |
| in_package = false; |
| in_platform_deps = false; |
| |
| match name.trim() { |
| "package" => in_package = true, |
| "target.'cfg(NOT_A_PLATFORM)'.dependencies" => { |
| in_platform_deps = true; |
| not_a_platform_range.start = offset; |
| }, |
| _ => {}, |
| } |
| }, |
| TomlPart::Value(key, value) if in_package => match key.trim_end() { |
| "name" => name = value.trim(), |
| "version" => { |
| version_range.start = offset + (value.len() - value.trim().len()) + key.len() + 1; |
| version_range.end = offset + key.len() + value.trim_end().len() + 1; |
| }, |
| _ => {}, |
| }, |
| TomlPart::Value(..) => {}, |
| } |
| } |
| CargoPackage { |
| name, |
| version_range, |
| not_a_platform_range, |
| } |
| } |
| |
| pub struct ClippyInfo { |
| pub path: PathBuf, |
| pub version: Version, |
| pub has_intellij_hook: bool, |
| } |
| impl ClippyInfo { |
| #[must_use] |
| pub fn search_for_manifest() -> Self { |
| let mut path = env::current_dir().expect("error reading the working directory"); |
| let mut buf = String::new(); |
| loop { |
| path.push("Cargo.toml"); |
| if let Some(mut file) = File::open_if_exists(&path, OpenOptions::new().read(true)) { |
| file.read_to_cleared_string(&mut buf); |
| let package = parse_cargo_package(&buf); |
| if package.name == "\"clippy\"" { |
| if let Some(version) = buf[package.version_range].strip_prefix('"') |
| && let Some(version) = version.strip_suffix('"') |
| && let Ok(version) = version.parse() |
| { |
| path.pop(); |
| return ClippyInfo { |
| path, |
| version, |
| has_intellij_hook: !package.not_a_platform_range.is_empty(), |
| }; |
| } |
| panic!("error reading clippy version from `{}`", file.path.display()); |
| } |
| } |
| |
| path.pop(); |
| assert!( |
| path.pop(), |
| "error finding project root, please run from inside the clippy directory" |
| ); |
| } |
| } |
| } |
| |
| #[derive(Clone, Copy)] |
| pub enum UpdateStatus { |
| Unchanged, |
| Changed, |
| } |
| impl UpdateStatus { |
| #[must_use] |
| pub fn from_changed(value: bool) -> Self { |
| if value { Self::Changed } else { Self::Unchanged } |
| } |
| |
| #[must_use] |
| pub fn is_changed(self) -> bool { |
| matches!(self, Self::Changed) |
| } |
| } |
| |
| #[derive(Clone, Copy)] |
| pub enum UpdateMode { |
| Change, |
| Check, |
| } |
| impl UpdateMode { |
| #[must_use] |
| pub fn from_check(check: bool) -> Self { |
| if check { Self::Check } else { Self::Change } |
| } |
| |
| #[must_use] |
| pub fn is_check(self) -> bool { |
| matches!(self, Self::Check) |
| } |
| } |
| |
| #[derive(Default)] |
| pub struct FileUpdater { |
| src_buf: String, |
| dst_buf: String, |
| } |
| impl FileUpdater { |
| #[track_caller] |
| fn update_file_checked_inner( |
| &mut self, |
| tool: &str, |
| mode: UpdateMode, |
| path: &Path, |
| update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, |
| ) { |
| let mut file = File::open(path, OpenOptions::new().read(true).write(true)); |
| file.read_to_cleared_string(&mut self.src_buf); |
| self.dst_buf.clear(); |
| match (mode, update(path, &self.src_buf, &mut self.dst_buf)) { |
| (UpdateMode::Check, UpdateStatus::Changed) => { |
| eprintln!( |
| "the contents of `{}` are out of date\nplease run `{tool}` to update", |
| path.display() |
| ); |
| process::exit(1); |
| }, |
| (UpdateMode::Change, UpdateStatus::Changed) => file.replace_contents(self.dst_buf.as_bytes()), |
| (UpdateMode::Check | UpdateMode::Change, UpdateStatus::Unchanged) => {}, |
| } |
| } |
| |
| #[track_caller] |
| fn update_file_inner(&mut self, path: &Path, update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus) { |
| let mut file = File::open(path, OpenOptions::new().read(true).write(true)); |
| file.read_to_cleared_string(&mut self.src_buf); |
| self.dst_buf.clear(); |
| if update(path, &self.src_buf, &mut self.dst_buf).is_changed() { |
| file.replace_contents(self.dst_buf.as_bytes()); |
| } |
| } |
| |
| #[track_caller] |
| pub fn update_file_checked( |
| &mut self, |
| tool: &str, |
| mode: UpdateMode, |
| path: impl AsRef<Path>, |
| update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, |
| ) { |
| self.update_file_checked_inner(tool, mode, path.as_ref(), update); |
| } |
| |
| #[track_caller] |
| pub fn update_file( |
| &mut self, |
| path: impl AsRef<Path>, |
| update: &mut dyn FnMut(&Path, &str, &mut String) -> UpdateStatus, |
| ) { |
| self.update_file_inner(path.as_ref(), update); |
| } |
| } |
| |
| /// Replaces a region in a text delimited by two strings. Returns the new text if both delimiters |
| /// were found, or the missing delimiter if not. |
| pub fn update_text_region( |
| path: &Path, |
| start: &str, |
| end: &str, |
| src: &str, |
| dst: &mut String, |
| insert: &mut impl FnMut(&mut String), |
| ) -> UpdateStatus { |
| let Some((src_start, src_end)) = src.split_once(start) else { |
| panic!("`{}` does not contain `{start}`", path.display()); |
| }; |
| let Some((replaced_text, src_end)) = src_end.split_once(end) else { |
| panic!("`{}` does not contain `{end}`", path.display()); |
| }; |
| dst.push_str(src_start); |
| dst.push_str(start); |
| let new_start = dst.len(); |
| insert(dst); |
| let changed = dst[new_start..] != *replaced_text; |
| dst.push_str(end); |
| dst.push_str(src_end); |
| UpdateStatus::from_changed(changed) |
| } |
| |
| pub fn update_text_region_fn( |
| start: &str, |
| end: &str, |
| mut insert: impl FnMut(&mut String), |
| ) -> impl FnMut(&Path, &str, &mut String) -> UpdateStatus { |
| move |path, src, dst| update_text_region(path, start, end, src, dst, &mut insert) |
| } |
| |
| #[derive(Clone, Copy)] |
| pub enum Token<'a> { |
| /// Matches any number of comments / doc comments. |
| AnyComment, |
| Ident(&'a str), |
| CaptureIdent, |
| LitStr, |
| CaptureLitStr, |
| Bang, |
| CloseBrace, |
| CloseBracket, |
| CloseParen, |
| /// This will consume the first colon even if the second doesn't exist. |
| DoubleColon, |
| Comma, |
| Eq, |
| Lifetime, |
| Lt, |
| Gt, |
| OpenBrace, |
| OpenBracket, |
| OpenParen, |
| Pound, |
| Semi, |
| } |
| |
| pub struct RustSearcher<'txt> { |
| text: &'txt str, |
| cursor: lexer::Cursor<'txt>, |
| pos: u32, |
| next_token: lexer::Token, |
| } |
| impl<'txt> RustSearcher<'txt> { |
| #[must_use] |
| #[expect(clippy::inconsistent_struct_constructor)] |
| pub fn new(text: &'txt str) -> Self { |
| let mut cursor = lexer::Cursor::new(text, FrontmatterAllowed::Yes); |
| Self { |
| text, |
| pos: 0, |
| next_token: cursor.advance_token(), |
| cursor, |
| } |
| } |
| |
| #[must_use] |
| pub fn peek_text(&self) -> &'txt str { |
| &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] |
| } |
| |
| #[must_use] |
| pub fn peek_len(&self) -> u32 { |
| self.next_token.len |
| } |
| |
| #[must_use] |
| pub fn peek(&self) -> lexer::TokenKind { |
| self.next_token.kind |
| } |
| |
| #[must_use] |
| pub fn pos(&self) -> u32 { |
| self.pos |
| } |
| |
| #[must_use] |
| pub fn at_end(&self) -> bool { |
| self.next_token.kind == lexer::TokenKind::Eof |
| } |
| |
| pub fn step(&mut self) { |
| // `next_len` is zero for the sentinel value and the eof marker. |
| self.pos += self.next_token.len; |
| self.next_token = self.cursor.advance_token(); |
| } |
| |
| /// Consumes the next token if it matches the requested value and captures the value if |
| /// requested. Returns true if a token was matched. |
| fn read_token(&mut self, token: Token<'_>, captures: &mut slice::IterMut<'_, &mut &'txt str>) -> bool { |
| loop { |
| match (token, self.next_token.kind) { |
| (_, lexer::TokenKind::Whitespace) |
| | ( |
| Token::AnyComment, |
| lexer::TokenKind::BlockComment { terminated: true, .. } | lexer::TokenKind::LineComment { .. }, |
| ) => self.step(), |
| (Token::AnyComment, _) => return true, |
| (Token::Bang, lexer::TokenKind::Bang) |
| | (Token::CloseBrace, lexer::TokenKind::CloseBrace) |
| | (Token::CloseBracket, lexer::TokenKind::CloseBracket) |
| | (Token::CloseParen, lexer::TokenKind::CloseParen) |
| | (Token::Comma, lexer::TokenKind::Comma) |
| | (Token::Eq, lexer::TokenKind::Eq) |
| | (Token::Lifetime, lexer::TokenKind::Lifetime { .. }) |
| | (Token::Lt, lexer::TokenKind::Lt) |
| | (Token::Gt, lexer::TokenKind::Gt) |
| | (Token::OpenBrace, lexer::TokenKind::OpenBrace) |
| | (Token::OpenBracket, lexer::TokenKind::OpenBracket) |
| | (Token::OpenParen, lexer::TokenKind::OpenParen) |
| | (Token::Pound, lexer::TokenKind::Pound) |
| | (Token::Semi, lexer::TokenKind::Semi) |
| | ( |
| Token::LitStr, |
| lexer::TokenKind::Literal { |
| kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, |
| .. |
| }, |
| ) => { |
| self.step(); |
| return true; |
| }, |
| (Token::Ident(x), lexer::TokenKind::Ident) if x == self.peek_text() => { |
| self.step(); |
| return true; |
| }, |
| (Token::DoubleColon, lexer::TokenKind::Colon) => { |
| self.step(); |
| if !self.at_end() && matches!(self.next_token.kind, lexer::TokenKind::Colon) { |
| self.step(); |
| return true; |
| } |
| return false; |
| }, |
| ( |
| Token::CaptureLitStr, |
| lexer::TokenKind::Literal { |
| kind: lexer::LiteralKind::Str { terminated: true } | lexer::LiteralKind::RawStr { .. }, |
| .. |
| }, |
| ) |
| | (Token::CaptureIdent, lexer::TokenKind::Ident) => { |
| **captures.next().unwrap() = self.peek_text(); |
| self.step(); |
| return true; |
| }, |
| _ => return false, |
| } |
| } |
| } |
| |
| #[must_use] |
| pub fn find_token(&mut self, token: Token<'_>) -> bool { |
| let mut capture = [].iter_mut(); |
| while !self.read_token(token, &mut capture) { |
| self.step(); |
| if self.at_end() { |
| return false; |
| } |
| } |
| true |
| } |
| |
| #[must_use] |
| pub fn find_capture_token(&mut self, token: Token<'_>) -> Option<&'txt str> { |
| let mut res = ""; |
| let mut capture = &mut res; |
| let mut capture = slice::from_mut(&mut capture).iter_mut(); |
| while !self.read_token(token, &mut capture) { |
| self.step(); |
| if self.at_end() { |
| return None; |
| } |
| } |
| Some(res) |
| } |
| |
| #[must_use] |
| pub fn match_tokens(&mut self, tokens: &[Token<'_>], captures: &mut [&mut &'txt str]) -> bool { |
| let mut captures = captures.iter_mut(); |
| tokens.iter().all(|&t| self.read_token(t, &mut captures)) |
| } |
| } |
| |
| #[track_caller] |
| pub fn try_rename_file(old_name: &Path, new_name: &Path) -> bool { |
| match OpenOptions::new().create_new(true).write(true).open(new_name) { |
| Ok(file) => drop(file), |
| Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false, |
| Err(ref e) => panic_action(e, ErrAction::Create, new_name), |
| } |
| match fs::rename(old_name, new_name) { |
| Ok(()) => true, |
| Err(ref e) => { |
| drop(fs::remove_file(new_name)); |
| // `NotADirectory` happens on posix when renaming a directory to an existing file. |
| // Windows will ignore this and rename anyways. |
| if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) { |
| false |
| } else { |
| panic_action(e, ErrAction::Rename, old_name); |
| } |
| }, |
| } |
| } |
| |
| #[track_caller] |
| pub fn try_rename_dir(old_name: &Path, new_name: &Path) -> bool { |
| match fs::create_dir(new_name) { |
| Ok(()) => {}, |
| Err(e) if matches!(e.kind(), io::ErrorKind::AlreadyExists | io::ErrorKind::NotFound) => return false, |
| Err(ref e) => panic_action(e, ErrAction::Create, new_name), |
| } |
| // Windows can't reliably rename to an empty directory. |
| #[cfg(windows)] |
| drop(fs::remove_dir(new_name)); |
| match fs::rename(old_name, new_name) { |
| Ok(()) => true, |
| Err(ref e) => { |
| // Already dropped earlier on windows. |
| #[cfg(not(windows))] |
| drop(fs::remove_dir(new_name)); |
| // `NotADirectory` happens on posix when renaming a file to an existing directory. |
| if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) { |
| false |
| } else { |
| panic_action(e, ErrAction::Rename, old_name); |
| } |
| }, |
| } |
| } |
| |
| #[track_caller] |
| pub fn run_exit_on_err(path: &(impl AsRef<Path> + ?Sized), cmd: &mut Command) { |
| match expect_action(cmd.status(), ErrAction::Run, path.as_ref()).code() { |
| Some(0) => {}, |
| Some(n) => process::exit(n), |
| None => { |
| eprintln!("{} killed by signal", path.as_ref().display()); |
| process::exit(1); |
| }, |
| } |
| } |
| |
| #[track_caller] |
| #[must_use] |
| pub fn run_with_output(path: &(impl AsRef<Path> + ?Sized), cmd: &mut Command) -> Vec<u8> { |
| fn f(path: &Path, cmd: &mut Command) -> Vec<u8> { |
| let output = expect_action( |
| cmd.stdin(Stdio::null()) |
| .stdout(Stdio::piped()) |
| .stderr(Stdio::inherit()) |
| .output(), |
| ErrAction::Run, |
| path, |
| ); |
| expect_action(output.status.exit_ok(), ErrAction::Run, path); |
| output.stdout |
| } |
| f(path.as_ref(), cmd) |
| } |
| |
| /// Splits an argument list across multiple `Command` invocations. |
| /// |
| /// The argument list will be split into a number of batches based on |
| /// `thread::available_parallelism`, with `min_batch_size` setting a lower bound on the size of each |
| /// batch. |
| /// |
| /// If the size of the arguments would exceed the system limit additional batches will be created. |
| pub fn split_args_for_threads( |
| min_batch_size: usize, |
| make_cmd: impl FnMut() -> Command, |
| args: impl ExactSizeIterator<Item: AsRef<OsStr>>, |
| ) -> impl Iterator<Item = Command> { |
| struct Iter<F, I> { |
| make_cmd: F, |
| args: I, |
| min_batch_size: usize, |
| batch_size: usize, |
| thread_count: usize, |
| } |
| impl<F, I> Iterator for Iter<F, I> |
| where |
| F: FnMut() -> Command, |
| I: ExactSizeIterator<Item: AsRef<OsStr>>, |
| { |
| type Item = Command; |
| fn next(&mut self) -> Option<Self::Item> { |
| if self.thread_count > 1 { |
| self.thread_count -= 1; |
| } |
| let mut cmd = (self.make_cmd)(); |
| let mut cmd_len = 0usize; |
| for arg in self.args.by_ref().take(self.batch_size) { |
| cmd.arg(arg.as_ref()); |
| // `+ 8` to account for the `argv` pointer on unix. |
| // Windows is complicated since the arguments are first converted to UTF-16ish, |
| // but this needs to account for the space between arguments and whatever additional |
| // is needed to escape within an argument. |
| cmd_len += arg.as_ref().len() + 8; |
| cmd_len += 8; |
| |
| // Windows has a command length limit of 32767. For unix systems this is more |
| // complicated since the limit includes environment variables and room needs to be |
| // left to edit them once the program starts, but the total size comes from |
| // `getconf ARG_MAX`. |
| // |
| // For simplicity we use 30000 here under a few assumptions. |
| // * Individual arguments aren't super long (the final argument is still added) |
| // * `ARG_MAX` is set to a reasonable amount. Basically every system will be configured way above |
| // what windows supports, but POSIX only requires `4096`. |
| if cmd_len > 30000 { |
| self.batch_size = self.args.len().div_ceil(self.thread_count).max(self.min_batch_size); |
| break; |
| } |
| } |
| (cmd_len != 0).then_some(cmd) |
| } |
| } |
| let thread_count = thread::available_parallelism().map_or(1, NonZero::get); |
| let batch_size = args.len().div_ceil(thread_count).max(min_batch_size); |
| Iter { |
| make_cmd, |
| args, |
| min_batch_size, |
| batch_size, |
| thread_count, |
| } |
| } |
| |
| #[track_caller] |
| pub fn delete_file_if_exists(path: &Path) -> bool { |
| match fs::remove_file(path) { |
| Ok(()) => true, |
| Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::IsADirectory) => false, |
| Err(ref e) => panic_action(e, ErrAction::Delete, path), |
| } |
| } |
| |
| #[track_caller] |
| pub fn delete_dir_if_exists(path: &Path) { |
| match fs::remove_dir_all(path) { |
| Ok(()) => {}, |
| Err(e) if matches!(e.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) => {}, |
| Err(ref e) => panic_action(e, ErrAction::Delete, path), |
| } |
| } |
| |
| /// Walks all items excluding top-level dot files/directories and any target directories. |
| pub fn walk_dir_no_dot_or_target() -> impl Iterator<Item = ::walkdir::Result<::walkdir::DirEntry>> { |
| WalkDir::new(".").into_iter().filter_entry(|e| { |
| e.path() |
| .file_name() |
| .is_none_or(|x| x != "target" && x.as_encoded_bytes().first().copied() != Some(b'.')) |
| }) |
| } |