Skip to content

Commit 6c9733e

Browse files
Add SwiftPM "target" and "configuration" properties to Swift launch configurations (#1890)
1 parent 6228bd4 commit 6c9733e

File tree

13 files changed

+332
-205
lines changed

13 files changed

+332
-205
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Prompt to cancel and replace the active test run if one is in flight ([#1774](https://github.com/swiftlang/vscode-swift/pull/1774))
1010
- A walkthrough for first time extension users ([#1560](https://github.com/swiftlang/vscode-swift/issues/1560))
1111
- Allow `swift.backgroundCompilation` setting to accept an object where enabling the `useDefaultTask` property will run the default build task, and the `release` property will run the `release` variant of the Build All task ([#1857](https://github.com/swiftlang/vscode-swift/pull/1857))
12+
- Added new `target` and `configuration` properties to `swift` launch configurations that can be used instead of `program` for SwiftPM based projects ([#1890](https://github.com/swiftlang/vscode-swift/pull/1890))
1213

1314
### Fixed
1415

assets/test/.vscode/launch.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"type": "swift",
55
"request": "launch",
66
"name": "Debug PackageExe (defaultPackage)",
7+
// Explicitly use "program" to test searching for launch configs by program.
78
"program": "${workspaceFolder:test}/defaultPackage/.build/debug/PackageExe",
89
"args": [],
910
"cwd": "${workspaceFolder:test}/defaultPackage",
@@ -15,7 +16,9 @@
1516
"type": "swift",
1617
"request": "launch",
1718
"name": "Release PackageExe (defaultPackage)",
18-
"program": "${workspaceFolder:test}/defaultPackage/.build/release/PackageExe",
19+
// Explicitly use "target" and "configuration" to test searching for launch configs by target.
20+
"target": "PackageExe",
21+
"configuration": "release",
1922
"args": [],
2023
"cwd": "${workspaceFolder:test}/defaultPackage",
2124
"preLaunchTask": "swift: Build Release PackageExe (defaultPackage)",

package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,14 +1725,23 @@
17251725
},
17261726
"configurationAttributes": {
17271727
"launch": {
1728-
"required": [
1729-
"program"
1730-
],
17311728
"properties": {
17321729
"program": {
17331730
"type": "string",
17341731
"description": "Path to the program to debug."
17351732
},
1733+
"target": {
1734+
"type": "string",
1735+
"description": "The name of the SwiftPM target to debug."
1736+
},
1737+
"configuration": {
1738+
"type": "string",
1739+
"enum": [
1740+
"debug",
1741+
"release"
1742+
],
1743+
"description": "The configuration of the SwiftPM target to use."
1744+
},
17361745
"args": {
17371746
"type": [
17381747
"array",

src/commands/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export async function folderCleanBuild(folderContext: FolderContext) {
7373
export async function debugBuildWithOptions(
7474
ctx: WorkspaceContext,
7575
options: vscode.DebugSessionOptions,
76-
targetName?: string
76+
targetName: string | undefined
7777
) {
7878
const current = ctx.currentFolder;
7979
if (!current) {
@@ -107,7 +107,7 @@ export async function debugBuildWithOptions(
107107
return;
108108
}
109109

110-
const launchConfig = await getLaunchConfiguration(target.name, current);
110+
const launchConfig = await getLaunchConfiguration(target.name, "debug", current);
111111
if (launchConfig) {
112112
ctx.buildStarted(target.name, launchConfig, options);
113113
const result = await debugLaunchConfig(

src/debugger/debugAdapterFactory.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { SwiftToolchain } from "../toolchain/toolchain";
2121
import { fileExists } from "../utilities/filesystem";
2222
import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities";
2323
import { DebugAdapter, LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter";
24+
import { getTargetBinaryPath } from "./launch";
2425
import { getLLDBLibPath, updateLaunchConfigForCI } from "./lldb";
2526
import { registerLoggingDebugAdapterTracker } from "./logTracker";
2627

@@ -94,10 +95,48 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration
9495
folder: vscode.WorkspaceFolder | undefined,
9596
launchConfig: vscode.DebugConfiguration
9697
): Promise<vscode.DebugConfiguration | undefined | null> {
97-
const workspaceFolder = this.workspaceContext.folders.find(
98+
const folderContext = this.workspaceContext.folders.find(
9899
f => f.workspaceFolder.uri.fsPath === folder?.uri.fsPath
99100
);
100-
const toolchain = workspaceFolder?.toolchain ?? this.workspaceContext.globalToolchain;
101+
const toolchain = folderContext?.toolchain ?? this.workspaceContext.globalToolchain;
102+
103+
// "launch" requests must have either a "target" or "program" property
104+
if (
105+
launchConfig.request === "launch" &&
106+
!("program" in launchConfig) &&
107+
!("target" in launchConfig)
108+
) {
109+
throw new Error(
110+
"You must specify either a 'program' or a 'target' when 'request' is set to 'launch' in a Swift debug configuration. Please update your debug configuration."
111+
);
112+
}
113+
114+
// Convert the "target" and "configuration" properties to a "program"
115+
if (typeof launchConfig.target === "string") {
116+
if ("program" in launchConfig) {
117+
throw new Error(
118+
`Unable to set both "target" and "program" on the same Swift debug configuration. Please remove one of them from your debug configuration.`
119+
);
120+
}
121+
const targetName = launchConfig.target;
122+
if (!folderContext) {
123+
throw new Error(
124+
`Unable to resolve target "${targetName}". No Swift package is available to search within.`
125+
);
126+
}
127+
const buildConfiguration = launchConfig.configuration ?? "debug";
128+
if (!["debug", "release"].includes(buildConfiguration)) {
129+
throw new Error(
130+
`Unknown configuration property "${buildConfiguration}" in Swift debug configuration. Valid options are "debug" or "release. Please update your debug configuration.`
131+
);
132+
}
133+
launchConfig.program = await getTargetBinaryPath(
134+
targetName,
135+
buildConfiguration,
136+
folderContext
137+
);
138+
delete launchConfig.target;
139+
}
101140

102141
// Fix the program path on Windows to include the ".exe" extension
103142
if (

src/debugger/launch.ts

Lines changed: 93 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// SPDX-License-Identifier: Apache-2.0
1212
//
1313
//===----------------------------------------------------------------------===//
14-
import { realpathSync } from "fs";
1514
import * as path from "path";
1615
import { isDeepStrictEqual } from "util";
1716
import * as vscode from "vscode";
@@ -70,6 +69,8 @@ export async function makeDebugConfigurations(
7069
const config = structuredClone(launchConfigs[index]);
7170
updateConfigWithNewKeys(config, generatedConfig, [
7271
"program",
72+
"target",
73+
"configuration",
7374
"cwd",
7475
"preLaunchTask",
7576
"type",
@@ -121,55 +122,85 @@ export async function makeDebugConfigurations(
121122
return true;
122123
}
123124

124-
// Return debug launch configuration for an executable in the given folder
125-
export async function getLaunchConfiguration(
126-
target: string,
125+
export async function getTargetBinaryPath(
126+
targetName: string,
127+
buildConfiguration: "debug" | "release",
127128
folderCtx: FolderContext
128-
): Promise<vscode.DebugConfiguration | undefined> {
129-
const wsLaunchSection = vscode.workspace.workspaceFile
130-
? vscode.workspace.getConfiguration("launch")
131-
: vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder);
132-
const launchConfigs = wsLaunchSection.get<vscode.DebugConfiguration[]>("configurations") || [];
133-
const { folder } = getFolderAndNameSuffix(folderCtx);
129+
): Promise<string> {
134130
try {
135131
// Use dynamic path resolution with --show-bin-path
136132
const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath(
137133
folderCtx.folder.fsPath,
138-
folder,
139-
"debug",
134+
buildConfiguration,
140135
folderCtx.workspaceContext.logger
141136
);
142-
const targetPath = path.join(binPath, target);
143-
144-
const expandPath = (p: string) =>
145-
p.replace(
146-
`$\{workspaceFolder:${folderCtx.workspaceFolder.name}}`,
147-
folderCtx.folder.fsPath
148-
);
149-
150-
// Users could be on different platforms with different path annotations,
151-
// so normalize before we compare.
152-
const launchConfig = launchConfigs.find(
153-
config =>
154-
// Old launch configs had program paths that looked like ${workspaceFolder:test}/defaultPackage/.build/debug,
155-
// where `debug` was a symlink to the host-triple-folder/debug. Because targetPath is determined by `--show-bin-path`
156-
// in `getBuildBinaryPath` we need to follow this symlink to get the real path if we want to compare them.
157-
path.normalize(realpathSync(expandPath(config.program))) ===
158-
path.normalize(targetPath)
159-
);
160-
return launchConfig;
137+
return path.join(binPath, targetName);
161138
} catch (error) {
162139
// Fallback to traditional path construction if dynamic resolution fails
163-
const targetPath = path.join(
164-
BuildFlags.buildDirectoryFromWorkspacePath(folder, true),
165-
"debug",
166-
target
140+
return getLegacyTargetBinaryPath(targetName, buildConfiguration, folderCtx);
141+
}
142+
}
143+
144+
export function getLegacyTargetBinaryPath(
145+
targetName: string,
146+
buildConfiguration: "debug" | "release",
147+
folderCtx: FolderContext
148+
): string {
149+
return path.join(
150+
BuildFlags.buildDirectoryFromWorkspacePath(folderCtx.folder.fsPath, true),
151+
buildConfiguration,
152+
targetName
153+
);
154+
}
155+
156+
/** Expands VS Code variables such as ${workspaceFolder} in the given string. */
157+
function expandVariables(str: string): string {
158+
let expandedStr = str;
159+
const availableWorkspaceFolders = vscode.workspace.workspaceFolders ?? [];
160+
// Expand the top level VS Code workspace folder.
161+
if (availableWorkspaceFolders.length > 0) {
162+
expandedStr = expandedStr.replaceAll(
163+
"${workspaceFolder}",
164+
availableWorkspaceFolders[0].uri.fsPath
167165
);
168-
const launchConfig = launchConfigs.find(
169-
config => path.normalize(config.program) === path.normalize(targetPath)
166+
}
167+
// Expand each available VS Code workspace folder.
168+
for (const workspaceFolder of availableWorkspaceFolders) {
169+
expandedStr = expandedStr.replaceAll(
170+
`$\{workspaceFolder:${workspaceFolder.name}}`,
171+
workspaceFolder.uri.fsPath
170172
);
171-
return launchConfig;
172173
}
174+
return expandedStr;
175+
}
176+
177+
// Return debug launch configuration for an executable in the given folder
178+
export async function getLaunchConfiguration(
179+
target: string,
180+
buildConfiguration: "debug" | "release",
181+
folderCtx: FolderContext
182+
): Promise<vscode.DebugConfiguration | undefined> {
183+
const wsLaunchSection = vscode.workspace.workspaceFile
184+
? vscode.workspace.getConfiguration("launch")
185+
: vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder);
186+
const launchConfigs = wsLaunchSection.get<vscode.DebugConfiguration[]>("configurations") || [];
187+
const targetPath = await getTargetBinaryPath(target, buildConfiguration, folderCtx);
188+
const legacyTargetPath = getLegacyTargetBinaryPath(target, buildConfiguration, folderCtx);
189+
return launchConfigs.find(config => {
190+
// Newer launch configs use "target" and "configuration" properties which are easier to query.
191+
if (config.target) {
192+
const configBuildConfiguration = config.configuration ?? "debug";
193+
return config.target === target && configBuildConfiguration === buildConfiguration;
194+
}
195+
// Users could be on different platforms with different path annotations, so normalize before we compare.
196+
const normalizedConfigPath = path.normalize(expandVariables(config.program));
197+
const normalizedTargetPath = path.normalize(targetPath);
198+
const normalizedLegacyTargetPath = path.normalize(legacyTargetPath);
199+
// Old launch configs had program paths that looked like "${workspaceFolder:test}/defaultPackage/.build/debug",
200+
// where `debug` was a symlink to the <host-triple-folder>/debug. We want to support both old and new, so we're
201+
// comparing against both to find a match.
202+
return [normalizedTargetPath, normalizedLegacyTargetPath].includes(normalizedConfigPath);
203+
});
173204
}
174205

175206
// Return array of DebugConfigurations for executables based on what is in Package.swift
@@ -182,72 +213,30 @@ async function createExecutableConfigurations(
182213
// to make it easier for users switching between platforms.
183214
const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, undefined, "posix");
184215

185-
try {
186-
// Get dynamic build paths for both debug and release configurations
187-
const [debugBinPath, releaseBinPath] = await Promise.all([
188-
ctx.toolchain.buildFlags.getBuildBinaryPath(
189-
ctx.folder.fsPath,
190-
folder,
191-
"debug",
192-
ctx.workspaceContext.logger
193-
),
194-
ctx.toolchain.buildFlags.getBuildBinaryPath(
195-
ctx.folder.fsPath,
196-
folder,
197-
"release",
198-
ctx.workspaceContext.logger
199-
),
200-
]);
201-
202-
return executableProducts.flatMap(product => {
203-
const baseConfig = {
204-
type: SWIFT_LAUNCH_CONFIG_TYPE,
205-
request: "launch",
206-
args: [],
207-
cwd: folder,
208-
};
209-
return [
210-
{
211-
...baseConfig,
212-
name: `Debug ${product.name}${nameSuffix}`,
213-
program: path.posix.join(debugBinPath, product.name),
214-
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
215-
},
216-
{
217-
...baseConfig,
218-
name: `Release ${product.name}${nameSuffix}`,
219-
program: path.posix.join(releaseBinPath, product.name),
220-
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
221-
},
222-
];
223-
});
224-
} catch (error) {
225-
// Fallback to traditional path construction if dynamic resolution fails
226-
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix");
227-
228-
return executableProducts.flatMap(product => {
229-
const baseConfig = {
230-
type: SWIFT_LAUNCH_CONFIG_TYPE,
231-
request: "launch",
232-
args: [],
233-
cwd: folder,
234-
};
235-
return [
236-
{
237-
...baseConfig,
238-
name: `Debug ${product.name}${nameSuffix}`,
239-
program: path.posix.join(buildDirectory, "debug", product.name),
240-
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
241-
},
242-
{
243-
...baseConfig,
244-
name: `Release ${product.name}${nameSuffix}`,
245-
program: path.posix.join(buildDirectory, "release", product.name),
246-
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
247-
},
248-
];
249-
});
250-
}
216+
return executableProducts.flatMap(product => {
217+
const baseConfig = {
218+
type: SWIFT_LAUNCH_CONFIG_TYPE,
219+
request: "launch",
220+
args: [],
221+
cwd: folder,
222+
};
223+
return [
224+
{
225+
...baseConfig,
226+
name: `Debug ${product.name}${nameSuffix}`,
227+
target: product.name,
228+
configuration: "debug",
229+
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
230+
},
231+
{
232+
...baseConfig,
233+
name: `Release ${product.name}${nameSuffix}`,
234+
target: product.name,
235+
configuration: "release",
236+
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
237+
},
238+
];
239+
});
251240
}
252241

253242
/**
@@ -266,7 +255,6 @@ export async function createSnippetConfiguration(
266255
// Use dynamic path resolution with --show-bin-path
267256
const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath(
268257
ctx.folder.fsPath,
269-
folder,
270258
"debug",
271259
ctx.workspaceContext.logger
272260
);

src/toolchain/BuildFlags.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ export class BuildFlags {
234234
* @returns Promise resolving to the build binary path
235235
*/
236236
async getBuildBinaryPath(
237-
cwd: string,
238237
workspacePath: string,
239238
buildConfiguration: "debug" | "release" = "debug",
240239
logger: SwiftLogger
@@ -263,7 +262,9 @@ export class BuildFlags {
263262

264263
try {
265264
// Execute swift build --show-bin-path
266-
const result = await execSwift(fullArgs, this.toolchain, { cwd });
265+
const result = await execSwift(fullArgs, this.toolchain, {
266+
cwd: workspacePath,
267+
});
267268
const binPath = result.stdout.trim();
268269

269270
// Cache the result

0 commit comments

Comments
 (0)