|
| 1 | +use std::path::PathBuf; |
| 2 | + |
| 3 | +use bpaf::Bpaf; |
| 4 | + |
| 5 | +const VERSION: &str = match option_env!("OXC_VERSION") { |
| 6 | + Some(v) => v, |
| 7 | + None => "dev", |
| 8 | +}; |
| 9 | + |
| 10 | +#[expect(clippy::ptr_arg)] |
| 11 | +fn validate_paths(paths: &Vec<PathBuf>) -> bool { |
| 12 | + if paths.is_empty() { |
| 13 | + true |
| 14 | + } else { |
| 15 | + paths.iter().all(|p| p.components().all(|c| c != std::path::Component::ParentDir)) |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +const PATHS_ERROR_MESSAGE: &str = "PATH must not contain \"..\""; |
| 20 | + |
| 21 | +#[derive(Debug, Clone, Bpaf)] |
| 22 | +#[bpaf(options, version(VERSION))] |
| 23 | +pub struct FormatCommand { |
| 24 | + #[bpaf(external)] |
| 25 | + pub basic_options: BasicOptions, |
| 26 | + |
| 27 | + #[bpaf(external)] |
| 28 | + pub misc_options: MiscOptions, |
| 29 | + |
| 30 | + /// Single file, single path or list of paths |
| 31 | + #[bpaf(positional("PATH"), many, guard(validate_paths, PATHS_ERROR_MESSAGE))] |
| 32 | + pub paths: Vec<PathBuf>, |
| 33 | +} |
| 34 | + |
| 35 | +/// Basic Configuration |
| 36 | +#[derive(Debug, Clone, Bpaf)] |
| 37 | +pub struct BasicOptions { |
| 38 | + /// TODO: docs |
| 39 | + #[bpaf(switch)] |
| 40 | + pub check: bool, |
| 41 | +} |
| 42 | + |
| 43 | +/// Miscellaneous |
| 44 | +#[derive(Debug, Clone, Bpaf)] |
| 45 | +pub struct MiscOptions { |
| 46 | + /// Number of threads to use. Set to 1 for using only 1 CPU core |
| 47 | + #[bpaf(argument("INT"), hide_usage)] |
| 48 | + pub threads: Option<usize>, |
| 49 | +} |
| 50 | + |
| 51 | +impl FormatCommand { |
| 52 | + pub fn handle_threads(&self) { |
| 53 | + Self::init_rayon_thread_pool(self.misc_options.threads); |
| 54 | + } |
| 55 | + |
| 56 | + /// Initialize Rayon global thread pool with specified number of threads. |
| 57 | + /// |
| 58 | + /// If `--threads` option is not used, or `--threads 0` is given, |
| 59 | + /// default to the number of available CPU cores. |
| 60 | + #[expect(clippy::print_stderr)] |
| 61 | + fn init_rayon_thread_pool(threads: Option<usize>) { |
| 62 | + // Always initialize thread pool, even if using default thread count, |
| 63 | + // to ensure thread pool's thread count is locked after this point. |
| 64 | + // `rayon::current_num_threads()` will always return the same number after this point. |
| 65 | + // |
| 66 | + // If you don't initialize the global thread pool explicitly, or don't specify `num_threads`, |
| 67 | + // Rayon will initialize the thread pool when it's first used, with a thread count of |
| 68 | + // `std::thread::available_parallelism()`, and that thread count won't change thereafter. |
| 69 | + // So we don't *need* to initialize the thread pool here if we just want the default thread count. |
| 70 | + // |
| 71 | + // However, Rayon's docs state that: |
| 72 | + // > In the future, the default behavior may change to dynamically add or remove threads as needed. |
| 73 | + // https://docs.rs/rayon/1.11.0/rayon/struct.ThreadPoolBuilder.html#method.num_threads |
| 74 | + // |
| 75 | + // To ensure we continue to have a "locked" thread count, even after future Rayon upgrades, |
| 76 | + // we always initialize the thread pool and explicitly specify thread count here. |
| 77 | + |
| 78 | + let thread_count = if let Some(thread_count) = threads |
| 79 | + && thread_count > 0 |
| 80 | + { |
| 81 | + thread_count |
| 82 | + } else if let Ok(thread_count) = std::thread::available_parallelism() { |
| 83 | + thread_count.get() |
| 84 | + } else { |
| 85 | + eprintln!( |
| 86 | + "Unable to determine available thread count. Defaulting to 1.\nConsider specifying the number of threads explicitly with `--threads` option." |
| 87 | + ); |
| 88 | + 1 |
| 89 | + }; |
| 90 | + |
| 91 | + rayon::ThreadPoolBuilder::new().num_threads(thread_count).build_global().unwrap(); |
| 92 | + } |
| 93 | +} |
0 commit comments