use crate::{
ExpectedError, Result, ReuseBuildKind,
cargo_cli::CargoOptions,
dispatch::{
EarlyArgs,
common::ConfigOpts,
helpers::{acquire_graph_data, detect_build_platforms, runner_for_target},
},
output::{OutputContext, OutputWriter},
};
use camino::Utf8PathBuf;
use guppy::graph::PackageGraph;
use nextest_filtering::ParseContext;
use nextest_runner::{
cargo_config::CargoConfigs,
config::core::{
ConfigExperimental, EarlyProfile, ExperimentalConfig, NextestConfig, NextestVersionConfig,
NextestVersionEval, VersionOnlyConfig,
},
double_spawn::DoubleSpawnInfo,
list::BinaryList,
platform::BuildPlatforms,
reuse_build::ReuseBuildInfo,
target_runner::TargetRunner,
};
use owo_colors::OwoColorize;
use semver::Version;
use std::{
collections::BTreeSet,
env::VarError,
sync::{Arc, OnceLock},
};
use tracing::{Level, info, warn};
pub(crate) struct BaseApp {
pub(crate) output: OutputContext,
pub(crate) early_args: EarlyArgs,
pub(crate) build_platforms: BuildPlatforms,
pub(crate) cargo_metadata_json: Arc<String>,
package_graph: Arc<PackageGraph>,
pub(crate) workspace_root: Utf8PathBuf,
manifest_path: Option<Utf8PathBuf>,
pub(crate) reuse_build: ReuseBuildInfo,
pub(crate) cargo_opts: CargoOptions,
pub(crate) config_opts: ConfigOpts,
pub(crate) current_version: Version,
pub(crate) cargo_configs: CargoConfigs,
double_spawn: OnceLock<DoubleSpawnInfo>,
target_runner: OnceLock<TargetRunner>,
}
impl BaseApp {
pub(crate) fn new(
output: OutputContext,
early_args: EarlyArgs,
reuse_build: crate::reuse_build::ReuseBuildOpts,
cargo_opts: crate::cargo_cli::CargoOptions,
config_opts: ConfigOpts,
manifest_path: Option<Utf8PathBuf>,
writer: &mut OutputWriter,
) -> Result<Self> {
reuse_build.check_experimental(output);
let reuse_build = reuse_build.process(output, writer)?;
let cargo_configs = CargoConfigs::new(&cargo_opts.config).map_err(Box::new)?;
let build_platforms = match reuse_build.binaries_metadata() {
Some(kind) => kind.binary_list.rust_build_meta.build_platforms.clone(),
None => detect_build_platforms(&cargo_configs, cargo_opts.target.as_deref())?,
};
let (cargo_metadata_json, package_graph) = match reuse_build.cargo_metadata() {
Some(m) => (m.json.clone(), m.graph.clone()),
None => {
let json = acquire_graph_data(
manifest_path.as_deref(),
cargo_opts.target_dir.as_deref(),
&cargo_opts,
&build_platforms,
output,
)?;
let graph = PackageGraph::from_json(&json)
.map_err(|err| ExpectedError::cargo_metadata_parse_error(None, err))?;
(Arc::new(json), Arc::new(graph))
}
};
let manifest_path = if reuse_build.cargo_metadata.is_some() {
Some(package_graph.workspace().root().join("Cargo.toml"))
} else {
manifest_path
};
let workspace_root = match reuse_build.workspace_remap() {
Some(path) => path.to_owned(),
_ => package_graph.workspace().root().to_owned(),
};
let root_manifest_path = workspace_root.join("Cargo.toml");
if !root_manifest_path.exists() {
let reuse_build_kind = if reuse_build.workspace_remap().is_some() {
ReuseBuildKind::ReuseWithWorkspaceRemap { workspace_root }
} else if reuse_build.is_active() {
ReuseBuildKind::Reuse
} else {
ReuseBuildKind::Normal
};
return Err(ExpectedError::RootManifestNotFound {
path: root_manifest_path,
reuse_build_kind,
});
}
let current_version = current_version();
Ok(Self {
output,
early_args,
build_platforms,
cargo_metadata_json,
package_graph,
workspace_root,
reuse_build,
manifest_path,
cargo_opts,
config_opts,
cargo_configs,
current_version,
double_spawn: OnceLock::new(),
target_runner: OnceLock::new(),
})
}
pub(crate) fn load_config(
&self,
pcx: &ParseContext<'_>,
required_experimental: &BTreeSet<ConfigExperimental>,
) -> Result<(VersionOnlyConfig, NextestConfig)> {
let version_only_config = self
.config_opts
.make_version_only_config(&self.workspace_root)?;
self.check_version_config_initial(version_only_config.nextest_version())?;
self.check_experimental_config_initial(version_only_config.experimental())?;
let mut experimental = ConfigExperimental::from_env();
experimental.extend(version_only_config.experimental().known());
let missing = required_experimental
.difference(&experimental)
.copied()
.collect::<Vec<_>>();
if !missing.is_empty() {
let config_file = self
.config_opts
.config_file
.clone()
.unwrap_or_else(|| Utf8PathBuf::from(".config/nextest.toml"));
return Err(ExpectedError::ConfigExperimentalFeaturesNotEnabled {
config_file,
missing,
});
}
if !experimental.is_empty() {
info!(
"experimental features enabled: {}",
experimental
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
let config = self.config_opts.make_config(
&self.workspace_root,
pcx,
version_only_config.experimental().known(),
)?;
Ok((version_only_config, config))
}
fn check_version_config_initial(&self, version_cfg: &NextestVersionConfig) -> Result<()> {
let styles = self.output.stderr_styles();
match version_cfg.eval(
&self.current_version,
self.config_opts.override_version_check,
) {
NextestVersionEval::Satisfied => Ok(()),
NextestVersionEval::Error {
required,
current,
tool,
} => Err(ExpectedError::RequiredVersionNotMet {
required,
current,
tool,
}),
NextestVersionEval::Warn {
recommended: required,
current,
tool,
} => {
warn!(
"this repository recommends nextest version {}, but the current version is {}",
required.style(styles.bold),
current.style(styles.bold),
);
if let Some(tool) = tool {
info!(
target: "cargo_nextest::no_heading",
"(recommended version specified by tool `{}`)",
tool,
);
}
Ok(())
}
NextestVersionEval::ErrorOverride {
required,
current,
tool,
} => {
info!(
"overriding version check (required: {}, current: {})",
required, current
);
if let Some(tool) = tool {
info!(
target: "cargo_nextest::no_heading",
"(required version specified by tool `{}`)",
tool,
);
}
Ok(())
}
NextestVersionEval::WarnOverride {
recommended,
current,
tool,
} => {
info!(
"overriding version check (recommended: {}, current: {})",
recommended, current,
);
if let Some(tool) = tool {
info!(
target: "cargo_nextest::no_heading",
"(recommended version specified by tool `{}`)",
tool,
);
}
Ok(())
}
}
}
fn check_experimental_config_initial(
&self,
experimental_cfg: &ExperimentalConfig,
) -> Result<()> {
let config_file = self
.config_opts
.config_file
.clone()
.unwrap_or_else(|| self.workspace_root.join(NextestConfig::CONFIG_PATH));
if let Some(err) = experimental_cfg.eval().into_error(config_file) {
Err(err.into())
} else {
Ok(())
}
}
pub(crate) fn check_version_config_final(
&self,
version_cfg: &NextestVersionConfig,
) -> Result<()> {
let styles = self.output.stderr_styles();
match version_cfg.eval(
&self.current_version,
self.config_opts.override_version_check,
) {
NextestVersionEval::Satisfied => Ok(()),
NextestVersionEval::Error {
required,
current,
tool,
} => Err(ExpectedError::RequiredVersionNotMet {
required,
current,
tool,
}),
NextestVersionEval::Warn {
recommended: required,
current,
tool,
} => {
warn!(
"this repository recommends nextest version {}, but the current version is {}",
required.style(styles.bold),
current.style(styles.bold),
);
if let Some(tool) = tool {
info!(
target: "cargo_nextest::no_heading",
"(recommended version specified by tool `{}`)",
tool,
);
}
crate::helpers::log_needs_update(
Level::INFO,
crate::helpers::BYPASS_VERSION_TEXT,
&styles,
);
Ok(())
}
NextestVersionEval::ErrorOverride { .. } | NextestVersionEval::WarnOverride { .. } => {
Ok(())
}
}
}
pub(crate) fn load_double_spawn(&self) -> &DoubleSpawnInfo {
self.double_spawn.get_or_init(|| {
if std::env::var("NEXTEST_EXPERIMENTAL_DOUBLE_SPAWN").is_ok() {
warn!(
"double-spawn is no longer experimental: \
NEXTEST_EXPERIMENTAL_DOUBLE_SPAWN does not need to be set"
);
}
if std::env::var("NEXTEST_DOUBLE_SPAWN") == Ok("0".to_owned()) {
info!("NEXTEST_DOUBLE_SPAWN=0 set, disabling double-spawn for test processes");
DoubleSpawnInfo::disabled()
} else {
DoubleSpawnInfo::try_enable()
}
})
}
pub(crate) fn load_runner(&self, build_platforms: &BuildPlatforms) -> &TargetRunner {
self.target_runner.get_or_init(|| {
runner_for_target(
&self.cargo_configs,
build_platforms,
&self.output.stderr_styles(),
)
})
}
pub(crate) fn build_scope_args(&self) -> Vec<String> {
self.cargo_opts
.build_scope
.to_cli_args()
.into_iter()
.map(|s| s.to_owned())
.collect()
}
pub(crate) fn build_binary_list(&self, cargo_command: &str) -> Result<Arc<BinaryList>> {
let binary_list = match self.reuse_build.binaries_metadata() {
Some(m) => m.binary_list.clone(),
None => Arc::new(self.cargo_opts.compute_binary_list(
cargo_command,
self.graph(),
self.manifest_path.as_deref(),
self.output,
self.build_platforms.clone(),
)?),
};
Ok(binary_list)
}
pub(crate) fn build_binary_list_with_rerun(
&self,
cargo_command: &str,
rerun_build_scope: Option<&[String]>,
) -> Result<Arc<BinaryList>> {
if let Some(m) = self.reuse_build.binaries_metadata() {
return Ok(m.binary_list.clone());
}
let cli_has_scope = self.cargo_opts.build_scope.has_any();
let inherited_scope = match (rerun_build_scope, cli_has_scope) {
(Some(scope), false) => {
if scope.is_empty() {
info!("rerun: inheriting build scope from original run: (default scope)");
} else {
info!(
"rerun: inheriting build scope from original run: {}",
scope.join(" ")
);
}
Some(scope)
}
(Some(_), true) => {
info!("rerun: using provided build scope, not inheriting from original run");
None
}
(None, _) => None,
};
let binary_list = if let Some(scope) = inherited_scope {
self.cargo_opts.compute_binary_list_with_inherited(
cargo_command,
scope,
self.graph(),
self.manifest_path.as_deref(),
self.output,
self.build_platforms.clone(),
)?
} else {
self.cargo_opts.compute_binary_list(
cargo_command,
self.graph(),
self.manifest_path.as_deref(),
self.output,
self.build_platforms.clone(),
)?
};
Ok(Arc::new(binary_list))
}
#[inline]
pub(crate) fn graph(&self) -> &PackageGraph {
&self.package_graph
}
pub(crate) fn load_profile<'cfg>(
&self,
config: &'cfg NextestConfig,
) -> Result<EarlyProfile<'cfg>> {
let profile_name = self.config_opts.profile.as_deref().unwrap_or_else(|| {
if std::env::var_os("MIRI_SYSROOT").is_some() {
NextestConfig::DEFAULT_MIRI_PROFILE
} else {
NextestConfig::DEFAULT_PROFILE
}
});
let profile = config
.profile(profile_name)
.map_err(ExpectedError::profile_not_found)?;
if profile.has_junit() {
let store_dir = profile.store_dir();
std::fs::create_dir_all(store_dir).map_err(|err| {
ExpectedError::StoreDirCreateError {
store_dir: store_dir.to_owned(),
err,
}
})?;
}
Ok(profile)
}
}
pub(crate) fn current_version() -> Version {
match std::env::var("__NEXTEST_TEST_VERSION") {
Ok(version) => version
.parse()
.expect("__NEXTEST_TEST_VERSION should be a valid semver version"),
Err(VarError::NotPresent) => env!("CARGO_PKG_VERSION")
.parse()
.expect("CARGO_PKG_VERSION should be a valid semver version"),
Err(error) => {
panic!("error reading __NEXTEST_TEST_VERSION: {error}");
}
}
}
mod helpers {
use crate::{
ExpectedError, Result,
cargo_cli::{CargoCli, CargoOptions},
dispatch::core::value_enums::CargoMessageFormatOpt,
output::OutputContext,
};
use camino::Utf8Path;
use guppy::graph::PackageGraph;
use nextest_runner::{
list::{BinaryList, BinaryListBuilder},
platform::BuildPlatforms,
};
use std::io::{BufRead, BufReader};
impl CargoOptions {
pub(crate) fn compute_binary_list(
&self,
cargo_command: &str,
graph: &PackageGraph,
manifest_path: Option<&Utf8Path>,
output: OutputContext,
build_platforms: BuildPlatforms,
) -> Result<BinaryList> {
let mut cargo_cli = CargoCli::new(cargo_command, manifest_path, output);
let message_format = CargoMessageFormatOpt::combine(&self.cargo_message_format)?;
cargo_cli.add_args(["--no-run", "--message-format", message_format.cargo_arg()]);
cargo_cli.add_options(self);
Self::run_cargo_build(
cargo_cli,
graph,
build_platforms,
message_format.forward_json(),
)
}
pub(crate) fn compute_binary_list_with_inherited(
&self,
cargo_command: &str,
inherited_build_scope: &[String],
graph: &PackageGraph,
manifest_path: Option<&Utf8Path>,
output: OutputContext,
build_platforms: BuildPlatforms,
) -> Result<BinaryList> {
let mut cargo_cli = CargoCli::new(cargo_command, manifest_path, output);
let message_format = CargoMessageFormatOpt::combine(&self.cargo_message_format)?;
cargo_cli.add_args(["--no-run", "--message-format", message_format.cargo_arg()]);
for arg in inherited_build_scope {
cargo_cli.add_owned_arg(arg.clone());
}
cargo_cli.add_non_build_scope_options(self);
Self::run_cargo_build(
cargo_cli,
graph,
build_platforms,
message_format.forward_json(),
)
}
fn run_cargo_build(
cargo_cli: CargoCli<'_>,
graph: &PackageGraph,
build_platforms: BuildPlatforms,
forward_json: bool,
) -> Result<BinaryList> {
let expression = cargo_cli.to_expression();
let reader_handle = expression
.stdout_capture()
.unchecked()
.reader()
.map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?;
let mut binary_list_builder = BinaryListBuilder::new(graph, build_platforms);
for line in BufReader::new(&reader_handle).lines() {
let line = line
.map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?;
if forward_json {
println!("{}", line);
}
binary_list_builder.process_message_line(&line)?;
}
let output = reader_handle
.try_wait()
.map_err(|err| ExpectedError::build_exec_failed(cargo_cli.all_args(), err))?
.expect("child process should have exited after EOF");
if !output.status.success() {
return Err(ExpectedError::build_failed(
cargo_cli.all_args(),
output.status.code(),
));
}
Ok(binary_list_builder.finish())
}
}
}