дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(build): build selected CMake targets in the Build window (#2061)
Fixes #2039 CMake configurations bypassed the Build executor and Build Project ignored the selected target, so CMake projects had no hammer, configure/build output went to Run tabs, and generated names carried a "(build)" suffix (the build directory) that read like an action. Now the toolbar hammer and Cmd+F9 build the selected CMake target and profile without running it; configure/build prerequisites of Run, Debug and CTest run in the Build window and a failed build prevents the launch; generated executables are "CMake Application" configurations; generated names add profile/build directory only when needed to tell them apart ("hello", or "hello [Debug · build-debug/]"). Saved names and configuration ids are kept. Verified on macOS.
Цей коміт міститься в:
1 джерело
6373bce476
коміт
192b59801a
27 files changed
+670
-90
No files matched your search
@@ -67,6 +67,11 @@ impl BuildStep {
|
||||
}
|
||||
}
|
||||
|
||||
/// A CMake configure/build invocation, parsed as compiler output in the Build view.
|
||||
pub fn cmake(program: &str, args: Vec<String>, cwd: PathBuf, env: BTreeMap<String, String>) -> Self {
|
||||
Self { program: program.into(), args, cwd, env, format: Format::Gcc }
|
||||
}
|
||||
|
||||
/// The command line as echoed in the console.
|
||||
pub fn display(&self) -> String {
|
||||
let program = Path::new(&self.program).file_name().map_or(self.program.clone(), |n| n.to_string_lossy().into());
|
||||
|
||||
@@ -488,15 +488,32 @@ pub async fn maven_exclude_dependency(
|
||||
.map_err(join_err)?
|
||||
}
|
||||
|
||||
fn cmake_plan(root: &Path, kind: BuildKind, configs: &[ide_run::RunConfig], id: &str) -> IdeResult<compile::BuildPlan> {
|
||||
let commands = ide_run::cmake_build_configs(configs, id)?;
|
||||
let title = commands.last().map(|c| c.name.clone()).unwrap_or_default();
|
||||
let mut steps = Vec::new();
|
||||
for mut config in commands {
|
||||
let cwd = ide_core::project_roots::resolve(root, config.cwd.as_deref().unwrap_or(Path::new("")));
|
||||
ide_run::prepare_cmake(&config, &cwd)?;
|
||||
if kind == BuildKind::Rebuild && config.args.first().is_some_and(|a| a == "--build") {
|
||||
config.args.insert(2, "--clean-first".into());
|
||||
}
|
||||
steps.push(compile::BuildStep::cmake(&config.command, config.args, cwd, config.env));
|
||||
}
|
||||
Ok(compile::BuildPlan { system: compile::BuildSystem::CMake, title, steps })
|
||||
}
|
||||
|
||||
/// Build › Build Project / Rebuild Project / Build Module (`file`: a file or folder of the
|
||||
/// module) / Build Artifacts, streaming the console and messages on `on_event`; cancellable
|
||||
/// with `cancel_request(request)`. Diagnostics of `clear` (the files of the previous build)
|
||||
/// are cleared first; the new messages are published as `build` diagnostics.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn build_run(
|
||||
host: &Host,
|
||||
window: &str,
|
||||
kind: BuildKind,
|
||||
file: Option<std::path::PathBuf>,
|
||||
cmake: Option<String>,
|
||||
request: RequestId,
|
||||
clear: Vec<std::path::PathBuf>,
|
||||
on_event: Channel<BuildEvent>,
|
||||
@@ -515,7 +532,10 @@ pub async fn build_run(
|
||||
items: Vec::new(),
|
||||
});
|
||||
}
|
||||
let plan = if let Some(workspace) = ide_workspace::workspace::Workspace::load(&root)? {
|
||||
let plan = if let Some(id) = cmake {
|
||||
let configs = super::run::project_configs(&root, &settings, &config_dir)?;
|
||||
cmake_plan(&root, kind, &configs, &id)?
|
||||
} else if let Some(workspace) = ide_workspace::workspace::Workspace::load(&root)? {
|
||||
let mut combined: Option<compile::BuildPlan> = None;
|
||||
for member in workspace.projects.iter().filter(|p| file.as_ref().is_none_or(|f| f.starts_with(p))) {
|
||||
let settings = ide_workspace::SettingsStore::load(&config_dir, Some(member)).effective();
|
||||
@@ -612,6 +632,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
&c.window,
|
||||
c.arg("kind")?,
|
||||
c.arg("file")?,
|
||||
c.arg("cmake")?,
|
||||
c.arg("request")?,
|
||||
c.arg("clear")?,
|
||||
c.sender("onEvent")?,
|
||||
@@ -624,6 +645,42 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
mod build_tool_settings_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cmake_builds_keep_target_configuration_environment_and_directory() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let member = root.path().join("nested project");
|
||||
std::fs::create_dir_all(&member).unwrap();
|
||||
let config = |id: &str, args: &[&str]| -> ide_run::RunConfig {
|
||||
serde_json::from_value(serde_json::json!({
|
||||
"id":id,"name":id,"kind":"cmake","command":"custom-cmake",
|
||||
"args":args,"cwd":"nested project","env":{"PROFILE":"release"},
|
||||
"detected":true,"isTest":false,"debugAdapter":null
|
||||
}))
|
||||
.unwrap()
|
||||
};
|
||||
let configure = config("configure", &["-S", ".", "-B", "release", "-G", "Ninja"]);
|
||||
let mut build =
|
||||
config("target", &["--build", "release", "--target", "hello", "--config", "Release", "-j", "8"]);
|
||||
build.extras.before_launch.push(ide_run::BeforeLaunchTask { kind: "build".into(), target: "configure".into() });
|
||||
let configs = [configure, build];
|
||||
for kind in [BuildKind::Build, BuildKind::Rebuild] {
|
||||
let plan = cmake_plan(root.path(), kind, &configs, "target").unwrap();
|
||||
assert_eq!(plan.system, compile::BuildSystem::CMake);
|
||||
assert_eq!(plan.steps.len(), 2);
|
||||
assert_eq!(plan.steps[0].args, configs[0].args);
|
||||
let mut args = configs[1].args.clone();
|
||||
if kind == BuildKind::Rebuild {
|
||||
args.insert(2, "--clean-first".into());
|
||||
}
|
||||
assert_eq!(plan.steps[1].args, args);
|
||||
for step in plan.steps {
|
||||
assert_eq!(step.program, "custom-cmake");
|
||||
assert_eq!(step.cwd, member);
|
||||
assert_eq!(step.env["PROFILE"], "release");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_apply_the_same_explicit_toolchains_as_runs() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -9,14 +9,20 @@ use std::time::Duration;
|
||||
pub fn configs(host: &Host, window: &str) -> IdeResult<Vec<RunConfig>> {
|
||||
host.require_trust(window)?;
|
||||
let project = host.project(window)?;
|
||||
let root = project.root.clone();
|
||||
let user = ide_run::load_user_configs(&root)?;
|
||||
let root_suppressed = ide_run::load_suppressed_config_ids(&root)?;
|
||||
let settings = project.settings.effective();
|
||||
let mut all = detected_and_saved(&root, &settings, &user, &root_suppressed);
|
||||
if let Some(workspace) = ide_workspace::workspace::Workspace::load(&root)? {
|
||||
project_configs(&project.root, &project.settings.effective(), &host.config_dir)
|
||||
}
|
||||
|
||||
pub(super) fn project_configs(
|
||||
root: &std::path::Path,
|
||||
settings: &ide_core::settings::Settings,
|
||||
config_dir: &std::path::Path,
|
||||
) -> IdeResult<Vec<RunConfig>> {
|
||||
let user = ide_run::load_user_configs(root)?;
|
||||
let root_suppressed = ide_run::load_suppressed_config_ids(root)?;
|
||||
let mut all = detected_and_saved(root, settings, &user, &root_suppressed);
|
||||
if let Some(workspace) = ide_workspace::workspace::Workspace::load(root)? {
|
||||
for member in workspace.projects.into_iter().filter(|p| p.is_dir()) {
|
||||
let settings = ide_workspace::SettingsStore::load(&host.config_dir, Some(&member)).effective();
|
||||
let settings = ide_workspace::SettingsStore::load(config_dir, Some(&member)).effective();
|
||||
let saved = ide_run::load_user_configs(&member)?;
|
||||
let suppressed = ide_run::load_suppressed_config_ids(&member)?;
|
||||
let configs = detected_and_saved(&member, &settings, &saved, &suppressed);
|
||||
@@ -24,7 +30,7 @@ pub fn configs(host: &Host, window: &str) -> IdeResult<Vec<RunConfig>> {
|
||||
let name = member.file_name().unwrap_or_default().to_string_lossy();
|
||||
for mut config in configs {
|
||||
for task in &mut config.extras.before_launch {
|
||||
if task.kind == "run" {
|
||||
if matches!(task.kind.as_str(), "run" | "build") && !task.target.is_empty() {
|
||||
task.target = format!("workspace:{}:{}", key, task.target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(crate) fn detect_with_profiles(
|
||||
config.id = scope.id(&config.id);
|
||||
config.name = scope.name(&config.name);
|
||||
for task in &mut config.extras.before_launch {
|
||||
if task.kind == "run" {
|
||||
if matches!(task.kind.as_str(), "run" | "build") && !task.target.is_empty() {
|
||||
task.target = scope.id(&task.target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,57 @@ fn reply_file(reply: &Path, name: &str) -> Option<Value> {
|
||||
}
|
||||
|
||||
fn before(config: &mut RunConfig, id: &str) {
|
||||
config.extras.before_launch.push(BeforeLaunchTask { kind: "run".into(), target: id.into() });
|
||||
config.extras.before_launch.push(BeforeLaunchTask { kind: "build".into(), target: id.into() });
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Names {
|
||||
contexts: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Names {
|
||||
fn push(&mut self, out: &mut Vec<RunConfig>, config: RunConfig, dir: &str, profile: &str) {
|
||||
self.contexts.push((dir.replace('\\', "/"), profile.into()));
|
||||
out.push(config);
|
||||
}
|
||||
|
||||
fn apply(self, configs: &mut [RunConfig]) {
|
||||
let names: Vec<_> = configs.iter().map(|c| c.name.clone()).collect();
|
||||
for (index, config) in configs.iter_mut().enumerate() {
|
||||
let duplicates: Vec<_> = names.iter().enumerate().filter(|(_, name)| **name == names[index]).collect();
|
||||
if duplicates.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let (dir, profile) = &self.contexts[index];
|
||||
let needs_dir = duplicates.iter().any(|(other, _)| *other != index && self.contexts[*other].1 == *profile);
|
||||
let mut context = Vec::new();
|
||||
if !profile.is_empty() {
|
||||
context.push(profile.clone());
|
||||
}
|
||||
if needs_dir {
|
||||
context.push(format!("{}/", dir.trim_end_matches('/')));
|
||||
}
|
||||
if !context.is_empty() {
|
||||
config.name = format!("{} [{}]", config.name, context.join(" · "));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn detect(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut Vec<RunConfig>) {
|
||||
let start = out.len();
|
||||
let mut names = Names::default();
|
||||
detect_configs(root, profiles, out, &mut names);
|
||||
names.apply(&mut out[start..]);
|
||||
}
|
||||
|
||||
fn detect_configs(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut Vec<RunConfig>, names: &mut Names) {
|
||||
if !root.join("CMakeLists.txt").is_file() {
|
||||
return;
|
||||
}
|
||||
if let Some(profiles) = profiles {
|
||||
for (index, profile) in profiles.iter().enumerate() {
|
||||
detect_profile(root, profile, index, profiles.len() > 1, out);
|
||||
detect_profile(root, profile, index, out, names);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -62,35 +103,30 @@ pub(super) fn detect(root: &Path, profiles: Option<&[CMakeProfile]>, out: &mut V
|
||||
if !build.join("CMakeCache.txt").is_file() {
|
||||
args.push("-DCMAKE_BUILD_TYPE=Debug");
|
||||
}
|
||||
let mut configure = config(&configure_id, format!("CMake Configure ({dir})"), "cmake", "cmake", &args);
|
||||
let mut configure = config(&configure_id, "CMake Configure", "cmake", "cmake", &args);
|
||||
configure.extras.params.insert("buildDir".into(), dir.clone());
|
||||
out.push(configure);
|
||||
let mut all = config(
|
||||
&build_id,
|
||||
format!("CMake Build ({dir})"),
|
||||
"cmake",
|
||||
"cmake",
|
||||
&["--build", &dir, "--config", &configuration],
|
||||
);
|
||||
names.push(out, configure, &dir, "");
|
||||
let mut all =
|
||||
config(&build_id, "CMake Build", "cmake", "cmake", &["--build", &dir, "--config", &configuration]);
|
||||
// `cmake --build` regenerates by itself once a build directory is configured.
|
||||
if !build.join("CMakeCache.txt").is_file() {
|
||||
before(&mut all, &configure_id);
|
||||
}
|
||||
out.push(all);
|
||||
names.push(out, all, &dir, "");
|
||||
let mut test = config(
|
||||
format!("{prefix}:test"),
|
||||
format!("CTest ({dir})"),
|
||||
"CTest",
|
||||
"cmake",
|
||||
"ctest",
|
||||
&["--test-dir", &dir, "-C", &configuration, "--output-on-failure"],
|
||||
);
|
||||
before(&mut test, &build_id);
|
||||
out.push(test);
|
||||
targets(root, &build, &dir, &prefix, None, out);
|
||||
names.push(out, test, &dir, "");
|
||||
targets(root, &build, &dir, &prefix, None, out, names);
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, multiple: bool, out: &mut Vec<RunConfig>) {
|
||||
fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, out: &mut Vec<RunConfig>, names: &mut Names) {
|
||||
let dir = if profile.build_dir.trim().is_empty() {
|
||||
format!("cmake-build-{}", profile.build_type.to_lowercase())
|
||||
} else {
|
||||
@@ -101,8 +137,7 @@ fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, multiple: b
|
||||
let configure_id = format!("{prefix}:configure");
|
||||
let build_id = format!("{prefix}:build");
|
||||
let start = out.len();
|
||||
let mut configure =
|
||||
config(&configure_id, format!("CMake Configure ({dir})"), "cmake", "cmake", &["-S", ".", "-B", &dir]);
|
||||
let mut configure = config(&configure_id, "CMake Configure", "cmake", "cmake", &["-S", ".", "-B", &dir]);
|
||||
if !profile.generator.trim().is_empty() {
|
||||
configure.args.extend(["-G".into(), profile.generator.clone()]);
|
||||
}
|
||||
@@ -110,34 +145,26 @@ fn detect_profile(root: &Path, profile: &CMakeProfile, index: usize, multiple: b
|
||||
configure.args.push("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON".into());
|
||||
configure.args.extend_from_slice(&profile.cmake_args);
|
||||
configure.extras.params.insert("buildDir".into(), dir.clone());
|
||||
out.push(configure);
|
||||
let mut all = config(
|
||||
&build_id,
|
||||
format!("CMake Build ({dir})"),
|
||||
"cmake",
|
||||
"cmake",
|
||||
&["--build", &dir, "--config", &profile.build_type],
|
||||
);
|
||||
names.push(out, configure, &dir, &profile.name);
|
||||
let mut all =
|
||||
config(&build_id, "CMake Build", "cmake", "cmake", &["--build", &dir, "--config", &profile.build_type]);
|
||||
all.args.extend_from_slice(&profile.build_args);
|
||||
if !build.join("CMakeCache.txt").is_file() {
|
||||
before(&mut all, &configure_id);
|
||||
}
|
||||
out.push(all);
|
||||
names.push(out, all, &dir, &profile.name);
|
||||
let mut test = config(
|
||||
format!("{prefix}:test"),
|
||||
format!("CTest ({dir})"),
|
||||
"CTest",
|
||||
"cmake",
|
||||
"ctest",
|
||||
&["--test-dir", &dir, "-C", &profile.build_type, "--output-on-failure"],
|
||||
);
|
||||
before(&mut test, &build_id);
|
||||
out.push(test);
|
||||
targets(root, &build, &dir, &prefix, Some(profile), out);
|
||||
names.push(out, test, &dir, &profile.name);
|
||||
targets(root, &build, &dir, &prefix, Some(profile), out, names);
|
||||
for config in &mut out[start..] {
|
||||
config.env = profile.env.clone();
|
||||
if multiple {
|
||||
config.name = format!("{} [{}]", config.name, profile.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +175,7 @@ fn targets(
|
||||
prefix: &str,
|
||||
profile: Option<&CMakeProfile>,
|
||||
out: &mut Vec<RunConfig>,
|
||||
names: &mut Names,
|
||||
) {
|
||||
let reply = build.join(".cmake/api/v1/reply");
|
||||
let Some(index_path) = fs::read_dir(&reply).ok().and_then(|entries| {
|
||||
@@ -198,8 +226,7 @@ fn targets(
|
||||
if !name.is_empty() {
|
||||
args.extend(["--config", name]);
|
||||
}
|
||||
let mut build_config =
|
||||
config(&target_build_id, format!("Build {target_name} ({dir} {name})"), "cmake", "cmake", &args);
|
||||
let mut build_config = config(&target_build_id, format!("Build {target_name}"), "cmake", "cmake", &args);
|
||||
if let Some(profile) = profile {
|
||||
if name.is_empty() {
|
||||
build_config.args.extend(["--config".into(), profile.build_type.clone()]);
|
||||
@@ -209,11 +236,11 @@ fn targets(
|
||||
before(&mut build_config, &format!("{prefix}:configure"));
|
||||
}
|
||||
}
|
||||
out.push(build_config);
|
||||
let label = profile.map_or(name, |p| p.name.as_str());
|
||||
names.push(out, build_config, dir, label);
|
||||
let binary = build.join(artifact);
|
||||
let mut run =
|
||||
config(&id, format!("{target_name} ({dir} {name})"), "native", &binary.to_string_lossy(), &[])
|
||||
.debug("lldb-dap");
|
||||
config(&id, target_name, "cmake-application", &binary.to_string_lossy(), &[]).debug("lldb-dap");
|
||||
before(&mut run, &target_build_id);
|
||||
let sources: Vec<String> = target["sources"]
|
||||
.as_array()
|
||||
@@ -231,13 +258,50 @@ fn targets(
|
||||
.params
|
||||
.insert("cmakeCanonicalSourceRoot".into(), source_root.to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
out.push(run);
|
||||
names.push(out, run, dir, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the codemodel only when the user runs a configure configuration.
|
||||
pub(crate) fn prepare(config: &RunConfig, cwd: &Path) -> crate::IdeResult<()> {
|
||||
/// Ordered configure/build commands for a CMake build configuration. Executables and CTest
|
||||
/// are never executed by Build; only their explicit build prerequisites are accepted.
|
||||
pub fn build_configs(configs: &[RunConfig], id: &str) -> crate::IdeResult<Vec<RunConfig>> {
|
||||
fn visit(
|
||||
configs: &[RunConfig],
|
||||
id: &str,
|
||||
chain: &mut Vec<String>,
|
||||
out: &mut Vec<RunConfig>,
|
||||
) -> crate::IdeResult<()> {
|
||||
if chain.iter().any(|entry| entry == id) {
|
||||
return Err(ide_core::IdeError::invalid("The before launch tasks run in a cycle"));
|
||||
}
|
||||
let config = configs
|
||||
.iter()
|
||||
.find(|c| c.id == id)
|
||||
.ok_or_else(|| ide_core::IdeError::not_found(format!("Configuration '{id}' no longer exists")))?;
|
||||
if config.kind != "cmake" || !config.args.first().is_some_and(|a| a == "--build" || a == "-S") {
|
||||
return Err(ide_core::IdeError::invalid("Not a CMake configure or build configuration"));
|
||||
}
|
||||
chain.push(id.into());
|
||||
for task in &config.extras.before_launch {
|
||||
if !matches!(task.kind.as_str(), "run" | "build") || task.target.is_empty() {
|
||||
return Err(ide_core::IdeError::invalid("CMake builds require configure/build prerequisites"));
|
||||
}
|
||||
visit(configs, &task.target, chain, out)?;
|
||||
}
|
||||
chain.pop();
|
||||
if !out.iter().any(|c| c.id == id) {
|
||||
out.push(config.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
visit(configs, id, &mut Vec::new(), &mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Request the codemodel when executing a configure configuration.
|
||||
pub fn prepare(config: &RunConfig, cwd: &Path) -> crate::IdeResult<()> {
|
||||
if config.kind == "cmake"
|
||||
&& config.args.first().is_some_and(|a| a == "-S")
|
||||
&& let Some(dir) = config.extras.params.get("buildDir")
|
||||
@@ -300,10 +364,10 @@ mod tests {
|
||||
};
|
||||
let mut configs = Vec::new();
|
||||
detect(root.path(), Some(std::slice::from_ref(&profile)), &mut configs);
|
||||
let run = configs.iter().find(|c| c.kind == "native").unwrap();
|
||||
let run = configs.iter().find(|c| c.kind == "cmake-application").unwrap();
|
||||
assert!(run.command.ends_with("release/app"));
|
||||
assert_eq!(run.env, profile.env);
|
||||
assert_eq!(configs.iter().filter(|c| c.kind == "native").count(), 1);
|
||||
assert_eq!(configs.iter().filter(|c| c.kind == "cmake-application").count(), 1);
|
||||
let build = configs.iter().find(|c| c.id == run.extras.before_launch[0].target).unwrap();
|
||||
assert_eq!(
|
||||
build.args,
|
||||
@@ -320,6 +384,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_dependencies_are_scoped_and_reject_cycles_or_executables() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let nested = root.path().join("nested");
|
||||
fs::create_dir_all(&nested).unwrap();
|
||||
fs::write(nested.join("CMakeLists.txt"), "project(app)").unwrap();
|
||||
let configs = crate::detect(root.path());
|
||||
let build = configs.iter().find(|c| c.args.first().is_some_and(|a| a == "--build")).unwrap();
|
||||
assert_eq!(build.extras.before_launch[0].kind, "build");
|
||||
let ordered = build_configs(&configs, &build.id).unwrap();
|
||||
assert_eq!(ordered.len(), 2);
|
||||
assert_eq!(ordered[0].args[0], "-S");
|
||||
assert_eq!(ordered[1].args[0], "--build");
|
||||
assert_eq!(ordered[1].cwd, build.cwd);
|
||||
assert!(build_configs(&configs, "missing").is_err());
|
||||
let mut cycle = configs.clone();
|
||||
let configure = cycle.iter_mut().find(|c| c.id == ordered[0].id).unwrap();
|
||||
before(configure, &build.id);
|
||||
assert!(build_configs(&cycle, &build.id).is_err());
|
||||
for kind in ["native", "cmake-application", "shell"] {
|
||||
let mut invalid = build.clone();
|
||||
invalid.kind = kind.into();
|
||||
assert!(build_configs(&[invalid.clone()], &invalid.id).is_err());
|
||||
}
|
||||
let test = configs.iter().find(|c| c.command == "ctest").unwrap();
|
||||
assert!(build_configs(&configs, &test.id).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generator_change_clears_only_cmake_state_at_launch() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -20,6 +20,7 @@ mod utf8;
|
||||
pub mod which;
|
||||
pub mod wrap;
|
||||
|
||||
pub use detect::cmake::{build_configs as cmake_build_configs, prepare as prepare_cmake};
|
||||
pub use process::{ProcessManager, StartOptions};
|
||||
|
||||
use ide_core::{IdeResult, RunId};
|
||||
@@ -59,7 +60,8 @@ pub struct RunConfig {
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BeforeLaunchTask {
|
||||
/// `run` (another configuration, `target` = its id) | `build` | `tool` (external tool id) |
|
||||
/// `run` (another configuration id) | `build` (project, or `target` = CMake build id) |
|
||||
/// `tool` (external tool id) |
|
||||
/// `command` (a shell command line) | `toolWindow` (tool window id).
|
||||
pub kind: String,
|
||||
#[serde(default)]
|
||||
|
||||
@@ -676,6 +676,130 @@ fn cmake_detection_is_read_only_and_preserves_existing_build_directories() {
|
||||
assert_eq!(test.extras.before_launch[0].target, build.id);
|
||||
}
|
||||
|
||||
fn cmake_reply(root: &Path, dir: &str, configurations: &[(&str, &[&str])]) {
|
||||
let reply = root.join(dir).join(".cmake/api/v1/reply");
|
||||
fs::create_dir_all(&reply).unwrap();
|
||||
fs::write(root.join(dir).join("CMakeCache.txt"), "").unwrap();
|
||||
let write = |name: &str, value: serde_json::Value| fs::write(reply.join(name), value.to_string()).unwrap();
|
||||
write(
|
||||
"index-1.json",
|
||||
serde_json::json!({
|
||||
"objects": [{"kind": "codemodel", "version": {"major": 2}, "jsonFile": "model.json"}]
|
||||
}),
|
||||
);
|
||||
let configurations: Vec<_> = configurations
|
||||
.iter()
|
||||
.map(|(configuration, targets)| {
|
||||
let targets: Vec<_> = targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let file = format!("{configuration}-{target}.json");
|
||||
write(
|
||||
&file,
|
||||
serde_json::json!({
|
||||
"name": target, "type": "EXECUTABLE", "artifacts": [{"path": target}]
|
||||
}),
|
||||
);
|
||||
serde_json::json!({"jsonFile": file})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({"name": configuration, "targets": targets})
|
||||
})
|
||||
.collect();
|
||||
write("model.json", serde_json::json!({"paths": {"source": root}, "configurations": configurations}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_names_only_include_context_that_distinguishes_duplicates() {
|
||||
for configuration in ["", "Debug"] {
|
||||
let root = project(&[("CMakeLists.txt", "project(demo)")]);
|
||||
cmake_reply(root.path(), "build", &[(configuration, &["hello", "other"])]);
|
||||
let single = detect(root.path());
|
||||
let expected = ["CMake Configure", "CMake Build", "CTest", "Build hello", "hello", "Build other", "other"];
|
||||
assert_eq!(single.iter().map(|c| c.name.as_str()).collect::<Vec<_>>(), expected);
|
||||
assert_eq!(find(&single, &format!("cmake:build:{configuration}:hello")).name, "hello");
|
||||
|
||||
cmake_reply(root.path(), "build-debug", &[(configuration, &["hello", "unique"])]);
|
||||
let multiple = detect(root.path());
|
||||
let context = if configuration.is_empty() { "build/" } else { "Debug · build/" };
|
||||
assert_eq!(find(&multiple, &format!("cmake:build:{configuration}:hello")).name, format!("hello [{context}]"));
|
||||
assert_eq!(
|
||||
find(&multiple, &format!("cmake:build:{configuration}:hello:build")).name,
|
||||
format!("Build hello [{context}]")
|
||||
);
|
||||
let context = if configuration.is_empty() { "build-debug/" } else { "Debug · build-debug/" };
|
||||
assert_eq!(
|
||||
find(&multiple, &format!("cmake:build-debug:{configuration}:hello")).name,
|
||||
format!("hello [{context}]")
|
||||
);
|
||||
for (id, name) in [("configure", "CMake Configure"), ("build", "CMake Build"), ("test", "CTest")] {
|
||||
for dir in ["build", "build-debug"] {
|
||||
assert_eq!(find(&multiple, &format!("cmake:{dir}:{id}")).name, format!("{name} [{dir}/]"));
|
||||
}
|
||||
}
|
||||
assert_eq!(find(&multiple, &format!("cmake:build:{configuration}:other")).name, "other");
|
||||
assert_eq!(find(&multiple, &format!("cmake:build-debug:{configuration}:unique")).name, "unique");
|
||||
for mut config in single {
|
||||
let renamed = find(&multiple, &config.id);
|
||||
config.name = renamed.name.clone();
|
||||
assert_eq!(&config, renamed, "only names may change when another directory appears");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_multiconfig_names_use_configuration_without_redundant_directory() {
|
||||
let root = project(&[("CMakeLists.txt", "project(demo)")]);
|
||||
cmake_reply(root.path(), "build", &[("Debug", &["hello"]), ("Release", &["hello"])]);
|
||||
let configs = detect(root.path());
|
||||
for configuration in ["Debug", "Release"] {
|
||||
assert_eq!(
|
||||
find(&configs, &format!("cmake:build:{configuration}:hello")).name,
|
||||
format!("hello [{configuration}]")
|
||||
);
|
||||
assert_eq!(
|
||||
find(&configs, &format!("cmake:build:{configuration}:hello:build")).name,
|
||||
format!("Build hello [{configuration}]")
|
||||
);
|
||||
}
|
||||
assert_eq!(find(&configs, "cmake:build:build").name, "CMake Build");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_profile_names_disambiguate_before_adding_directory_locations() {
|
||||
use ide_core::settings::CMakeProfile;
|
||||
let root = project(&[("CMakeLists.txt", "project(demo)")]);
|
||||
cmake_reply(root.path(), "out/debug", &[("Debug", &["hello"])]);
|
||||
cmake_reply(root.path(), "out/release", &[("Release", &["hello"])]);
|
||||
for same_name in [false, true] {
|
||||
let profiles = [
|
||||
CMakeProfile { name: "Development".into(), build_dir: "out/debug".into(), ..Default::default() },
|
||||
CMakeProfile {
|
||||
name: if same_name { "Development" } else { "Production" }.into(),
|
||||
build_type: "Release".into(),
|
||||
build_dir: "out/release".into(),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let configs = ide_run::detect_with_profiles(root.path(), &[], Some(&profiles));
|
||||
for (index, profile) in profiles.iter().enumerate() {
|
||||
let context =
|
||||
if same_name { format!("{} · {}/", profile.name, profile.build_dir) } else { profile.name.clone() };
|
||||
for (id, name) in [("configure", "CMake Configure"), ("build", "CMake Build"), ("test", "CTest")] {
|
||||
assert_eq!(find(&configs, &format!("cmake:profile:{index}:{id}")).name, format!("{name} [{context}]"));
|
||||
}
|
||||
assert_eq!(
|
||||
find(&configs, &format!("cmake:profile:{index}:{}:hello", profile.build_type)).name,
|
||||
format!("hello [{context}]")
|
||||
);
|
||||
assert_eq!(
|
||||
find(&configs, &format!("cmake:profile:{index}:{}:hello:build", profile.build_type)).name,
|
||||
format!("Build hello [{context}]")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmake_configures_before_the_first_build() {
|
||||
let dir = project(&[("CMakeLists.txt", "project(demo)")]);
|
||||
@@ -693,7 +817,7 @@ fn cmake_ignores_malformed_file_api_replies() {
|
||||
]);
|
||||
let configs = detect(dir.path());
|
||||
assert_eq!(configs.iter().filter(|c| c.kind == "cmake").count(), 3);
|
||||
assert!(!configs.iter().any(|c| c.kind == "native"));
|
||||
assert!(!configs.iter().any(|c| c.kind == "cmake-application"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -789,7 +913,7 @@ fn cmake_profiles_control_commands_names_and_environment() {
|
||||
"",
|
||||
]);
|
||||
assert_eq!(configure.args, expected);
|
||||
assert_eq!(configure.name, "CMake Configure (output dir)");
|
||||
assert_eq!(configure.name, "CMake Configure");
|
||||
let build = find(&configs, "cmake:profile:0:build");
|
||||
assert_eq!(build.args, ["--build", "output dir", "--config", "Release", "-j", "8", "--", "VERBOSE=1"]);
|
||||
assert_eq!(build.extras.before_launch[0].target, configure.id);
|
||||
|
||||
@@ -572,7 +572,7 @@ fn cmake_configure_discovers_builds_and_runs_a_multifile_target() {
|
||||
assert!(h.root.path().join("build/.cmake/api/v1/query/client-openidea/codemodel-v2").exists());
|
||||
let root = h.root.path().canonicalize().unwrap();
|
||||
let configs = ide_run::detect(&root);
|
||||
let target = configs.iter().find(|c| c.kind == "native").expect("File API executable");
|
||||
let target = configs.iter().find(|c| c.kind == "cmake-application").expect("File API executable");
|
||||
assert_eq!(target.debug_adapter.as_deref(), Some("lldb-dap"));
|
||||
let sources = &target.extras.params["cmakeSources"];
|
||||
assert!(sources.lines().any(|s| s.ends_with("/main.cpp")));
|
||||
|
||||
@@ -143,3 +143,68 @@ test("Go reruns keep each package's exact failures and the original configuratio
|
||||
["test", "-json", "./..."],
|
||||
]);
|
||||
});
|
||||
|
||||
test("CMake builds the selected target in Build before starting only the application in Run", async ({
|
||||
workbench: page,
|
||||
}) => {
|
||||
await stubDesktop(page);
|
||||
await page.evaluate(async () => {
|
||||
const build = {
|
||||
id: "cmake:release:hello:build",
|
||||
name: "Build hello",
|
||||
kind: "cmake",
|
||||
command: "cmake",
|
||||
args: ["--build", "release", "--target", "hello", "--config", "Release"],
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
};
|
||||
const app = {
|
||||
...build,
|
||||
id: "cmake:release:hello",
|
||||
name: "hello [Release, release]",
|
||||
kind: "cmake-application",
|
||||
command: "/dev/acme-api/release/hello",
|
||||
args: [],
|
||||
debugAdapter: "lldb-dap",
|
||||
beforeLaunch: [{ kind: "build", target: build.id }],
|
||||
};
|
||||
const h = window.__ipc.handlers;
|
||||
h.run_configs = () => [build, app];
|
||||
h.run_apply_settings = (args) => args.configs;
|
||||
h.build_run = (args) => {
|
||||
window.__ipc.send(args.onEvent, { kind: "started", system: "cmake", title: "Build hello" });
|
||||
window.__ipc.send(args.onEvent, { kind: "output", text: "COMPILER_OUTPUT\n", stderr: false });
|
||||
return {
|
||||
system: "cmake",
|
||||
title: "Build hello",
|
||||
success: true,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
durationMs: 1,
|
||||
messages: [],
|
||||
};
|
||||
};
|
||||
h.run_start = () => {
|
||||
setTimeout(() => window.__ipc.emit({ type: "runExited", run: 7, code: 0 }), 100);
|
||||
return { id: 7, configId: app.id, name: app.name, pid: 4242, running: true, pty: false };
|
||||
};
|
||||
const runModule = "/src/features/run/store.ts";
|
||||
const { loadConfigs, selectConfig } = await import(/* @vite-ignore */ runModule);
|
||||
await loadConfigs();
|
||||
selectConfig(app.id);
|
||||
});
|
||||
await page.getByRole("button", { name: "Build Project", exact: true }).click();
|
||||
const build = page.getByRole("region", { name: "Build", exact: true });
|
||||
await expect(build.getByText("COMPILER_OUTPUT", { exact: true })).toBeVisible();
|
||||
expect(await ipcCalls(page, "run_start")).toHaveLength(0);
|
||||
expect(await ipcCalls(page, "build_run")).toEqual([expect.objectContaining({ cmake: "cmake:release:hello:build" })]);
|
||||
await page.keyboard.press("Control+r");
|
||||
await expect.poll(async () => (await ipcCalls(page, "run_start")).length).toBe(1);
|
||||
expect(await ipcCalls(page, "build_run")).toHaveLength(2);
|
||||
const run = page.getByRole("region", { name: "Run", exact: true });
|
||||
await expect(run.getByRole("tab", { name: "hello [Release, release]" })).toBeVisible();
|
||||
await expect(run.getByText("COMPILER_OUTPUT", { exact: true })).toHaveCount(0);
|
||||
});
|
||||
@@ -150,11 +150,13 @@ pub async fn maven_exclude_dependency(
|
||||
/// are cleared first; the new messages are published as `build` diagnostics.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn build_run(
|
||||
window: Window,
|
||||
state: State<'_, AppState>,
|
||||
kind: BuildKind,
|
||||
file: Option<std::path::PathBuf>,
|
||||
cmake: Option<String>,
|
||||
request: RequestId,
|
||||
clear: Vec<std::path::PathBuf>,
|
||||
on_event: Channel<BuildEvent>,
|
||||
@@ -164,6 +166,7 @@ pub async fn build_run(
|
||||
window.label(),
|
||||
kind,
|
||||
file,
|
||||
cmake,
|
||||
request,
|
||||
clear,
|
||||
crate::commands::sender(on_event),
|
||||
|
||||
@@ -761,7 +761,7 @@ export const commands = {
|
||||
* with `cancel_request(request)`. Diagnostics of `clear` (the files of the previous build)
|
||||
* are cleared first; the new messages are published as `build` diagnostics.
|
||||
*/
|
||||
buildRun: (kind: BuildKind, file: string | null, request: RequestId, clear: string[], onEvent: Channel<BuildEvent>) => typedError<BuildResult, IdeError>(__TAURI_INVOKE("build_run", { kind, file, request, clear, onEvent })),
|
||||
buildRun: (kind: BuildKind, file: string | null, cmake: string | null, request: RequestId, clear: string[], onEvent: Channel<BuildEvent>) => typedError<BuildResult, IdeError>(__TAURI_INVOKE("build_run", { kind, file, cmake, request, clear, onEvent })),
|
||||
/**
|
||||
* Cancellable with `cancel_request(request)`.
|
||||
* Builds the workspace (when `build`), then resolves the runtime (or test)
|
||||
@@ -1949,7 +1949,8 @@ export type AutoUpload = "never" |
|
||||
/** A step run before a configuration launches; a failing step aborts the launch. */
|
||||
export type BeforeLaunchTask = {
|
||||
/**
|
||||
* `run` (another configuration, `target` = its id) | `build` | `tool` (external tool id) |
|
||||
* `run` (another configuration id) | `build` (project, or `target` = CMake build id) |
|
||||
* `tool` (external tool id) |
|
||||
* `command` (a shell command line) | `toolWindow` (tool window id).
|
||||
*/
|
||||
kind: string,
|
||||
|
||||
@@ -8,10 +8,12 @@ import styles from "./Build.module.css";
|
||||
/** Run configuration kinds a project build precedes; scripts (Python, shell, Node…) have nothing to compile. */
|
||||
const COMPILED_KINDS = new Set(["", "spring-boot", "application", "maven", "gradle"]);
|
||||
|
||||
/** IDEA's main toolbar hammer, left of the run widget: Maven / Gradle projects, unless a script configuration is selected. */
|
||||
/** Main toolbar hammer, left of the run widget, for compiled project configurations. */
|
||||
export function BuildTitleItem() {
|
||||
const shown = useContextKeys(
|
||||
(c) => (!!c["build.maven"] || !!c["build.gradle"]) && COMPILED_KINDS.has(String(c["run.selectedKind"] ?? "")),
|
||||
(c) =>
|
||||
!!c["run.cmakeBuild"] ||
|
||||
((!!c["build.maven"] || !!c["build.gradle"]) && COMPILED_KINDS.has(String(c["run.selectedKind"] ?? ""))),
|
||||
);
|
||||
const running = useContextKeys((c) => !!c[BUILD_RUNNING]);
|
||||
if (!shown) return null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
type ToolWindowService,
|
||||
} from "@/workbench/contributions";
|
||||
import { useTasks } from "@/workbench/tasks";
|
||||
import { BuildTitleItem } from "./BuildTitleItem";
|
||||
import { BuildView } from "./BuildView";
|
||||
import { activate } from "./index";
|
||||
import {
|
||||
@@ -86,13 +87,20 @@ describe("build model", () => {
|
||||
type Note = Parameters<NotificationService["notify"]>[0];
|
||||
|
||||
class FakeBackend implements BuildBackend {
|
||||
calls: Array<{ kind: string; file: string | null; clear: string[] }> = [];
|
||||
calls: Array<{ kind: string; file: string | null; clear: string[]; cmake?: string }> = [];
|
||||
cancelled: number[] = [];
|
||||
private finish?: (r: BuildResult) => void;
|
||||
private fail?: (e: unknown) => void;
|
||||
emit?: (e: BuildEvent) => void;
|
||||
run(kind: string, file: string | null, _request: number, clear: string[], onEvent: (e: BuildEvent) => void) {
|
||||
this.calls.push({ kind, file, clear });
|
||||
run(
|
||||
kind: string,
|
||||
file: string | null,
|
||||
_request: number,
|
||||
clear: string[],
|
||||
onEvent: (e: BuildEvent) => void,
|
||||
cmake: string | null,
|
||||
) {
|
||||
this.calls.push({ kind, file, clear, ...(cmake ? { cmake } : {}) });
|
||||
this.emit = onEvent;
|
||||
return new Promise<BuildResult>((resolve, reject) => {
|
||||
this.finish = resolve;
|
||||
@@ -150,6 +158,26 @@ describe("build feature", () => {
|
||||
setBuildBackend(null);
|
||||
useProject.setState({ project: null });
|
||||
setContext("projectOpen", false);
|
||||
setContext("run.cmakeBuild", null);
|
||||
});
|
||||
|
||||
it("shows the CMake hammer and builds the selected target through the shared action", async () => {
|
||||
setContext("run.cmakeBuild", "cmake:release:hello:build");
|
||||
render(<BuildTitleItem />);
|
||||
expect(screen.getByRole("button", { name: "Build Project" })).toBeVisible();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Build Project" }));
|
||||
await act(async () => {});
|
||||
expect(fake.calls.at(-1)).toMatchObject({ cmake: "cmake:release:hello:build" });
|
||||
await act(async () => {
|
||||
fake.resolve(result([], true));
|
||||
});
|
||||
const explicit = executeAction("build.buildProject", { cmake: "cmake:debug:other:build", activate: false });
|
||||
await act(async () => {});
|
||||
expect(fake.calls.at(-1)).toMatchObject({ cmake: "cmake:debug:other:build" });
|
||||
await act(async () => {
|
||||
fake.resolve(result([], true));
|
||||
await explicit;
|
||||
});
|
||||
});
|
||||
|
||||
it("registers the Build menu actions, shortcuts and an on-use bottom tool window", () => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Build menu (Build Project ⌘F9, Build Module ⇧⌘F9, Rebuild Project, Build
|
||||
* Artifacts) and the Build tool window. The backend picks the build system of
|
||||
* the project or module: Maven, Gradle, Cargo, Go or TypeScript.
|
||||
* the project or module: Maven, Gradle, Cargo, Go, TypeScript or CMake.
|
||||
*/
|
||||
import { getContext } from "@/platform/context";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import {
|
||||
@@ -57,11 +58,17 @@ export function activate(): () => void {
|
||||
category: "Build",
|
||||
icon: "build-project",
|
||||
when: WHEN,
|
||||
keywords: ["compile", "make", "mvn", "gradle", "cargo", "tsc", "go build"],
|
||||
keywords: ["compile", "make", "mvn", "gradle", "cargo", "tsc", "go build", "cmake"],
|
||||
// Awaitable: Run's "Build" before launch task waits for the result.
|
||||
run: async ({ arg }) => {
|
||||
const activate = !(typeof arg === "object" && arg !== null && "activate" in arg && arg.activate === false);
|
||||
await startBuild("build", null, activate);
|
||||
const cmake =
|
||||
arg == null
|
||||
? getContext()["run.cmakeBuild"]
|
||||
: typeof arg === "object" && arg !== null && "cmake" in arg
|
||||
? arg.cmake
|
||||
: null;
|
||||
await startBuild("build", null, activate, typeof cmake === "string" ? cmake : null);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -84,7 +91,10 @@ export function activate(): () => void {
|
||||
icon: "build-rebuild",
|
||||
when: WHEN,
|
||||
keywords: ["clean", "compile"],
|
||||
run: () => void startBuild("rebuild"),
|
||||
run: () => {
|
||||
const cmake = getContext()["run.cmakeBuild"];
|
||||
void startBuild("rebuild", null, true, typeof cmake === "string" ? cmake : null);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "build.buildArtifacts",
|
||||
|
||||
@@ -41,15 +41,16 @@ export interface BuildBackend {
|
||||
request: RequestId,
|
||||
clear: string[],
|
||||
onEvent: (e: BuildEvent) => void,
|
||||
cmake: string | null,
|
||||
): Promise<BuildResult>;
|
||||
cancel(request: RequestId): void;
|
||||
}
|
||||
|
||||
const tauriBackend: BuildBackend = {
|
||||
run(kind, file, request, clear, onEvent) {
|
||||
run(kind, file, request, clear, onEvent, cmake) {
|
||||
const channel = new Channel<BuildEvent>();
|
||||
channel.onmessage = onEvent;
|
||||
return call(commands.buildRun(kind, file, request, clear, channel));
|
||||
return call(commands.buildRun(kind, file, cmake, request, clear, channel));
|
||||
},
|
||||
cancel(request) {
|
||||
void commands.cancelRequest(request);
|
||||
@@ -130,6 +131,7 @@ export async function startBuild(
|
||||
kind: BuildKind,
|
||||
file: string | null = null,
|
||||
activate = true,
|
||||
cmake: string | null = null,
|
||||
): Promise<BuildResult | null> {
|
||||
if (isBuilding() || !useProject.getState().project) return null;
|
||||
const request = nextRequestId();
|
||||
@@ -170,7 +172,7 @@ export async function startBuild(
|
||||
|
||||
let result: BuildResult | null = null;
|
||||
try {
|
||||
result = await backend.run(kind, file, request, previous, onEvent);
|
||||
result = await backend.run(kind, file, request, previous, onEvent, cmake);
|
||||
clearTimeout(timer);
|
||||
flush();
|
||||
const status: BuildStatus = result.success ? "success" : "failed";
|
||||
|
||||
@@ -299,19 +299,22 @@ describe("run config → launch config", () => {
|
||||
expect(extra(dap)).toEqual({ env: ["K=v"] });
|
||||
});
|
||||
|
||||
it("maps CMake debug-info paths to the editor's canonical source root", () => {
|
||||
const config = rc({
|
||||
kind: "native",
|
||||
command: "/private/tmp/project/build/app",
|
||||
debugAdapter: "lldb-dap",
|
||||
env: { K: "v" },
|
||||
params: { cmakeSourceRoot: "/tmp/project", cmakeCanonicalSourceRoot: "/private/tmp/project" },
|
||||
});
|
||||
expect(extra(launchConfigFor(config, "/private/tmp/project")!)).toEqual({
|
||||
env: ["K=v"],
|
||||
sourceMap: [["/tmp/project", "/private/tmp/project"]],
|
||||
});
|
||||
});
|
||||
it.each(["native", "cmake-application"])(
|
||||
"maps %s CMake debug-info paths to the editor's canonical source root",
|
||||
(kind) => {
|
||||
const config = rc({
|
||||
kind,
|
||||
command: "/private/tmp/project/build/app",
|
||||
debugAdapter: "lldb-dap",
|
||||
env: { K: "v" },
|
||||
params: { cmakeSourceRoot: "/tmp/project", cmakeCanonicalSourceRoot: "/private/tmp/project" },
|
||||
});
|
||||
expect(extra(launchConfigFor(config, "/private/tmp/project")!)).toEqual({
|
||||
env: ["K=v"],
|
||||
sourceMap: [["/tmp/project", "/private/tmp/project"]],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("Godot: the editor's adapter plays the main scene or the chosen one of the project", () => {
|
||||
const project = rc({
|
||||
|
||||
@@ -263,7 +263,7 @@ function ConfigChooser() {
|
||||
items={rows}
|
||||
getKey={(r) => r.key}
|
||||
isSection={isSection}
|
||||
renderSection={(r) => (r.type === "section" ? r.label : null)}
|
||||
renderSection={(r) => (r.type === "section" ? <T>{r.label}</T> : null)}
|
||||
activeIndex={clamped}
|
||||
onActiveChange={setActive}
|
||||
focusable={false}
|
||||
|
||||
@@ -504,7 +504,9 @@ function EditConfigsDialog() {
|
||||
>
|
||||
<Icon name={collapsed.has(r.key) ? "chevron-right" : "chevron-down"} size={12} />
|
||||
<Icon name={r.icon} size={13} className={styles.chooserIcon} />
|
||||
<span className={clsx(styles.chooserName, styles.editGroupName)}>{r.label}</span>
|
||||
<span className={clsx(styles.chooserName, styles.editGroupName)}>
|
||||
<T>{r.label}</T>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
if (r.type === "folder") {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Before launch tasks (IDEA's "Before launch" list): run in order before a
|
||||
* configuration starts; the first failure aborts the launch.
|
||||
*/
|
||||
import { buildCMake, isCMakeBuild } from "./cmake";
|
||||
import { dotnetBuildConfig } from "./dotnetTypes";
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { getContext } from "@/platform/context";
|
||||
@@ -33,7 +34,7 @@ export function taskLabel(task: BeforeLaunchTask, configs: readonly RunConfig[]
|
||||
case "run":
|
||||
return `Run '${configs.find((c) => c.id === target)?.name ?? target}'`;
|
||||
case "build":
|
||||
return "Build Project";
|
||||
return configs.find((c) => c.id === target)?.name ?? "Build Project";
|
||||
case "dotnetBuild":
|
||||
return "Build .NET project";
|
||||
case "tool":
|
||||
@@ -71,6 +72,7 @@ async function runTask(
|
||||
case "dotnetBuild":
|
||||
return (await runToEnd(dotnetBuildConfig(config), chain, show)) ? null : "Build failed";
|
||||
case "build": {
|
||||
if (target) return buildCMake(target, show === true);
|
||||
if (!getAction(BUILD_ACTION)) return "Building is not available";
|
||||
if (!(await executeAction(BUILD_ACTION, { activate: false }))) return "A build is already running";
|
||||
return getContext()[BUILD_STATUS] === "success" ? null : "Build failed";
|
||||
@@ -79,6 +81,7 @@ async function runTask(
|
||||
if (chain.includes(target)) return "The before launch tasks run in a cycle";
|
||||
const other = useRun.getState().configs.find((c) => c.id === target);
|
||||
if (!other) return `Configuration '${target}' no longer exists`;
|
||||
if (isCMakeBuild(other)) return buildCMake(other.id, show === true);
|
||||
return (await runToEnd(other, chain, show)) ? null : `'${other.name}' failed`;
|
||||
}
|
||||
case "tool": {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { provideNotificationService } from "@/workbench/contributions";
|
||||
import { validateConfig } from "./configs";
|
||||
import { cmakeBuildId, isCMakeBuild } from "./cmake";
|
||||
import { runBeforeLaunch, taskLabel } from "./beforeLaunch";
|
||||
import { resetRuns, startRun, useRun } from "./store";
|
||||
|
||||
const config = (id: string, kind: string, args: string[] = []): RunConfig => ({
|
||||
id,
|
||||
kind,
|
||||
args,
|
||||
name: id,
|
||||
command: "cmake",
|
||||
cwd: null,
|
||||
env: {},
|
||||
detected: true,
|
||||
isTest: false,
|
||||
debugAdapter: null,
|
||||
});
|
||||
const configure = config("cmake:build:configure", "cmake", ["-S", ".", "-B", "build"]);
|
||||
const build = config("cmake:build:build", "cmake", ["--build", "build"]);
|
||||
const target = config("cmake:release:hello:build", "cmake", [
|
||||
"--build",
|
||||
"release",
|
||||
"--target",
|
||||
"hello",
|
||||
"--config",
|
||||
"Release",
|
||||
]);
|
||||
const app = { ...config("hello", "cmake-application"), beforeLaunch: [{ kind: "build", target: target.id }] };
|
||||
const configs = [configure, build, target, app];
|
||||
afterEach(() => resetRuns());
|
||||
|
||||
it("selects the target build, configure's build directory, and CTest's prerequisite", () => {
|
||||
expect(cmakeBuildId(configure, configs)).toBe(build.id);
|
||||
expect(cmakeBuildId(target, configs)).toBe(target.id);
|
||||
expect(cmakeBuildId(app, configs)).toBe(target.id);
|
||||
expect(
|
||||
cmakeBuildId(
|
||||
{ ...config("ctest", "cmake", ["--test-dir", "build"]), beforeLaunch: [{ kind: "build", target: build.id }] },
|
||||
configs,
|
||||
),
|
||||
).toBe(build.id);
|
||||
expect(cmakeBuildId(config("script", "shell"), configs)).toBeNull();
|
||||
expect(cmakeBuildId(undefined, configs)).toBeNull();
|
||||
expect(isCMakeBuild(config("test", "cmake", ["--test-dir", "build"]))).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["success", "failed", "cancelled"])(
|
||||
"awaits a %s target build before launching, including saved run prerequisites",
|
||||
async (status) => {
|
||||
const notify = vi.fn();
|
||||
provideNotificationService({ notify });
|
||||
useRun.setState({ configs });
|
||||
let finish!: () => void;
|
||||
const pending = new Promise<void>((resolve) => {
|
||||
finish = resolve;
|
||||
});
|
||||
const run = vi.fn(async () => {
|
||||
await pending;
|
||||
setContext("build.lastStatus", status);
|
||||
});
|
||||
const dispose = registerActions([{ id: "build.buildProject", title: "Build Project", run }]);
|
||||
try {
|
||||
const launch = runBeforeLaunch(app, [app.id]);
|
||||
expect(run).toHaveBeenCalledWith(expect.objectContaining({ arg: { cmake: target.id, activate: false } }));
|
||||
finish();
|
||||
expect(await launch).toBe(status === "success");
|
||||
const legacy = { ...app, beforeLaunch: [{ kind: "run", target: target.id }] };
|
||||
expect(await runBeforeLaunch(legacy, [app.id])).toBe(status === "success");
|
||||
expect(taskLabel(app.beforeLaunch[0]!, configs)).toBe(target.name);
|
||||
expect(useRun.getState().sessions).toHaveLength(0);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("sends explicit configure and build configurations to Build without creating Run tabs", async () => {
|
||||
const run = vi.fn(async () => setContext("build.lastStatus", "success"));
|
||||
const dispose = registerActions([{ id: "build.buildProject", title: "Build Project", run }]);
|
||||
try {
|
||||
for (const c of [configure, build, target]) {
|
||||
expect(await startRun(c)).toBeNull();
|
||||
expect(run).toHaveBeenLastCalledWith(expect.objectContaining({ arg: { cmake: c.id, activate: true } }));
|
||||
}
|
||||
expect(useRun.getState().sessions).toHaveLength(0);
|
||||
} finally {
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("validates target build prerequisites while allowing a generic project build", () => {
|
||||
expect(validateConfig(app, configs).beforeLaunch).toBeUndefined();
|
||||
expect(validateConfig(app, []).beforeLaunch).toBeDefined();
|
||||
expect(
|
||||
validateConfig({ ...app, beforeLaunch: [{ kind: "build", target: app.id }] }, configs).beforeLaunch,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
validateConfig({ ...app, beforeLaunch: [{ kind: "build", target: "" }] }, configs).beforeLaunch,
|
||||
).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { executeAction, getAction } from "@/platform/actions";
|
||||
import { getContext } from "@/platform/context";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
|
||||
export function isCMakeBuild(config: RunConfig): boolean {
|
||||
return config.kind === "cmake" && ["-S", "--build"].includes(config.args[0] ?? "");
|
||||
}
|
||||
|
||||
/** The selected target's build, including saved configurations and CTest. */
|
||||
export function cmakeBuildId(config: RunConfig | undefined, configs: readonly RunConfig[]): string | null {
|
||||
if (!config) return null;
|
||||
if (isCMakeBuild(config)) {
|
||||
if (config.args[0] === "--build") return config.id;
|
||||
const build = configs.find((c) => c.id !== config.id && c.id === config.id.replace(/:configure$/, ":build"));
|
||||
return build && isCMakeBuild(build) ? build.id : null;
|
||||
}
|
||||
return (
|
||||
config.beforeLaunch
|
||||
?.filter((task) => task.kind === "run" || task.kind === "build")
|
||||
.map((task) => configs.find((c) => c.id === task.target))
|
||||
.find((c) => c && isCMakeBuild(c))?.id ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export async function buildCMake(id: string, activate = false): Promise<string | null> {
|
||||
if (!getAction("build.buildProject")) return "Building is not available";
|
||||
if (!(await executeAction("build.buildProject", { cmake: id, activate }))) return "A build is already running";
|
||||
return getContext()["build.lastStatus"] === "success" ? null : "Build failed";
|
||||
}
|
||||
@@ -45,6 +45,16 @@ describe("config merge / override", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps saved CMake names when generated context changes", () => {
|
||||
const id = "cmake:build:Debug:hello";
|
||||
const saved = cfg({ id, kind: "cmake-application", name: "My hello (build)", detected: false });
|
||||
for (const name of ["hello", "hello [Debug · build/]"]) {
|
||||
const generated = cfg({ id, kind: "cmake-application", name });
|
||||
const merged = mergeConfigs([generated], [saved]);
|
||||
expect(merged).toEqual([saved]);
|
||||
}
|
||||
});
|
||||
|
||||
it("editing a detected config creates a user copy with the same id", () => {
|
||||
const edited = upsertConfig(detected, { ...detected[0]!, args: ["run", "dev", "--port", "3001"] });
|
||||
expect(edited[0]).toMatchObject({ id: "npm:dev", detected: false, args: ["run", "dev", "--port", "3001"] });
|
||||
|
||||
@@ -29,6 +29,7 @@ const KIND_LABELS: Record<string, string> = {
|
||||
kotlinc: "Kotlin Single File",
|
||||
cc: "C/C++ Single File",
|
||||
native: "Native Executable",
|
||||
"cmake-application": "CMake Application",
|
||||
ruby: "Ruby",
|
||||
php: "PHP",
|
||||
perl: "Perl",
|
||||
@@ -85,6 +86,7 @@ export function configIcon(config: Pick<RunConfig, "kind" | "isTest">): string {
|
||||
case "rustc":
|
||||
return "file-rust";
|
||||
case "cc":
|
||||
case "cmake-application":
|
||||
return "file-cpp";
|
||||
case "shell":
|
||||
return "file-shell";
|
||||
@@ -228,7 +230,10 @@ export function validateConfig(config: RunConfig, all: readonly RunConfig[] = []
|
||||
} else if (!config.command.trim() && !useRunConfigTypes.getState().types.get(config.kind)?.debugOnly)
|
||||
errors.command = "Command is required";
|
||||
for (const t of config.beforeLaunch ?? []) {
|
||||
if (t.kind === "run" && (t.target === config.id || !all.some((c) => c.id === t.target)))
|
||||
if (
|
||||
(t.kind === "run" || (t.kind === "build" && t.target)) &&
|
||||
(t.target === config.id || !all.some((c) => c.id === t.target))
|
||||
)
|
||||
errors.beforeLaunch = "A before launch task runs a missing configuration or this one";
|
||||
else if (t.kind !== "build" && t.kind !== "dotnetBuild" && t.kind !== "run" && !(t.target ?? "").trim())
|
||||
errors.beforeLaunch = "A before launch task is incomplete";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { registerEditorContribution } from "@/editor/api";
|
||||
import { executeAction, getAction, registerActions } from "@/platform/actions";
|
||||
import { getContext, setContext } from "@/platform/context";
|
||||
import { getContext, setContext, useContextKeys } from "@/platform/context";
|
||||
import { call, commands, onIdeEvent, type RunConfig } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { onWorkflowSettingsChange } from "@/platform/workflowSettings";
|
||||
@@ -44,6 +44,7 @@ import { RunWidget } from "./RunWidget";
|
||||
import { runConfigsRunAnything } from "./runAnything";
|
||||
import { cleanupRemovedProject, recordRun, runServiceProvider } from "./services";
|
||||
import { liveServiceSession, serviceSessions } from "./serviceSessions";
|
||||
import { cmakeBuildId } from "./cmake";
|
||||
import { runBeforeLaunch } from "./beforeLaunch";
|
||||
import { compoundChildren } from "./configs";
|
||||
import { containerDebuggable, remoteDebugBlocked } from "./remote";
|
||||
@@ -636,7 +637,10 @@ export function activate(): () => void {
|
||||
onIdeEvent("runExited", (e) => onRunExited(e.run, e.code)),
|
||||
// Lets other title items follow the selected configuration (e.g. Build shows only for compiled ones).
|
||||
useRun.subscribe((s) => {
|
||||
setContext("run.selectedKind", s.configs.find((c) => c.id === s.selectedId)?.kind ?? "");
|
||||
const selected = s.configs.find((c) => c.id === s.selectedId);
|
||||
setContext("run.selectedKind", selected?.kind ?? "");
|
||||
const cmake = selected ?? s.configs.find((c) => c.kind === "cmake" && c.args[0] === "--build");
|
||||
setContext("run.cmakeBuild", cmakeBuildId(cmake, s.configs));
|
||||
setContext("run.updatableRunning", updatableSessions().length > 0);
|
||||
}),
|
||||
useProject.subscribe((s, prev) => {
|
||||
@@ -661,6 +665,9 @@ export function activate(): () => void {
|
||||
if (paths.some((p) => p.endsWith("/__init__.py"))) invalidatePythonPackages();
|
||||
if (paths.some((p) => CONFIG_FILES.test(p))) scheduleReload();
|
||||
}),
|
||||
useContextKeys.subscribe((s, prev) => {
|
||||
if (prev["build.running"] && !s["build.running"]) scheduleReload();
|
||||
}),
|
||||
onIdeEvent("runExited", (e) => {
|
||||
if (useRun.getState().sessions.some((s) => s.runId === e.run && s.config.kind === "cmake")) scheduleReload();
|
||||
}),
|
||||
|
||||
@@ -48,6 +48,7 @@ import { TeamcityOutput } from "./testing/teamcity";
|
||||
import type { TestStdout } from "./testing/types";
|
||||
import { recordTestStates } from "./testing/states";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfigs, testsConfigs } from "./testing/rerun";
|
||||
import { buildCMake, isCMakeBuild } from "./cmake";
|
||||
import { compoundChildren } from "./configs";
|
||||
import { remoteLabel, startWithRemedies } from "./remote";
|
||||
import { TerminalOutput } from "./terminalOutput";
|
||||
@@ -710,6 +711,10 @@ export async function startRun(
|
||||
chain?: readonly string[];
|
||||
} = {},
|
||||
): Promise<RunSession | null> {
|
||||
if (!options.launch && isCMakeBuild(config)) {
|
||||
await buildCMake(config.id, options.show !== false);
|
||||
return null;
|
||||
}
|
||||
if (config.kind === "compound") {
|
||||
await startCompound(config, options.chain, options.show);
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"CMake Configure": "CMake Configure",
|
||||
"CMake Build": "CMake Build",
|
||||
"CTest": "CTest",
|
||||
"Build {0}": "Build {0}",
|
||||
"Not a CMake configure or build configuration": "Not a CMake configure or build configuration",
|
||||
"CMake builds require configure/build prerequisites": "CMake builds require configure/build prerequisites",
|
||||
"CMake Application": "CMake Application",
|
||||
"Invalid PTY request": "Invalid PTY request",
|
||||
"Cannot write to PTY": "Cannot write to PTY",
|
||||
"Cannot resize PTY": "Cannot resize PTY",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"CMake Configure": "CMake 配置",
|
||||
"CMake Build": "CMake 构建",
|
||||
"CTest": "CTest",
|
||||
"Build {0}": "构建 {0}",
|
||||
"Not a CMake configure or build configuration": "不是 CMake 配置或构建配置",
|
||||
"CMake builds require configure/build prerequisites": "CMake 构建需要配置或构建前置任务",
|
||||
"CMake Application": "CMake 应用程序",
|
||||
"Invalid PTY request": "无效的 PTY 请求",
|
||||
"Cannot write to PTY": "无法写入 PTY",
|
||||
"Cannot resize PTY": "无法调整 PTY 大小",
|
||||
|
||||
Reference in new issue
Заблокувати користувача