Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 92 additions & 6 deletions crates/turborepo-lib/src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ use turborepo_types::{
use usage::{Args as UsageArgs, Cli, Subcommands, ValueEnum};

use super::{exit_with_heap_profile, observability};
use crate::{commands::prune, get_version};

const DEFAULT_NUM_WORKERS: u32 = 10;
use crate::{
commands::prune,
get_version,
opts::{ExecutionSelector, RunSelector, DEFAULT_CACHE_WORKERS},
};
const SUPPORTED_GRAPH_FILE_EXTENSIONS: [&str; 8] =
["svg", "png", "jpg", "pdf", "json", "html", "mermaid", "dot"];

Expand Down Expand Up @@ -966,6 +968,56 @@ impl Args {
}
}

/// Normalize command-specific arguments into parser-agnostic selectors.
///
/// Call this after runtime CLI normalization has applied repository mode
/// and package inference to the parsed arguments.
pub(crate) fn selectors(&self) -> (RunSelector, ExecutionSelector) {
let default_execution_selector = ExecutionSelector {
single_package: self.single_package,
..Default::default()
};

match &self.command {
Some(Command::Run {
run_args,
execution_args,
}) => (run_args.into(), execution_args.into()),
Some(Command::Watch { execution_args, .. }) => {
(RunSelector::default(), execution_args.into())
}
Some(Command::Ls {
affected, filter, ..
}) => (
RunSelector::default(),
ExecutionSelector {
filter: filter.clone(),
affected: *affected,
..Default::default()
},
),
Some(Command::Boundaries { filter, .. }) => (
RunSelector::default(),
ExecutionSelector {
filter: filter.clone(),
..Default::default()
},
),
Some(Command::Query {
subcommand: Some(QuerySubcommand::Ls(ls_args)),
..
}) => (
RunSelector::default(),
ExecutionSelector {
filter: ls_args.filter.clone(),
affected: ls_args.affected,
..Default::default()
},
),
_ => (RunSelector::default(), default_execution_selector),
}
}

/// Fetch the run args supplied to the command
pub fn run_args(&self) -> Option<&RunArgs> {
match &self.command {
Expand Down Expand Up @@ -1426,6 +1478,27 @@ pub struct ExecutionArgs {
pub pass_through_args: Vec<String>,
}

impl From<&ExecutionArgs> for ExecutionSelector {
fn from(args: &ExecutionArgs) -> Self {
Self {
output_logs: args.output_logs.map(Into::into),
log_prefix: args.log_prefix.into(),
json: args.json,
log_file: args.log_file.clone(),
tasks: args.tasks.clone(),
framework_inference: args.framework_inference,
continue_execution: args.continue_execution.into(),
pass_through_args: args.pass_through_args.clone(),
only: args.only,
single_package: args.single_package,
affected: args.affected,
global_deps: args.global_deps.clone(),
pkg_inference_root: args.pkg_inference_root.clone(),
filter: args.filter.clone(),
}
}
}

impl ExecutionArgs {
fn track(&self, telemetry: &CommandEventBuilder) {
// default to false
Expand Down Expand Up @@ -1512,7 +1585,7 @@ pub struct RunArgs {
pub no_cache: bool,

/// Set the number of concurrent cache operations (default 10)
#[usage(long, default_value_t = DEFAULT_NUM_WORKERS, default = "10")]
#[usage(long, default_value_t = DEFAULT_CACHE_WORKERS, default = "10")]
pub cache_workers: u32,
#[usage(alias = "dry", long = "dry-run", num_args = 0..=1, default_missing = "text")]
pub dry_run: Option<DryRunModeArg>,
Expand Down Expand Up @@ -1560,7 +1633,7 @@ impl Default for RunArgs {
remote_only: None,
cache: None,
force: None,
cache_workers: DEFAULT_NUM_WORKERS,
cache_workers: DEFAULT_CACHE_WORKERS,
dry_run: None,
graph: None,
no_cache: false,
Expand All @@ -1575,6 +1648,19 @@ impl Default for RunArgs {
}
}

impl From<&RunArgs> for RunSelector {
fn from(args: &RunArgs) -> Self {
Self {
graph: args.graph.as_deref().map(ToOwned::to_owned),
parallel: args.parallel,
profile: args.profile.clone(),
dry_run: args.dry_run.map(Into::into),
no_cache: args.no_cache,
cache_workers: args.cache_workers,
}
}
}

impl RunArgs {
pub fn remote_only(&self) -> Option<bool> {
let remote_only = self.remote_only?;
Expand Down Expand Up @@ -1647,7 +1733,7 @@ impl RunArgs {
telemetry.track_arg_value("dry-run", dry_run, EventType::NonSensitive);
}

if self.cache_workers != DEFAULT_NUM_WORKERS {
if self.cache_workers != DEFAULT_CACHE_WORKERS {
telemetry.track_arg_value("cache-workers", self.cache_workers, EventType::NonSensitive);
}

Expand Down
143 changes: 137 additions & 6 deletions crates/turborepo-lib/src/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ use camino::Utf8PathBuf;
use insta::assert_snapshot;
use itertools::Itertools;
use pretty_assertions::assert_eq;
use turborepo_types::LogOrder;

use crate::cli::{
ContinueModeArg, DryRunModeArg, EnvModeArg, ExecutionArgs, GenerateCommand,
GeneratorCustomArgs, GraphOutput, LogOrderArg, LogPrefixArg, NonEmptyPath, OutputLogsModeArg,
RunArgs,
use turborepo_types::{ContinueMode, DryRunMode, LogOrder, LogPrefix, OutputLogsMode};

use crate::{
cli::{
ContinueModeArg, DryRunModeArg, EnvModeArg, ExecutionArgs, GenerateCommand,
GeneratorCustomArgs, GraphOutput, LogOrderArg, LogPrefixArg, NonEmptyPath,
OutputLogsModeArg, RunArgs,
},
opts::{ExecutionSelector, RunSelector},
};

fn parse_args<I, S>(args: I) -> Result<Args, String>
Expand All @@ -28,6 +31,134 @@ fn get_subcommand(name: &str) -> &'static usage::Command<'static> {
.unwrap_or_else(|| panic!("subcommand '{name}' not found"))
}

#[test]
fn selectors_convert_run_command_arguments() {
let run_args = RunArgs {
graph: Some(GraphOutput("graph.svg".into())),
parallel: true,
profile: Some("profile.json".into()),
dry_run: Some(DryRunModeArg::Json),
no_cache: true,
cache_workers: 20,
..Default::default()
};
let execution_args = ExecutionArgs {
output_logs: Some(OutputLogsModeArg::ErrorsOnly),
log_prefix: LogPrefixArg::None,
json: true,
log_file: Some(Some("turbo.log".into())),
tasks: vec!["build".into()],
framework_inference: Some(false),
continue_execution: ContinueModeArg::Always,
pass_through_args: vec!["--watch".into()],
only: true,
single_package: true,
affected: true,
global_deps: vec![".env".into()],
pkg_inference_root: Some("apps/web".into()),
filter: vec!["web".into()],
..Default::default()
};
let args = Args {
command: Some(Command::Run {
run_args,
execution_args,
}),
..Default::default()
};

assert_eq!(
args.selectors(),
(
RunSelector {
graph: Some("graph.svg".into()),
parallel: true,
profile: Some("profile.json".into()),
dry_run: Some(DryRunMode::Json),
no_cache: true,
cache_workers: 20,
},
ExecutionSelector {
output_logs: Some(OutputLogsMode::ErrorsOnly),
log_prefix: LogPrefix::None,
json: true,
log_file: Some(Some("turbo.log".into())),
tasks: vec!["build".into()],
framework_inference: Some(false),
continue_execution: ContinueMode::Always,
pass_through_args: vec!["--watch".into()],
only: true,
single_package: true,
affected: true,
global_deps: vec![".env".into()],
pkg_inference_root: Some("apps/web".into()),
filter: vec!["web".into()],
},
)
);
}

#[test]
fn selectors_normalize_watch_command() {
let args = parse_args(["turbo", "watch", "build", "--filter", "web"]).unwrap();

assert_eq!(
args.selectors(),
(
RunSelector::default(),
ExecutionSelector {
tasks: vec!["build".into()],
framework_inference: Some(true),
filter: vec!["web".into()],
..Default::default()
}
)
);
}

#[test_case::test_case(&["turbo", "ls", "--affected", "--filter", "web"] ; "ls")]
#[test_case::test_case(&["turbo", "boundaries", "--filter", "web"] ; "boundaries")]
#[test_case::test_case(&["turbo", "query", "ls", "--affected", "--filter", "web"] ; "query ls")]
fn selectors_normalize_non_execution_commands(argv: &[&str]) {
let args = parse_args(argv).unwrap();
let (run_selector, execution_selector) = args.selectors();

assert_eq!(run_selector, RunSelector::default());
assert_eq!(execution_selector.filter, ["web"]);
assert_eq!(
execution_selector.affected,
!matches!(args.command, Some(Command::Boundaries { .. }))
);
assert_eq!(
execution_selector,
ExecutionSelector {
filter: vec!["web".into()],
affected: execution_selector.affected,
..Default::default()
}
);
}

#[test]
fn selectors_preserve_root_single_package_for_other_commands() {
let args = Args {
single_package: true,
command: Some(Command::Bin),
..Default::default()
};

assert_eq!(
args.selectors(),
(
RunSelector::default(),
ExecutionSelector {
single_package: true,
..Default::default()
}
)
);
}

#[test_case::test_case("", None ; "root")]
#[test_case::test_case("apps/web", Some("apps/web") ; "workspace")]
#[test_case::test_case("crates", Some("crates") ; "plain directory")]
Expand Down
51 changes: 48 additions & 3 deletions crates/turborepo-lib/src/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf, AnchoredSystemPathBuf
use turborepo_api_client::APIAuth;
use turborepo_cache::{CacheOpts, RemoteCacheOpts};
use turborepo_types::{
APIClientOpts, ContinueMode, DryRunMode, EnvMode, GraphOpts, LogOrder, LogPrefix, RepoOpts,
ResolvedLogOrder, ResolvedLogPrefix, RunCacheOpts, RunOptsInfo, ScopeOpts, TaskArgs, TuiOpts,
UIMode,
APIClientOpts, ContinueMode, DryRunMode, EnvMode, GraphOpts, LogOrder, LogPrefix,
OutputLogsMode, RepoOpts, ResolvedLogOrder, ResolvedLogPrefix, RunCacheOpts, RunOptsInfo,
ScopeOpts, TaskArgs, TuiOpts, UIMode,
};

use crate::{
Expand All @@ -18,6 +18,51 @@ use crate::{
Args,
};

pub(crate) const DEFAULT_CACHE_WORKERS: u32 = 10;

/// Parser-agnostic run options consumed while resolving [`Opts`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RunSelector {
pub(crate) graph: Option<String>,
pub(crate) parallel: bool,
pub(crate) profile: Option<String>,
pub(crate) dry_run: Option<DryRunMode>,
pub(crate) no_cache: bool,
pub(crate) cache_workers: u32,
}

impl Default for RunSelector {
fn default() -> Self {
Self {
graph: None,
parallel: false,
profile: None,
dry_run: None,
no_cache: false,
cache_workers: DEFAULT_CACHE_WORKERS,
}
}
}

/// Parser-agnostic execution options consumed while resolving [`Opts`].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct ExecutionSelector {
pub(crate) output_logs: Option<OutputLogsMode>,
pub(crate) log_prefix: LogPrefix,
pub(crate) json: bool,
pub(crate) log_file: Option<Option<String>>,
pub(crate) tasks: Vec<String>,
pub(crate) framework_inference: Option<bool>,
pub(crate) continue_execution: ContinueMode,
pub(crate) pass_through_args: Vec<String>,
pub(crate) only: bool,
pub(crate) single_package: bool,
pub(crate) affected: bool,
pub(crate) global_deps: Vec<String>,
pub(crate) pkg_inference_root: Option<String>,
pub(crate) filter: Vec<String>,
}

/// Why remote caching was disabled by local configuration.
/// Determined during opts resolution — no network call required.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
Expand Down
Loading