From 61af000e56ff08a66d4bd24c3b52582cc95a2036 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 14 Oct 2025 14:35:28 -0400 Subject: [PATCH 01/12] add a "target" property to launch configurations --- package.json | 7 +- src/debugger/debugAdapterFactory.ts | 17 +- src/debugger/launch.ts | 160 +++++++----------- .../debugger/debugAdapterFactory.test.ts | 46 ++++- test/unit-tests/debugger/launch.test.ts | 32 ++-- 5 files changed, 141 insertions(+), 121 deletions(-) diff --git a/package.json b/package.json index d4dec5478..56432382e 100644 --- a/package.json +++ b/package.json @@ -1725,14 +1725,15 @@ }, "configurationAttributes": { "launch": { - "required": [ - "program" - ], "properties": { "program": { "type": "string", "description": "Path to the program to debug." }, + "target": { + "type": "string", + "description": "The name of the SwiftPM target to debug." + }, "args": { "type": [ "array", diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 67390fa08..c1840e0a7 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -21,6 +21,7 @@ import { SwiftToolchain } from "../toolchain/toolchain"; import { fileExists } from "../utilities/filesystem"; import { getErrorDescription, swiftRuntimeEnv } from "../utilities/utilities"; import { DebugAdapter, LaunchConfigType, SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; +import { getTargetBinaryPath } from "./launch"; import { getLLDBLibPath, updateLaunchConfigForCI } from "./lldb"; import { registerLoggingDebugAdapterTracker } from "./logTracker"; @@ -94,10 +95,22 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration folder: vscode.WorkspaceFolder | undefined, launchConfig: vscode.DebugConfiguration ): Promise { - const workspaceFolder = this.workspaceContext.folders.find( + const folderContext = this.workspaceContext.folders.find( f => f.workspaceFolder.uri.fsPath === folder?.uri.fsPath ); - const toolchain = workspaceFolder?.toolchain ?? this.workspaceContext.globalToolchain; + const toolchain = folderContext?.toolchain ?? this.workspaceContext.globalToolchain; + + // Convert the "target" property to a "program" + if (typeof launchConfig.target === "string") { + const targetName = launchConfig.target; + if (!folderContext) { + throw new Error( + `Unable to resolve target "${targetName}". No Swift package is available to search within.` + ); + } + launchConfig.program = await getTargetBinaryPath(targetName, folderContext); + delete launchConfig.target; + } // Fix the program path on Windows to include the ".exe" extension if ( diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 30d8783d2..6c5119a70 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -70,6 +70,7 @@ export async function makeDebugConfigurations( const config = structuredClone(launchConfigs[index]); updateConfigWithNewKeys(config, generatedConfig, [ "program", + "target", "cwd", "preLaunchTask", "type", @@ -121,15 +122,10 @@ export async function makeDebugConfigurations( return true; } -// Return debug launch configuration for an executable in the given folder -export async function getLaunchConfiguration( - target: string, +export async function getTargetBinaryPath( + targetName: string, folderCtx: FolderContext -): Promise { - const wsLaunchSection = vscode.workspace.workspaceFile - ? vscode.workspace.getConfiguration("launch") - : vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder); - const launchConfigs = wsLaunchSection.get("configurations") || []; +): Promise { const { folder } = getFolderAndNameSuffix(folderCtx); try { // Use dynamic path resolution with --show-bin-path @@ -139,39 +135,49 @@ export async function getLaunchConfiguration( "debug", folderCtx.workspaceContext.logger ); - const targetPath = path.join(binPath, target); - - const expandPath = (p: string) => - p.replace( - `$\{workspaceFolder:${folderCtx.workspaceFolder.name}}`, - folderCtx.folder.fsPath - ); - - // Users could be on different platforms with different path annotations, - // so normalize before we compare. - const launchConfig = launchConfigs.find( - config => - // Old launch configs had program paths that looked like ${workspaceFolder:test}/defaultPackage/.build/debug, - // where `debug` was a symlink to the host-triple-folder/debug. Because targetPath is determined by `--show-bin-path` - // in `getBuildBinaryPath` we need to follow this symlink to get the real path if we want to compare them. - path.normalize(realpathSync(expandPath(config.program))) === - path.normalize(targetPath) - ); - return launchConfig; + return path.join(binPath, targetName); } catch (error) { // Fallback to traditional path construction if dynamic resolution fails - const targetPath = path.join( + return path.join( BuildFlags.buildDirectoryFromWorkspacePath(folder, true), "debug", - target - ); - const launchConfig = launchConfigs.find( - config => path.normalize(config.program) === path.normalize(targetPath) + targetName ); - return launchConfig; } } +// Return debug launch configuration for an executable in the given folder +export async function getLaunchConfiguration( + target: string, + folderCtx: FolderContext +): Promise { + const wsLaunchSection = vscode.workspace.workspaceFile + ? vscode.workspace.getConfiguration("launch") + : vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder); + const launchConfigs = wsLaunchSection.get("configurations") || []; + const targetPath: string = await getTargetBinaryPath(target, folderCtx); + const expandPath = (p: string): string => { + return p.replace( + `$\{workspaceFolder:${folderCtx.workspaceFolder.name}}`, + folderCtx.folder.fsPath + ); + }; + // Users could be on different platforms with different path annotations, + // so normalize before we compare. + return launchConfigs.find(config => { + // Newer launch configs use a "target" property which is easy to query. + if (config.target) { + return config.target === target; + } + // Old launch configs had program paths that looked like ${workspaceFolder:test}/defaultPackage/.build/debug, + // where `debug` was a symlink to the host-triple-folder/debug. Because targetPath is determined by `--show-bin-path` + // in `getBuildBinaryPath` we need to follow this symlink to get the real path if we want to compare them. + const normalizedConfigPath = path.normalize(realpathSync(expandPath(config.program))); + const normalizedTargetPath = path.normalize(targetPath); + return normalizedConfigPath === normalizedTargetPath; + }); +} + // Return array of DebugConfigurations for executables based on what is in Package.swift async function createExecutableConfigurations( ctx: FolderContext @@ -182,72 +188,28 @@ async function createExecutableConfigurations( // to make it easier for users switching between platforms. const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, undefined, "posix"); - try { - // Get dynamic build paths for both debug and release configurations - const [debugBinPath, releaseBinPath] = await Promise.all([ - ctx.toolchain.buildFlags.getBuildBinaryPath( - ctx.folder.fsPath, - folder, - "debug", - ctx.workspaceContext.logger - ), - ctx.toolchain.buildFlags.getBuildBinaryPath( - ctx.folder.fsPath, - folder, - "release", - ctx.workspaceContext.logger - ), - ]); - - return executableProducts.flatMap(product => { - const baseConfig = { - type: SWIFT_LAUNCH_CONFIG_TYPE, - request: "launch", - args: [], - cwd: folder, - }; - return [ - { - ...baseConfig, - name: `Debug ${product.name}${nameSuffix}`, - program: path.posix.join(debugBinPath, product.name), - preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`, - }, - { - ...baseConfig, - name: `Release ${product.name}${nameSuffix}`, - program: path.posix.join(releaseBinPath, product.name), - preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`, - }, - ]; - }); - } catch (error) { - // Fallback to traditional path construction if dynamic resolution fails - const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix"); - - return executableProducts.flatMap(product => { - const baseConfig = { - type: SWIFT_LAUNCH_CONFIG_TYPE, - request: "launch", - args: [], - cwd: folder, - }; - return [ - { - ...baseConfig, - name: `Debug ${product.name}${nameSuffix}`, - program: path.posix.join(buildDirectory, "debug", product.name), - preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`, - }, - { - ...baseConfig, - name: `Release ${product.name}${nameSuffix}`, - program: path.posix.join(buildDirectory, "release", product.name), - preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`, - }, - ]; - }); - } + return executableProducts.flatMap(product => { + const baseConfig = { + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + args: [], + cwd: folder, + }; + return [ + { + ...baseConfig, + name: `Debug ${product.name}${nameSuffix}`, + target: product.name, + preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`, + }, + { + ...baseConfig, + name: `Release ${product.name}${nameSuffix}`, + target: product.name, + preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`, + }, + ]; + }); } /** diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index 285a8dbaa..91c0becbc 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; import * as mockFS from "mock-fs"; +import * as path from "path"; import * as vscode from "vscode"; import { FolderContext } from "@src/FolderContext"; @@ -23,6 +24,7 @@ import * as debugAdapter from "@src/debugger/debugAdapter"; import { LLDBDebugConfigurationProvider } from "@src/debugger/debugAdapterFactory"; import * as lldb from "@src/debugger/lldb"; import { SwiftLogger } from "@src/logging/SwiftLogger"; +import { BuildFlags } from "@src/toolchain/BuildFlags"; import { SwiftToolchain } from "@src/toolchain/toolchain"; import { Result } from "@src/utilities/result"; import { Version } from "@src/utilities/version"; @@ -39,12 +41,17 @@ import { suite("LLDBDebugConfigurationProvider Tests", () => { let mockWorkspaceContext: MockedObject; let mockToolchain: MockedObject; + let mockBuildFlags: MockedObject; let mockLogger: MockedObject; const mockDebugAdapter = mockGlobalObject(debugAdapter, "DebugAdapter"); const mockWindow = mockGlobalObject(vscode, "window"); setup(() => { - mockToolchain = mockObject({ swiftVersion: new Version(6, 0, 0) }); + mockBuildFlags = mockObject({ getBuildBinaryPath: mockFn() }); + mockToolchain = mockObject({ + swiftVersion: new Version(6, 0, 0), + buildFlags: instance(mockBuildFlags), + }); mockLogger = mockObject({ info: mockFn(), }); @@ -135,6 +142,43 @@ suite("LLDBDebugConfigurationProvider Tests", () => { expect(launchConfig).to.be.null; }); + test("sets the 'program' property if a 'target' property is present", async () => { + mockBuildFlags.getBuildBinaryPath.resolves( + "/path/to/swift-executable/.build/arm64-apple-macosx/debug/" + ); + const folder: vscode.WorkspaceFolder = { + index: 0, + name: "swift-executable", + uri: vscode.Uri.file("/path/to/swift-executable"), + }; + const mockedFolderCtx = mockObject({ + workspaceContext: instance(mockWorkspaceContext), + workspaceFolder: folder, + folder: folder.uri, + toolchain: instance(mockToolchain), + relativePath: "./", + }); + mockWorkspaceContext.folders = [instance(mockedFolderCtx)]; + const configProvider = new LLDBDebugConfigurationProvider( + "darwin", + instance(mockWorkspaceContext), + instance(mockLogger) + ); + const launchConfig = await configProvider.resolveDebugConfigurationWithSubstitutedVariables( + folder, + { + name: "Test Launch Config", + type: SWIFT_LAUNCH_CONFIG_TYPE, + request: "launch", + target: "executable", + } + ); + expect(launchConfig).to.have.property( + "program", + path.normalize("/path/to/swift-executable/.build/arm64-apple-macosx/debug/executable") + ); + }); + suite("CodeLLDB selected in settings", () => { let mockLldbConfiguration: MockedObject; const mockLLDB = mockGlobalModule(lldb); diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts index cc6c04bce..6357100ce 100644 --- a/test/unit-tests/debugger/launch.test.ts +++ b/test/unit-tests/debugger/launch.test.ts @@ -77,7 +77,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -86,7 +86,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ], @@ -115,7 +115,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -124,7 +124,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ], @@ -140,7 +140,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -149,7 +149,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ]); @@ -164,7 +164,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -173,7 +173,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ], @@ -190,7 +190,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -199,7 +199,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ]); @@ -217,7 +217,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -226,7 +226,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ]); @@ -242,7 +242,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -251,7 +251,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ], @@ -267,7 +267,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", preLaunchTask: "swift: Build Debug executable", }, { @@ -276,7 +276,7 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", preLaunchTask: "swift: Build Release executable", }, ]); From 3541e3c70a47813f14e94134db3006c69fa5e6d3 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 14 Oct 2025 15:54:46 -0400 Subject: [PATCH 02/12] update docs and changelog --- CHANGELOG.md | 1 + .../userdocs.docc/Articles/Features/debugging.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e724fa7e..7bcc3efd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Prompt to cancel and replace the active test run if one is in flight ([#1774](https://github.com/swiftlang/vscode-swift/pull/1774)) - A walkthrough for first time extension users ([#1560](https://github.com/swiftlang/vscode-swift/issues/1560)) - 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)) +- Added a new `target` property to `swift` launch configurations that can be used instead of `program` for SwiftPM based projects ([#1890](https://github.com/swiftlang/vscode-swift/pull/1890)) ### Fixed diff --git a/userdocs/userdocs.docc/Articles/Features/debugging.md b/userdocs/userdocs.docc/Articles/Features/debugging.md index 0337d4881..a67bafed0 100644 --- a/userdocs/userdocs.docc/Articles/Features/debugging.md +++ b/userdocs/userdocs.docc/Articles/Features/debugging.md @@ -29,11 +29,23 @@ The most basic launch configuration uses the `"launch"` request and provides a p } ``` +For SwiftPM based projects, you may specify a `target` instead of a `program` to make your debug configurations shareable between different developers on different platforms: + +```javascript +{ + "label": "Debug my-executable", // Human readable name for the configuration + "type": "swift", // All Swift launch configurations use the same type + "request": "launch", // Launch an executable + "target": "my-executable" +} +``` + There are many more options that you can specify which will alter the behavior of the debugger: | Parameter | Type | Description | |-------------------------------|-------------|---------------------| | program | string | Path to the executable to launch. +| target | string | Path to the target to launch. Only available in SwiftPM projects. | args | [string] | An array of command line argument strings to be passed to the program being launched. | cwd | string | The program working directory. | env | dictionary | Environment variables to set when launching the program. The format of each environment variable string is "VAR=VALUE" for environment variables with values or just "VAR" for environment variables with no values. From e7482194a1a9b5b20f3c73fba46fe282b36beab5 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Tue, 14 Oct 2025 16:11:05 -0400 Subject: [PATCH 03/12] display an error if neither a "target" nor "program" property exist in a "launch" debug config --- src/debugger/debugAdapterFactory.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index c1840e0a7..c3737520e 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -100,6 +100,17 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration ); const toolchain = folderContext?.toolchain ?? this.workspaceContext.globalToolchain; + // "launch" requests must have either a "target" or "program" property + if ( + launchConfig.request === "launch" && + !("program" in launchConfig) && + !("target" in launchConfig) + ) { + throw new Error( + "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." + ); + } + // Convert the "target" property to a "program" if (typeof launchConfig.target === "string") { const targetName = launchConfig.target; From fbe3ee06285db828b58f5b0d24d0fc3e17a2e313 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 11:10:59 -0400 Subject: [PATCH 04/12] fix failing tests --- test/unit-tests/debugger/debugAdapterFactory.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit-tests/debugger/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index 91c0becbc..2fa04a136 100644 --- a/test/unit-tests/debugger/debugAdapterFactory.test.ts +++ b/test/unit-tests/debugger/debugAdapterFactory.test.ts @@ -459,6 +459,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", name: "Test Launch", + program: "/path/to/some/program", env: {}, }); @@ -481,6 +482,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", name: "Test Launch", + program: "/path/to/some/program", env, }); From 9e71c2dbff9b5ee58b39de3e43e2a6cfdcffd387 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 11:49:12 -0400 Subject: [PATCH 05/12] fix variable expansion in launch configurations --- src/debugger/launch.ts | 31 +++++++++++++++++++++++-------- src/utilities/filesystem.ts | 14 ++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 6c5119a70..6ab0708c3 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -11,7 +11,6 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// -import { realpathSync } from "fs"; import * as path from "path"; import { isDeepStrictEqual } from "util"; import * as vscode from "vscode"; @@ -19,6 +18,7 @@ import * as vscode from "vscode"; import { FolderContext } from "../FolderContext"; import configuration from "../configuration"; import { BuildFlags } from "../toolchain/BuildFlags"; +import { realpathSync } from "../utilities/filesystem"; import { stringArrayInEnglish } from "../utilities/utilities"; import { getFolderAndNameSuffix } from "./buildConfig"; import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; @@ -146,6 +146,27 @@ export async function getTargetBinaryPath( } } +/** Expands VS Code variables such as ${workspaceFolder} in the given string. */ +function expandVariables(str: string): string { + let expandedStr = str; + const availableWorkspaceFolders = vscode.workspace.workspaceFolders ?? []; + // Expand the top level VS Code workspace folder. + if (availableWorkspaceFolders.length > 0) { + expandedStr = expandedStr.replaceAll( + "${workspaceFolder}", + availableWorkspaceFolders[0].uri.fsPath + ); + } + // Expand each available VS Code workspace folder. + for (const workspaceFolder of availableWorkspaceFolders) { + expandedStr = expandedStr.replaceAll( + `$\{workspaceFolder:${workspaceFolder.name}}`, + workspaceFolder.uri.fsPath + ); + } + return expandedStr; +} + // Return debug launch configuration for an executable in the given folder export async function getLaunchConfiguration( target: string, @@ -156,12 +177,6 @@ export async function getLaunchConfiguration( : vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder); const launchConfigs = wsLaunchSection.get("configurations") || []; const targetPath: string = await getTargetBinaryPath(target, folderCtx); - const expandPath = (p: string): string => { - return p.replace( - `$\{workspaceFolder:${folderCtx.workspaceFolder.name}}`, - folderCtx.folder.fsPath - ); - }; // Users could be on different platforms with different path annotations, // so normalize before we compare. return launchConfigs.find(config => { @@ -172,7 +187,7 @@ export async function getLaunchConfiguration( // Old launch configs had program paths that looked like ${workspaceFolder:test}/defaultPackage/.build/debug, // where `debug` was a symlink to the host-triple-folder/debug. Because targetPath is determined by `--show-bin-path` // in `getBuildBinaryPath` we need to follow this symlink to get the real path if we want to compare them. - const normalizedConfigPath = path.normalize(realpathSync(expandPath(config.program))); + const normalizedConfigPath = path.normalize(realpathSync(expandVariables(config.program))); const normalizedTargetPath = path.normalize(targetPath); return normalizedConfigPath === normalizedTargetPath; }); diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts index 098177d56..71dbc4cda 100644 --- a/src/utilities/filesystem.ts +++ b/src/utilities/filesystem.ts @@ -12,6 +12,7 @@ // //===----------------------------------------------------------------------===// import { Options, convertPathToPattern, glob as fastGlob } from "fast-glob"; +import * as fsSync from "fs"; import * as fs from "fs/promises"; import { contains } from "micromatch"; import * as path from "path"; @@ -21,6 +22,19 @@ import configuration from "../configuration"; export const validFileTypes = ["swift", "c", "cpp", "h", "hpp", "m", "mm"]; +/** + * Finds the real path of a file synchronously. If the file does not exist + * then this will just return the provided path. + * @param path The file path to find the real location of. + * @returns The real path of the file. + */ +export function realpathSync(path: string): string { + if (!fsSync.existsSync(path)) { + return path; + } + return fsSync.realpathSync(path); +} + /** * Checks if a file, directory or symlink exists at the supplied path. * @param pathComponents The path to check for existence From a39c4d5e3f306a8a8e0b1057d2241d26ca70efa5 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 15:03:10 -0400 Subject: [PATCH 06/12] add "configuration" property to launch configurations --- assets/test/.vscode/launch.json | 5 +- package.json | 8 +++ src/commands/build.ts | 9 +-- src/debugger/debugAdapterFactory.ts | 7 ++- src/debugger/launch.ts | 51 ++++++++++------- src/toolchain/BuildFlags.ts | 5 +- src/utilities/filesystem.ts | 14 ----- test/integration-tests/commands/build.test.ts | 51 +++++++++-------- test/unit-tests/debugger/launch.test.ts | 16 ++++++ test/unit-tests/toolchain/BuildFlags.test.ts | 57 +++---------------- test/utilities/tasks.ts | 36 ++++++++++++ 11 files changed, 144 insertions(+), 115 deletions(-) diff --git a/assets/test/.vscode/launch.json b/assets/test/.vscode/launch.json index 90b9c8da4..c7011a707 100644 --- a/assets/test/.vscode/launch.json +++ b/assets/test/.vscode/launch.json @@ -4,6 +4,7 @@ "type": "swift", "request": "launch", "name": "Debug PackageExe (defaultPackage)", + // Explicitly use "program" to test searching for launch configs by program. "program": "${workspaceFolder:test}/defaultPackage/.build/debug/PackageExe", "args": [], "cwd": "${workspaceFolder:test}/defaultPackage", @@ -15,7 +16,9 @@ "type": "swift", "request": "launch", "name": "Release PackageExe (defaultPackage)", - "program": "${workspaceFolder:test}/defaultPackage/.build/release/PackageExe", + // Explicitly use "target" and "configuration" to test searching for launch configs by target. + "target": "PackageExe", + "configuration": "release", "args": [], "cwd": "${workspaceFolder:test}/defaultPackage", "preLaunchTask": "swift: Build Release PackageExe (defaultPackage)", diff --git a/package.json b/package.json index 56432382e..06563b0ab 100644 --- a/package.json +++ b/package.json @@ -1734,6 +1734,14 @@ "type": "string", "description": "The name of the SwiftPM target to debug." }, + "configuration": { + "type": "string", + "enum": [ + "debug", + "release" + ], + "description": "The configuration of the SwiftPM target to use." + }, "args": { "type": [ "array", diff --git a/src/commands/build.ts b/src/commands/build.ts index 4aec2b946..8d5a766cf 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -25,14 +25,14 @@ import { executeTaskWithUI } from "./utilities"; * Executes a {@link vscode.Task task} to run swift target. */ export async function runBuild(ctx: WorkspaceContext, target?: string) { - return await debugBuildWithOptions(ctx, { noDebug: true }, target); + return await debugBuildWithOptions(ctx, { noDebug: true }, target, "release"); } /** * Executes a {@link vscode.Task task} to debug swift target. */ export async function debugBuild(ctx: WorkspaceContext, target?: string) { - return await debugBuildWithOptions(ctx, {}, target); + return await debugBuildWithOptions(ctx, {}, target, "debug"); } /** @@ -73,7 +73,8 @@ export async function folderCleanBuild(folderContext: FolderContext) { export async function debugBuildWithOptions( ctx: WorkspaceContext, options: vscode.DebugSessionOptions, - targetName?: string + targetName: string | undefined, + buildConfiguration: "debug" | "release" ) { const current = ctx.currentFolder; if (!current) { @@ -107,7 +108,7 @@ export async function debugBuildWithOptions( return; } - const launchConfig = await getLaunchConfiguration(target.name, current); + const launchConfig = await getLaunchConfiguration(target.name, buildConfiguration, current); if (launchConfig) { ctx.buildStarted(target.name, launchConfig, options); const result = await debugLaunchConfig( diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index c3737520e..6584d05b0 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -119,7 +119,12 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration `Unable to resolve target "${targetName}". No Swift package is available to search within.` ); } - launchConfig.program = await getTargetBinaryPath(targetName, folderContext); + const buildConfiguration: "debug" | "release" = launchConfig.configuration ?? "debug"; + launchConfig.program = await getTargetBinaryPath( + targetName, + buildConfiguration, + folderContext + ); delete launchConfig.target; } diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 6ab0708c3..36a0a0ae4 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -18,7 +18,6 @@ import * as vscode from "vscode"; import { FolderContext } from "../FolderContext"; import configuration from "../configuration"; import { BuildFlags } from "../toolchain/BuildFlags"; -import { realpathSync } from "../utilities/filesystem"; import { stringArrayInEnglish } from "../utilities/utilities"; import { getFolderAndNameSuffix } from "./buildConfig"; import { SWIFT_LAUNCH_CONFIG_TYPE } from "./debugAdapter"; @@ -71,6 +70,7 @@ export async function makeDebugConfigurations( updateConfigWithNewKeys(config, generatedConfig, [ "program", "target", + "configuration", "cwd", "preLaunchTask", "type", @@ -124,28 +124,35 @@ export async function makeDebugConfigurations( export async function getTargetBinaryPath( targetName: string, + buildConfiguration: "debug" | "release", folderCtx: FolderContext ): Promise { - const { folder } = getFolderAndNameSuffix(folderCtx); try { // Use dynamic path resolution with --show-bin-path const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath( folderCtx.folder.fsPath, - folder, - "debug", + buildConfiguration, folderCtx.workspaceContext.logger ); return path.join(binPath, targetName); } catch (error) { // Fallback to traditional path construction if dynamic resolution fails - return path.join( - BuildFlags.buildDirectoryFromWorkspacePath(folder, true), - "debug", - targetName - ); + return getLegacyTargetBinaryPath(targetName, buildConfiguration, folderCtx); } } +export function getLegacyTargetBinaryPath( + targetName: string, + buildConfiguration: "debug" | "release", + folderCtx: FolderContext +): string { + return path.join( + BuildFlags.buildDirectoryFromWorkspacePath(folderCtx.folder.fsPath, true), + buildConfiguration, + targetName + ); +} + /** Expands VS Code variables such as ${workspaceFolder} in the given string. */ function expandVariables(str: string): string { let expandedStr = str; @@ -170,26 +177,29 @@ function expandVariables(str: string): string { // Return debug launch configuration for an executable in the given folder export async function getLaunchConfiguration( target: string, + buildConfiguration: "debug" | "release", folderCtx: FolderContext ): Promise { const wsLaunchSection = vscode.workspace.workspaceFile ? vscode.workspace.getConfiguration("launch") : vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder); const launchConfigs = wsLaunchSection.get("configurations") || []; - const targetPath: string = await getTargetBinaryPath(target, folderCtx); - // Users could be on different platforms with different path annotations, - // so normalize before we compare. + const targetPath = await getTargetBinaryPath(target, buildConfiguration, folderCtx); + const legacyTargetPath = getLegacyTargetBinaryPath(target, buildConfiguration, folderCtx); return launchConfigs.find(config => { - // Newer launch configs use a "target" property which is easy to query. + // Newer launch configs use "target" and "configuration" properties which are easier to query. if (config.target) { - return config.target === target; + const configBuildConfiguration = config.configuration ?? "debug"; + return config.target === target && configBuildConfiguration === buildConfiguration; } - // Old launch configs had program paths that looked like ${workspaceFolder:test}/defaultPackage/.build/debug, - // where `debug` was a symlink to the host-triple-folder/debug. Because targetPath is determined by `--show-bin-path` - // in `getBuildBinaryPath` we need to follow this symlink to get the real path if we want to compare them. - const normalizedConfigPath = path.normalize(realpathSync(expandVariables(config.program))); + // Users could be on different platforms with different path annotations, so normalize before we compare. + const normalizedConfigPath = path.normalize(expandVariables(config.program)); const normalizedTargetPath = path.normalize(targetPath); - return normalizedConfigPath === normalizedTargetPath; + const normalizedLegacyTargetPath = path.normalize(legacyTargetPath); + // Old launch configs had program paths that looked like "${workspaceFolder:test}/defaultPackage/.build/debug", + // where `debug` was a symlink to the /debug. We want to support both old and new, so we're + // comparing against both to find a match. + return [normalizedTargetPath, normalizedLegacyTargetPath].includes(normalizedConfigPath); }); } @@ -215,12 +225,14 @@ async function createExecutableConfigurations( ...baseConfig, name: `Debug ${product.name}${nameSuffix}`, target: product.name, + configuration: "debug", preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`, }, { ...baseConfig, name: `Release ${product.name}${nameSuffix}`, target: product.name, + configuration: "release", preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`, }, ]; @@ -243,7 +255,6 @@ export async function createSnippetConfiguration( // Use dynamic path resolution with --show-bin-path const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath( ctx.folder.fsPath, - folder, "debug", ctx.workspaceContext.logger ); diff --git a/src/toolchain/BuildFlags.ts b/src/toolchain/BuildFlags.ts index e4e870db7..8211ee8a2 100644 --- a/src/toolchain/BuildFlags.ts +++ b/src/toolchain/BuildFlags.ts @@ -234,7 +234,6 @@ export class BuildFlags { * @returns Promise resolving to the build binary path */ async getBuildBinaryPath( - cwd: string, workspacePath: string, buildConfiguration: "debug" | "release" = "debug", logger: SwiftLogger @@ -263,7 +262,9 @@ export class BuildFlags { try { // Execute swift build --show-bin-path - const result = await execSwift(fullArgs, this.toolchain, { cwd }); + const result = await execSwift(fullArgs, this.toolchain, { + cwd: workspacePath, + }); const binPath = result.stdout.trim(); // Cache the result diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts index 71dbc4cda..098177d56 100644 --- a/src/utilities/filesystem.ts +++ b/src/utilities/filesystem.ts @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// import { Options, convertPathToPattern, glob as fastGlob } from "fast-glob"; -import * as fsSync from "fs"; import * as fs from "fs/promises"; import { contains } from "micromatch"; import * as path from "path"; @@ -22,19 +21,6 @@ import configuration from "../configuration"; export const validFileTypes = ["swift", "c", "cpp", "h", "hpp", "m", "mm"]; -/** - * Finds the real path of a file synchronously. If the file does not exist - * then this will just return the provided path. - * @param path The file path to find the real location of. - * @returns The real path of the file. - */ -export function realpathSync(path: string): string { - if (!fsSync.existsSync(path)) { - return path; - } - return fsSync.realpathSync(path); -} - /** * Checks if a file, directory or symlink exists at the supplied path. * @param pathComponents The path to check for existence diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index 877cedca5..dfd8451c2 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -24,6 +24,7 @@ import { Version } from "@src/utilities/version"; import { testAssetUri } from "../../fixtures"; import { tag } from "../../tags"; import { continueSession, waitForDebugAdapterRequest } from "../../utilities/debug"; +import { withTaskWatcher } from "../../utilities/tasks"; import { activateExtensionForSuite, folderInRootWorkspace } from "../utilities/testutilities"; tag("large").suite("Build Commands", function () { @@ -43,7 +44,7 @@ tag("large").suite("Build Commands", function () { ) { this.skip(); } - // A breakpoint will have not effect on the Run command. + // A breakpoint will have no effect on the Run command. vscode.debug.addBreakpoints(breakpoints); workspaceContext = ctx; @@ -58,8 +59,13 @@ tag("large").suite("Build Commands", function () { }); test("Swift: Run Build", async () => { - const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); - expect(result).to.be.true; + await withTaskWatcher(async taskWatcher => { + const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); + expect(result).to.be.true; + expect(taskWatcher.completedTasks.map(t => t.name)).to.include( + "Build Release PackageExe (defaultPackage)" + ); + }); }); test("Swift: Debug Build", async function () { @@ -71,29 +77,26 @@ tag("large").suite("Build Commands", function () { ) { this.skip(); } - // Promise used to indicate we hit the break point. - // NB: "stopped" is the exact command when debuggee has stopped due to break point, - // but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue - const bpPromise = waitForDebugAdapterRequest( - "Debug PackageExe (defaultPackage)" + - (vscode.workspace.workspaceFile ? " (workspace)" : ""), - workspaceContext.globalToolchain.swiftVersion, - "stackTrace" - ); - const resultPromise: Thenable = vscode.commands.executeCommand( - Commands.DEBUG, - "PackageExe" - ); + await withTaskWatcher(async taskWatcher => { + const resultPromise = vscode.commands.executeCommand(Commands.DEBUG, "PackageExe"); - await bpPromise; - let succeeded = false; - void resultPromise.then(s => (succeeded = s)); - while (!succeeded) { + // Wait until we hit the breakpoint. + // NB: "stopped" is the exact command when debuggee has stopped due to break point, + // but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue + await waitForDebugAdapterRequest( + "Debug PackageExe (defaultPackage)" + + (vscode.workspace.workspaceFile ? " (workspace)" : ""), + workspaceContext.globalToolchain.swiftVersion, + "stackTrace" + ); await continueSession(); - await new Promise(r => setTimeout(r, 500)); - } - await expect(resultPromise).to.eventually.be.true; + + await expect(resultPromise).to.eventually.be.true; + expect(taskWatcher.completedTasks.map(t => t.name)).to.include( + "Build Debug PackageExe (defaultPackage)" + ); + }); }); test("Swift: Clean Build", async () => { @@ -108,7 +111,7 @@ tag("large").suite("Build Commands", function () { const afterItemCount = (await fs.readdir(buildPath)).length; // .build folder is going to be filled with built artifacts after Commands.RUN command - // After executing the clean command the build directory is guranteed to have less entry. + // After executing the clean command the build directory is guaranteed to have less entries. expect(afterItemCount).to.be.lessThan(beforeItemCount); }); }); diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts index 6357100ce..7f03f5178 100644 --- a/test/unit-tests/debugger/launch.test.ts +++ b/test/unit-tests/debugger/launch.test.ts @@ -78,6 +78,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -87,6 +88,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -116,6 +118,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -125,6 +128,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -141,6 +145,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -150,6 +155,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -165,6 +171,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -174,6 +181,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -191,6 +199,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -200,6 +209,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -218,6 +228,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -227,6 +238,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -243,6 +255,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -252,6 +265,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -268,6 +282,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Debug executable", target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -277,6 +292,7 @@ suite("Launch Configurations Test", () => { cwd: "${workspaceFolder:folder}", name: "Release executable", target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); diff --git a/test/unit-tests/toolchain/BuildFlags.test.ts b/test/unit-tests/toolchain/BuildFlags.test.ts index 8ec81a81c..224ee728d 100644 --- a/test/unit-tests/toolchain/BuildFlags.test.ts +++ b/test/unit-tests/toolchain/BuildFlags.test.ts @@ -459,7 +459,6 @@ suite("BuildFlags Test Suite", () => { test("debug configuration calls swift build with correct arguments", async () => { const result = await buildFlags.getBuildBinaryPath( "/test/workspace", - "${workspaceFolder:SimpleExecutable}", "debug", instance(logger) ); @@ -478,7 +477,6 @@ suite("BuildFlags Test Suite", () => { test("release configuration calls swift build with correct arguments", async () => { const result = await buildFlags.getBuildBinaryPath( "/test/workspace", - "${workspaceFolder:SimpleExecutable}", "release", instance(logger) ); @@ -494,12 +492,7 @@ suite("BuildFlags Test Suite", () => { test("includes build arguments in command", async () => { buildArgsConfig.setValue(["--build-system", "swiftbuild"]); - await buildFlags.getBuildBinaryPath( - "/test/workspace", - "${workspaceFolder:SimpleExecutable}", - "debug", - instance(logger) - ); + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); const [args] = execSwiftSpy.firstCall.args; expect(args).to.include("--build-system"); @@ -510,7 +503,6 @@ suite("BuildFlags Test Suite", () => { // First call const result1 = await buildFlags.getBuildBinaryPath( "/test/workspace", - "${workspaceFolder:SimpleExecutable}", "debug", instance(logger) ); @@ -520,7 +512,6 @@ suite("BuildFlags Test Suite", () => { // Second call should use cache const result2 = await buildFlags.getBuildBinaryPath( "/test/workspace", - "${workspaceFolder:SimpleExecutable}", "debug", instance(logger) ); @@ -529,7 +520,6 @@ suite("BuildFlags Test Suite", () => { // Different configuration should not use cache const result3 = await buildFlags.getBuildBinaryPath( - "/test/workspace", "/test/workspace", "release", instance(logger) @@ -540,24 +530,14 @@ suite("BuildFlags Test Suite", () => { test("different build arguments create different cache entries", async () => { // First call with no build arguments - await buildFlags.getBuildBinaryPath( - "/test/workspace", - "${workspaceFolder:SimpleExecutable}", - "debug", - instance(logger) - ); + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); expect(execSwiftSpy).to.have.been.calledOnce; // Change build arguments buildArgsConfig.setValue(["--build-system", "swiftbuild"]); // Second call should not use cache due to different build arguments - await buildFlags.getBuildBinaryPath( - "/test/workspace", - "${workspaceFolder:SimpleExecutable}", - "debug", - instance(logger) - ); + await buildFlags.getBuildBinaryPath("/test/workspace", "debug", instance(logger)); expect(execSwiftSpy).to.have.been.calledTwice; }); @@ -571,45 +551,24 @@ suite("BuildFlags Test Suite", () => { sinon.replace(utilities, "execSwift", execSwiftSpy); const log = instance(logger); - const result = await buildFlags.getBuildBinaryPath( - "/test/workspace", - "${workspaceFolder:SimpleExecutable}", - "debug", - log - ); + const result = await buildFlags.getBuildBinaryPath("/test/workspace", "debug", log); // Should fallback to traditional path - expect(result).to.include("${workspaceFolder:SimpleExecutable}"); - expect(result).to.include("debug"); + expect(result).to.equal(path.normalize("/test/workspace/.build/debug")); expect(log.warn).to.have.been.calledOnce; }); test("clearBuildPathCache clears all cached entries", async () => { // Cache some entries - await buildFlags.getBuildBinaryPath( - "cwd", - "${workspaceFolder:Workspace1}", - "debug", - instance(logger) - ); - await buildFlags.getBuildBinaryPath( - "cwd", - "${workspaceFolder:Workspace2}", - "release", - instance(logger) - ); + await buildFlags.getBuildBinaryPath("cwd", "debug", instance(logger)); + await buildFlags.getBuildBinaryPath("cwd", "release", instance(logger)); expect(execSwiftSpy).to.have.been.calledTwice; // Clear cache BuildFlags.clearBuildPathCache(); // Next calls should execute again - await buildFlags.getBuildBinaryPath( - "cwd", - "${workspaceFolder:Workspace1}", - "debug", - instance(logger) - ); + await buildFlags.getBuildBinaryPath("cwd", "debug", instance(logger)); expect(execSwiftSpy).to.have.been.calledThrice; }); }); diff --git a/test/utilities/tasks.ts b/test/utilities/tasks.ts index 7b0832200..df0d2a4a0 100644 --- a/test/utilities/tasks.ts +++ b/test/utilities/tasks.ts @@ -121,6 +121,42 @@ export function waitForNoRunningTasks(options?: { timeout: number }): Promise { + this.completedTasks.push(event.execution.task); + }), + ]; + } + + dispose() { + this.subscriptions.forEach(s => s.dispose()); + } +} + +/** Executes the given callback with a TaskWatcher that listens to the VS Code tasks API for the duration of the callback. */ +export async function withTaskWatcher( + task: (watcher: TaskWatcher) => Promise +): Promise { + const watcher = new TaskWatcher(); + try { + await task(watcher); + } finally { + watcher.dispose(); + } +} + /** * Ideally we would want to use {@link executeTaskAndWaitForResult} but that * requires the tests creating the task through some means. If the From 8817a9649462612e457723fd5ff1849f8dc06e82 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 15:05:37 -0400 Subject: [PATCH 07/12] display an error if "configuration" property is invalid --- src/debugger/debugAdapterFactory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 6584d05b0..3d103b358 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -119,7 +119,12 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration `Unable to resolve target "${targetName}". No Swift package is available to search within.` ); } - const buildConfiguration: "debug" | "release" = launchConfig.configuration ?? "debug"; + const buildConfiguration = launchConfig.configuration ?? "debug"; + if (!["debug", "release"].includes(buildConfiguration)) { + throw new Error( + `Unknown configuration property "${buildConfiguration}" in Swift debug configuration. Valid options are "debug" or "release. Please update your debug configuration.` + ); + } launchConfig.program = await getTargetBinaryPath( targetName, buildConfiguration, From 93f2169771e278fa1f3eb8b4f0189a7e151479eb Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 15:10:11 -0400 Subject: [PATCH 08/12] display an error if both "target" and "program" exist in a launch configuration --- src/debugger/debugAdapterFactory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 3d103b358..619bf9391 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -111,8 +111,13 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration ); } - // Convert the "target" property to a "program" + // Convert the "target" and "configuration" properties to a "program" if (typeof launchConfig.target === "string") { + if ("program" in launchConfig) { + throw new Error( + `Unable to set both "target" and "program" on the same Swift debug configuration. Please remove one of them from your debug configuration.` + ); + } const targetName = launchConfig.target; if (!folderContext) { throw new Error( From 7af7ed5163f3d6257f0460788651e9f9ec718467 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 16:17:13 -0400 Subject: [PATCH 09/12] improve task assertion --- test/integration-tests/commands/build.test.ts | 8 ++------ test/utilities/tasks.ts | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index dfd8451c2..c3d7717a0 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -62,9 +62,7 @@ tag("large").suite("Build Commands", function () { await withTaskWatcher(async taskWatcher => { const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); expect(result).to.be.true; - expect(taskWatcher.completedTasks.map(t => t.name)).to.include( - "Build Release PackageExe (defaultPackage)" - ); + taskWatcher.assertTaskCompleted("Build Release PackageExe (defaultPackage)"); }); }); @@ -93,9 +91,7 @@ tag("large").suite("Build Commands", function () { await continueSession(); await expect(resultPromise).to.eventually.be.true; - expect(taskWatcher.completedTasks.map(t => t.name)).to.include( - "Build Debug PackageExe (defaultPackage)" - ); + taskWatcher.assertTaskCompleted("Build Debug PackageExe (defaultPackage)"); }); }); diff --git a/test/utilities/tasks.ts b/test/utilities/tasks.ts index df0d2a4a0..72bda6192 100644 --- a/test/utilities/tasks.ts +++ b/test/utilities/tasks.ts @@ -11,6 +11,7 @@ // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// +import { AssertionError } from "chai"; import * as vscode from "vscode"; import { SwiftTask } from "@src/tasks/SwiftTaskProvider"; @@ -140,6 +141,21 @@ export class TaskWatcher implements vscode.Disposable { ]; } + /** Asserts that a task was completed with the given name. */ + assertTaskCompleted(name: string): void { + if (this.completedTasks.find(t => t.name === name)) { + return; + } + const createStringArray = (arr: string[]): string => { + return "[\n" + arr.map(s => " " + s).join(",\n") + "\n]"; + }; + throw new AssertionError(`expected a task with name "${name}" to have completed.`, { + actual: createStringArray(this.completedTasks.map(t => t.name)), + expected: createStringArray([name]), + showDiff: true, + }); + } + dispose() { this.subscriptions.forEach(s => s.dispose()); } From 3a7f1f04f40ad3c6820fecfc2a264d42232d9403 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 15 Oct 2025 16:55:34 -0400 Subject: [PATCH 10/12] don't match by exact task name as it will be slightly different for code workspace tests --- test/integration-tests/commands/build.test.ts | 4 ++-- test/utilities/tasks.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index c3d7717a0..28d0057dd 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -62,7 +62,7 @@ tag("large").suite("Build Commands", function () { await withTaskWatcher(async taskWatcher => { const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); expect(result).to.be.true; - taskWatcher.assertTaskCompleted("Build Release PackageExe (defaultPackage)"); + taskWatcher.assertTaskCompletedByName("Build Release PackageExe (defaultPackage)"); }); }); @@ -91,7 +91,7 @@ tag("large").suite("Build Commands", function () { await continueSession(); await expect(resultPromise).to.eventually.be.true; - taskWatcher.assertTaskCompleted("Build Debug PackageExe (defaultPackage)"); + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); }); }); diff --git a/test/utilities/tasks.ts b/test/utilities/tasks.ts index 72bda6192..52fa267d1 100644 --- a/test/utilities/tasks.ts +++ b/test/utilities/tasks.ts @@ -142,8 +142,8 @@ export class TaskWatcher implements vscode.Disposable { } /** Asserts that a task was completed with the given name. */ - assertTaskCompleted(name: string): void { - if (this.completedTasks.find(t => t.name === name)) { + assertTaskCompletedByName(name: string): void { + if (this.completedTasks.find(t => t.name.includes(name))) { return; } const createStringArray = (arr: string[]): string => { From 568f78a0f97de3b30010a28273cd7a86735e8c78 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 16 Oct 2025 11:11:02 -0400 Subject: [PATCH 11/12] always use debug build for run and debug commands --- src/commands/build.ts | 9 ++++----- test/integration-tests/commands/build.test.ts | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/commands/build.ts b/src/commands/build.ts index 8d5a766cf..4e2235c95 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -25,14 +25,14 @@ import { executeTaskWithUI } from "./utilities"; * Executes a {@link vscode.Task task} to run swift target. */ export async function runBuild(ctx: WorkspaceContext, target?: string) { - return await debugBuildWithOptions(ctx, { noDebug: true }, target, "release"); + return await debugBuildWithOptions(ctx, { noDebug: true }, target); } /** * Executes a {@link vscode.Task task} to debug swift target. */ export async function debugBuild(ctx: WorkspaceContext, target?: string) { - return await debugBuildWithOptions(ctx, {}, target, "debug"); + return await debugBuildWithOptions(ctx, {}, target); } /** @@ -73,8 +73,7 @@ export async function folderCleanBuild(folderContext: FolderContext) { export async function debugBuildWithOptions( ctx: WorkspaceContext, options: vscode.DebugSessionOptions, - targetName: string | undefined, - buildConfiguration: "debug" | "release" + targetName: string | undefined ) { const current = ctx.currentFolder; if (!current) { @@ -108,7 +107,7 @@ export async function debugBuildWithOptions( return; } - const launchConfig = await getLaunchConfiguration(target.name, buildConfiguration, current); + const launchConfig = await getLaunchConfiguration(target.name, "debug", current); if (launchConfig) { ctx.buildStarted(target.name, launchConfig, options); const result = await debugLaunchConfig( diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index 28d0057dd..0c25958f1 100644 --- a/test/integration-tests/commands/build.test.ts +++ b/test/integration-tests/commands/build.test.ts @@ -62,7 +62,7 @@ tag("large").suite("Build Commands", function () { await withTaskWatcher(async taskWatcher => { const result = await vscode.commands.executeCommand(Commands.RUN, "PackageExe"); expect(result).to.be.true; - taskWatcher.assertTaskCompletedByName("Build Release PackageExe (defaultPackage)"); + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); }); }); From 6bd5b844852008600ba9637eed5be6d52206dd55 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 16 Oct 2025 11:19:28 -0400 Subject: [PATCH 12/12] update documentation and change log to include configuration property --- CHANGELOG.md | 2 +- userdocs/userdocs.docc/Articles/Features/debugging.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bcc3efd8..be6a21af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Prompt to cancel and replace the active test run if one is in flight ([#1774](https://github.com/swiftlang/vscode-swift/pull/1774)) - A walkthrough for first time extension users ([#1560](https://github.com/swiftlang/vscode-swift/issues/1560)) - 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)) -- Added a new `target` property to `swift` launch configurations that can be used instead of `program` for SwiftPM based projects ([#1890](https://github.com/swiftlang/vscode-swift/pull/1890)) +- 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)) ### Fixed diff --git a/userdocs/userdocs.docc/Articles/Features/debugging.md b/userdocs/userdocs.docc/Articles/Features/debugging.md index a67bafed0..b57d623bc 100644 --- a/userdocs/userdocs.docc/Articles/Features/debugging.md +++ b/userdocs/userdocs.docc/Articles/Features/debugging.md @@ -29,14 +29,15 @@ The most basic launch configuration uses the `"launch"` request and provides a p } ``` -For SwiftPM based projects, you may specify a `target` instead of a `program` to make your debug configurations shareable between different developers on different platforms: +For SwiftPM based projects, you may specify a `target` and `configuration` instead of a `program` to make your debug configurations shareable between different developers on different platforms: ```javascript { "label": "Debug my-executable", // Human readable name for the configuration "type": "swift", // All Swift launch configurations use the same type "request": "launch", // Launch an executable - "target": "my-executable" + "target": "my-executable", + "configuration": "debug" } ``` @@ -45,7 +46,8 @@ There are many more options that you can specify which will alter the behavior o | Parameter | Type | Description | |-------------------------------|-------------|---------------------| | program | string | Path to the executable to launch. -| target | string | Path to the target to launch. Only available in SwiftPM projects. +| target | string | Name of the target to launch. Only available in SwiftPM projects. +| configuration | string | Configuration used to build the target (debug or release). Only available in SwiftPM projects. | args | [string] | An array of command line argument strings to be passed to the program being launched. | cwd | string | The program working directory. | env | dictionary | Environment variables to set when launching the program. The format of each environment variable string is "VAR=VALUE" for environment variables with values or just "VAR" for environment variables with no values.