Skip to content

Commit 30a6acf

Browse files
Support build-before-run for non-active executable targets via targetName arg (#4757)
* feat: support build-before-run for non-active executable targets (#4656) When a targetName is provided via ${input:...} args, resolve the named target directly without calling setLaunchTargetByName(), preventing the active launch target from changing as a side effect. Add build deduplication cache (10s TTL) to prepareLaunchTargetExecutable to avoid duplicate builds when multiple input variables resolve the same target within a single launch.json evaluation. Co-authored-by: hanniavalera <[email protected]> * docs and tests: document ${input:...} pattern and add named target tests - docs/debug-launch.md: add "Debugging a specific target" section with full ${input:...} examples for multi-executable projects - docs/cmake-settings.md: document targetName argument for command substitution commands - CHANGELOG.md: add feature entry for #4656 - test: add tests for named target resolution, active target preservation, buildBeforeRun honor/skip, and invalid target handling Co-authored-by: hanniavalera <[email protected]> * fix: check file existence before using build dedup cache The _prepareCache would return cached results even when the built binary was deleted, preventing buildBeforeRun from triggering a rebuild. Add fs.exists() check so the cache is invalidated when the artifact no longer exists on disk. Co-authored-by: hanniavalera <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: hanniavalera <[email protected]>
1 parent 45842d5 commit 30a6acf

6 files changed

Lines changed: 237 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Features:
1010
- Add "Set Build and Launch/Debug Target" command that sets both the build target and launch target simultaneously. [#4732](https://github.com/microsoft/vscode-cmake-tools/pull/4732)
1111
- Add `cmake.setBuildTargetSameAsLaunchTarget` setting to automatically set the build target when the launch/debug target is changed. [#4519](https://github.com/microsoft/vscode-cmake-tools/pull/4519) [@nikita-karatun](https://github.com/nikita-karatun)
1212
- Add `cmake.additionalBuildProblemMatchers` setting to define custom problem matchers for build output. Supports tools like clang-tidy, PCLint Plus, cppcheck, or custom scripts integrated via `add_custom_command`/`add_custom_target`. [#4077](https://github.com/microsoft/vscode-cmake-tools/issues/4077)
13+
- Support `targetName` argument for launch-target command substitutions (`cmake.launchTargetPath`, etc.) via `${input:...}` variables, enabling build-before-run for non-active executable targets without changing the active launch target. [#4656](https://github.com/microsoft/vscode-cmake-tools/issues/4656)
1314

1415
Improvements:
1516
- Make "CMake: Add ... Preset" commands available in the command palette when `cmake.useCMakePresets` is set to `auto`, even before a CMakePresets.json file exists. [#4401](https://github.com/microsoft/vscode-cmake-tools/issues/4401)

docs/cmake-settings.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,27 @@ Each matcher entry has the following properties:
219219
]
220220
```
221221

222+
#### Resolving a specific target with `${input:...}`
223+
224+
All launch-target commands (`cmake.launchTargetPath`, `cmake.getLaunchTargetPath`, and their directory/filename/name variants) accept an optional `targetName` argument. When `targetName` is provided, the command resolves that specific executable target **without changing the active launch target**. This is useful for projects with multiple executables, allowing stable per-target `launch.json` configurations.
225+
226+
Use VS Code [input variables](https://code.visualstudio.com/docs/editor/variables-reference#_input-variables) to pass arguments:
227+
228+
```jsonc
229+
{
230+
"inputs": [
231+
{
232+
"id": "serverPath",
233+
"type": "command",
234+
"command": "cmake.launchTargetPath",
235+
"args": { "targetName": "my_server" }
236+
}
237+
]
238+
}
239+
```
240+
241+
Then reference it in a launch configuration as `"program": "${input:serverPath}"`. See [Debugging a specific target](debug-launch.md#debugging-a-specific-target-multi-executable-projects) for full examples.
242+
222243
## Next steps
223244

224245
- Learn about [user vs. workspace settings](https://code.visualstudio.com/docs/getstarted/settings)

docs/debug-launch.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,57 @@ Here are minimal examples of a `launch.json` file that uses `cmake.launchTargetP
186186

187187
The value of the `program` attribute is expanded by CMake Tools to be the absolute path of the program to run.
188188

189+
### Debugging a specific target (multi-executable projects)
190+
191+
If your project defines multiple executables (for example, a `client` and a `server`), you can create stable per-target debug configurations using VS Code's [input variables](https://code.visualstudio.com/docs/editor/variables-reference#_input-variables). Pass the `targetName` argument to any launch-target command so that it resolves a specific executable **without changing the active launch target**. If `cmake.buildBeforeRun` is enabled, the named target is built automatically.
192+
193+
```jsonc
194+
{
195+
"version": "0.2.0",
196+
"inputs": [
197+
{
198+
"id": "serverPath",
199+
"type": "command",
200+
"command": "cmake.launchTargetPath",
201+
"args": { "targetName": "my_server" }
202+
},
203+
{
204+
"id": "serverDir",
205+
"type": "command",
206+
"command": "cmake.getLaunchTargetDirectory",
207+
"args": { "targetName": "my_server" }
208+
},
209+
{
210+
"id": "clientPath",
211+
"type": "command",
212+
"command": "cmake.launchTargetPath",
213+
"args": { "targetName": "my_client" }
214+
}
215+
],
216+
"configurations": [
217+
{
218+
"name": "Debug Server",
219+
"type": "cppdbg",
220+
"request": "launch",
221+
"program": "${input:serverPath}",
222+
"cwd": "${input:serverDir}"
223+
},
224+
{
225+
"name": "Debug Client",
226+
"type": "cppdbg",
227+
"request": "launch",
228+
"program": "${input:clientPath}",
229+
"cwd": "${workspaceFolder}"
230+
}
231+
]
232+
}
233+
```
234+
235+
When multiple `${input:...}` variables reference the same target (for example, `serverPath` and `serverDir` above), the build is triggered only once — results are cached for 10 seconds to avoid redundant builds.
236+
237+
> **Tip:**
238+
> For large projects, consider setting `cmake.buildBeforeRun` to `false` and using a `preLaunchTask` instead to keep launch times predictable.
239+
189240
### Cache variable substitution
190241

191242
You can substitute the value of any variable in the CMake cache by adding a `command`-type input for the `cmake.cacheVariable` command to the `inputs` section of `launch.json` with `args.name` as the name of the cache variable. That input can then be used with input variable substitution of values in the `configuration` section of `launch.json`. The optional `args.default` can provide a default value if the named variable isn't found in the CMake cache.

src/cmakeProject.ts

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,14 @@ export class CMakeProject {
758758
}
759759
private readonly _launchTargetName = new Property<string | null>(null);
760760

761+
/**
762+
* Short-lived cache for `prepareLaunchTargetExecutable` results keyed by target name.
763+
* Prevents duplicate builds when multiple `${input:...}` variables resolve the same
764+
* target within a single launch.json evaluation (typically < 1 s apart).
765+
*/
766+
private readonly _prepareCache = new Map<string, { timestamp: number; result: ExecutableTarget }>();
767+
private static readonly PREPARE_CACHE_TTL_MS = 10_000;
768+
761769
/**
762770
* Whether CTest is enabled
763771
*/
@@ -2715,9 +2723,10 @@ export class CMakeProject {
27152723

27162724
/**
27172725
* Implementation of `cmake.launchTargetPath`. This also ensures the target exists if `cmake.buildBeforeRun` is set.
2726+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27182727
*/
2719-
async launchTargetPath(): Promise<string | null> {
2720-
const executable = await this.prepareLaunchTargetExecutable();
2728+
async launchTargetPath(name?: string): Promise<string | null> {
2729+
const executable = await this.prepareLaunchTargetExecutable(name);
27212730
if (!executable) {
27222731
log.showChannel();
27232732
log.warning('=======================================================');
@@ -2732,9 +2741,10 @@ export class CMakeProject {
27322741

27332742
/**
27342743
* Implementation of `cmake.launchTargetDirectory`. This also ensures the target exists if `cmake.buildBeforeRun` is set.
2744+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27352745
*/
2736-
async launchTargetDirectory(): Promise<string | null> {
2737-
const targetPath = await this.launchTargetPath();
2746+
async launchTargetDirectory(name?: string): Promise<string | null> {
2747+
const targetPath = await this.launchTargetPath(name);
27382748
if (targetPath === null) {
27392749
return null;
27402750
}
@@ -2743,9 +2753,10 @@ export class CMakeProject {
27432753

27442754
/**
27452755
* Implementation of `cmake.launchTargetFilename`. This also ensures the target exists if `cmake.buildBeforeRun` is set.
2756+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27462757
*/
2747-
async launchTargetFilename(): Promise<string | null> {
2748-
const targetPath = await this.launchTargetPath();
2758+
async launchTargetFilename(name?: string): Promise<string | null> {
2759+
const targetPath = await this.launchTargetPath(name);
27492760
if (targetPath === null) {
27502761
return null;
27512762
}
@@ -2754,9 +2765,10 @@ export class CMakeProject {
27542765

27552766
/**
27562767
* Implementation of `cmake.launchTargetName`. This also ensures the target exists if `cmake.buildBeforeRun` is set.
2768+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27572769
*/
2758-
async launchTargetNameForSubstitution(): Promise<string | null> {
2759-
const targetPath = await this.launchTargetPath();
2770+
async launchTargetNameForSubstitution(name?: string): Promise<string | null> {
2771+
const targetPath = await this.launchTargetPath(name);
27602772
if (targetPath === null) {
27612773
return null;
27622774
}
@@ -2765,15 +2777,21 @@ export class CMakeProject {
27652777

27662778
/**
27672779
* Implementation of `cmake.getLaunchTargetPath`. This does not ensure the target exists.
2780+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27682781
*/
2769-
async getLaunchTargetPath(): Promise<string | null> {
2782+
async getLaunchTargetPath(name?: string): Promise<string | null> {
27702783
if (await this.needsReconfigure()) {
27712784
const rc = await this.configureInternal(ConfigureTrigger.launch, [], ConfigureType.Normal);
27722785
if (rc.exitCode !== 0) {
27732786
return null;
27742787
}
27752788
}
2776-
const target = await this.getOrSelectLaunchTarget();
2789+
let target: ExecutableTarget | null;
2790+
if (name) {
2791+
target = (await this.executableTargets).find(e => e.name === name) || null;
2792+
} else {
2793+
target = await this.getOrSelectLaunchTarget();
2794+
}
27772795
if (!target) {
27782796
log.showChannel();
27792797
log.warning('=======================================================');
@@ -2789,9 +2807,10 @@ export class CMakeProject {
27892807

27902808
/**
27912809
* Implementation of `cmake.getLaunchTargetDirectory`. This does not ensure the target exists.
2810+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
27922811
*/
2793-
async getLaunchTargetDirectory(): Promise<string | null> {
2794-
const targetPath = await this.getLaunchTargetPath();
2812+
async getLaunchTargetDirectory(name?: string): Promise<string | null> {
2813+
const targetPath = await this.getLaunchTargetPath(name);
27952814
if (targetPath === null) {
27962815
return null;
27972816
}
@@ -2800,9 +2819,10 @@ export class CMakeProject {
28002819

28012820
/**
28022821
* Implementation of `cmake.getLaunchTargetFilename`. This does not ensure the target exists.
2822+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
28032823
*/
2804-
async getLaunchTargetFilename(): Promise<string | null> {
2805-
const targetPath = await this.getLaunchTargetPath();
2824+
async getLaunchTargetFilename(name?: string): Promise<string | null> {
2825+
const targetPath = await this.getLaunchTargetPath(name);
28062826
if (targetPath === null) {
28072827
return null;
28082828
}
@@ -2811,9 +2831,10 @@ export class CMakeProject {
28112831

28122832
/**
28132833
* Implementation of `cmake.getLaunchTargetName`. This does not ensure the target exists.
2834+
* @param name Optional target name. When provided, resolves the named target without changing the active launch target.
28142835
*/
2815-
async getLaunchTargetName(): Promise<string | null> {
2816-
const targetPath = await this.getLaunchTargetPath();
2836+
async getLaunchTargetName(name?: string): Promise<string | null> {
2837+
const targetPath = await this.getLaunchTargetPath(name);
28172838
if (targetPath === null) {
28182839
return null;
28192840
}
@@ -2894,6 +2915,16 @@ export class CMakeProject {
28942915
}
28952916

28962917
async prepareLaunchTargetExecutable(name?: string): Promise<ExecutableTarget | null> {
2918+
// Return cached result for named targets to avoid duplicate builds when
2919+
// multiple ${input:...} variables resolve the same target in quick succession.
2920+
if (name) {
2921+
const cached = this._prepareCache.get(name);
2922+
if (cached && (Date.now() - cached.timestamp) < CMakeProject.PREPARE_CACHE_TTL_MS
2923+
&& await fs.exists(cached.result.path)) {
2924+
return cached.result;
2925+
}
2926+
}
2927+
28972928
let chosen: ExecutableTarget;
28982929

28992930
// Ensure that we've configured the project already. If we haven't, `getOrSelectLaunchTarget` won't see any
@@ -2944,6 +2975,12 @@ export class CMakeProject {
29442975
}
29452976

29462977
}
2978+
2979+
// Cache the result for named targets
2980+
if (name) {
2981+
this._prepareCache.set(name, { timestamp: Date.now(), result: chosen });
2982+
}
2983+
29472984
return chosen;
29482985
}
29492986

src/extension.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,7 +1753,7 @@ export class ExtensionManager implements vscode.Disposable {
17531753
telemetry.logEvent("substitution", { command: "launchTargetPath" });
17541754
return this.queryCMakeProject(async cmakeProject => {
17551755
if (targetName !== undefined && targetName !== null) {
1756-
await cmakeProject.setLaunchTargetByName(targetName);
1756+
return cmakeProject.launchTargetPath(targetName);
17571757
}
17581758
const targetPath = await cmakeProject.launchTargetPath();
17591759
return targetPath;
@@ -1766,7 +1766,7 @@ export class ExtensionManager implements vscode.Disposable {
17661766
telemetry.logEvent("substitution", { command: "launchTargetDirectory" });
17671767
return this.queryCMakeProject(async cmakeProject => {
17681768
if (targetName !== undefined && targetName !== null) {
1769-
await cmakeProject.setLaunchTargetByName(targetName);
1769+
return cmakeProject.launchTargetDirectory(targetName);
17701770
}
17711771
const targetDirectory = await cmakeProject.launchTargetDirectory();
17721772
return targetDirectory;
@@ -1779,7 +1779,7 @@ export class ExtensionManager implements vscode.Disposable {
17791779
telemetry.logEvent("substitution", { command: "launchTargetFilename" });
17801780
return this.queryCMakeProject(async cmakeProject => {
17811781
if (targetName !== undefined && targetName !== null) {
1782-
await cmakeProject.setLaunchTargetByName(targetName);
1782+
return cmakeProject.launchTargetFilename(targetName);
17831783
}
17841784
const targetFilename = await cmakeProject.launchTargetFilename();
17851785
return targetFilename;
@@ -1792,7 +1792,7 @@ export class ExtensionManager implements vscode.Disposable {
17921792
telemetry.logEvent("substitution", { command: "launchTargetName" });
17931793
return this.queryCMakeProject(async cmakeProject => {
17941794
if (targetName !== undefined && targetName !== null) {
1795-
await cmakeProject.setLaunchTargetByName(targetName);
1795+
return cmakeProject.launchTargetNameForSubstitution(targetName);
17961796
}
17971797
const targetFilename = await cmakeProject.launchTargetNameForSubstitution();
17981798
return targetFilename;
@@ -1805,7 +1805,7 @@ export class ExtensionManager implements vscode.Disposable {
18051805
telemetry.logEvent("substitution", { command: "getLaunchTargetPath" });
18061806
return this.queryCMakeProject(async cmakeProject => {
18071807
if (targetName !== undefined && targetName !== null) {
1808-
await cmakeProject.setLaunchTargetByName(targetName);
1808+
return cmakeProject.getLaunchTargetPath(targetName);
18091809
}
18101810
const targetPath = await cmakeProject.getLaunchTargetPath();
18111811
return targetPath;
@@ -1818,7 +1818,7 @@ export class ExtensionManager implements vscode.Disposable {
18181818
telemetry.logEvent("substitution", { command: "getLaunchTargetDirectory" });
18191819
return this.queryCMakeProject(async cmakeProject => {
18201820
if (targetName !== undefined && targetName !== null) {
1821-
await cmakeProject.setLaunchTargetByName(targetName);
1821+
return cmakeProject.getLaunchTargetDirectory(targetName);
18221822
}
18231823
const targetDirectory = await cmakeProject.getLaunchTargetDirectory();
18241824
return targetDirectory;
@@ -1831,7 +1831,7 @@ export class ExtensionManager implements vscode.Disposable {
18311831
telemetry.logEvent("substitution", { command: "getLaunchTargetFilename" });
18321832
return this.queryCMakeProject(async cmakeProject => {
18331833
if (targetName !== undefined && targetName !== null) {
1834-
await cmakeProject.setLaunchTargetByName(targetName);
1834+
return cmakeProject.getLaunchTargetFilename(targetName);
18351835
}
18361836
const targetFilename = await cmakeProject.getLaunchTargetFilename();
18371837
return targetFilename;
@@ -1844,7 +1844,7 @@ export class ExtensionManager implements vscode.Disposable {
18441844
telemetry.logEvent("substitution", { command: "getLaunchTargetName" });
18451845
return this.queryCMakeProject(async cmakeProject => {
18461846
if (targetName !== undefined && targetName !== null) {
1847-
await cmakeProject.setLaunchTargetByName(targetName);
1847+
return cmakeProject.getLaunchTargetName(targetName);
18481848
}
18491849
const targetFilename = await cmakeProject.getLaunchTargetName();
18501850
return targetFilename;

0 commit comments

Comments
 (0)