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.
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-08 04:48:14 +08:00
1 джерело 6373bce476
коміт 192b59801a
27 files changed
+670 -90

No files matched your search

+5
Переглянути файл
@@ -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());
+58 -1
Переглянути файл
@@ -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();
+14 -8
Переглянути файл
@@ -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);
}
}
+1 -1
Переглянути файл
@@ -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);
}
}
+135 -43
Переглянути файл
@@ -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();
+3 -1
Переглянути файл
@@ -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)]
+126 -2
Переглянути файл
@@ -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);
+1 -1
Переглянути файл
@@ -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")));
+65
Переглянути файл
@@ -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);
});
+3
Переглянути файл
@@ -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),
+3 -2
Переглянути файл
@@ -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,
+4 -2
Переглянути файл
@@ -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;
+31 -3
Переглянути файл
@@ -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", () => {
+14 -4
Переглянути файл
@@ -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",
+5 -3
Переглянути файл
@@ -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";
+16 -13
Переглянути файл
@@ -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({
+1 -1
Переглянути файл
@@ -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}
+3 -1
Переглянути файл
@@ -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") {
+4 -1
Переглянути файл
@@ -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": {
+105
Переглянути файл
@@ -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();
});
+29
Переглянути файл
@@ -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";
}
+10
Переглянути файл
@@ -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"] });
+6 -1
Переглянути файл
@@ -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";
+9 -2
Переглянути файл
@@ -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();
}),
+5
Переглянути файл
@@ -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;
+7
Переглянути файл
@@ -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",
+7
Переглянути файл
@@ -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 大小",