diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e724fa7e..be6a21af2 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 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/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 d4dec5478..06563b0ab 100644 --- a/package.json +++ b/package.json @@ -1725,14 +1725,23 @@ }, "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." + }, + "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..4e2235c95 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -73,7 +73,7 @@ export async function folderCleanBuild(folderContext: FolderContext) { export async function debugBuildWithOptions( ctx: WorkspaceContext, options: vscode.DebugSessionOptions, - targetName?: string + targetName: string | undefined ) { const current = ctx.currentFolder; if (!current) { @@ -107,7 +107,7 @@ export async function debugBuildWithOptions( return; } - const launchConfig = await getLaunchConfiguration(target.name, current); + const launchConfig = await getLaunchConfiguration(target.name, "debug", 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 67390fa08..619bf9391 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,48 @@ 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; + + // "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" 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( + `Unable to resolve target "${targetName}". No Swift package is available to search within.` + ); + } + 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, + 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..36a0a0ae4 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"; @@ -70,6 +69,8 @@ export async function makeDebugConfigurations( const config = structuredClone(launchConfigs[index]); updateConfigWithNewKeys(config, generatedConfig, [ "program", + "target", + "configuration", "cwd", "preLaunchTask", "type", @@ -121,55 +122,85 @@ 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, + 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 { folder } = getFolderAndNameSuffix(folderCtx); +): Promise { 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 ); - 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( - BuildFlags.buildDirectoryFromWorkspacePath(folder, true), - "debug", - target + 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; + 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 ); - const launchConfig = launchConfigs.find( - config => path.normalize(config.program) === path.normalize(targetPath) + } + // Expand each available VS Code workspace folder. + for (const workspaceFolder of availableWorkspaceFolders) { + expandedStr = expandedStr.replaceAll( + `$\{workspaceFolder:${workspaceFolder.name}}`, + workspaceFolder.uri.fsPath ); - return launchConfig; } + return expandedStr; +} + +// 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 = await getTargetBinaryPath(target, buildConfiguration, folderCtx); + const legacyTargetPath = getLegacyTargetBinaryPath(target, buildConfiguration, folderCtx); + return launchConfigs.find(config => { + // Newer launch configs use "target" and "configuration" properties which are easier to query. + if (config.target) { + const configBuildConfiguration = config.configuration ?? "debug"; + return config.target === target && configBuildConfiguration === buildConfiguration; + } + // 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); + 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); + }); } // Return array of DebugConfigurations for executables based on what is in Package.swift @@ -182,72 +213,30 @@ 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, + 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}`, + }, + ]; + }); } /** @@ -266,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/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts index 877cedca5..0c25958f1 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,11 @@ 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; + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); + }); }); test("Swift: Debug Build", async function () { @@ -71,29 +75,24 @@ 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; + taskWatcher.assertTaskCompletedByName("Build Debug PackageExe (defaultPackage)"); + }); }); test("Swift: Clean Build", async () => { @@ -108,7 +107,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/debugAdapterFactory.test.ts b/test/unit-tests/debugger/debugAdapterFactory.test.ts index 285a8dbaa..2fa04a136 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); @@ -415,6 +459,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", name: "Test Launch", + program: "/path/to/some/program", env: {}, }); @@ -437,6 +482,7 @@ suite("LLDBDebugConfigurationProvider Tests", () => { type: SWIFT_LAUNCH_CONFIG_TYPE, request: "launch", name: "Test Launch", + program: "/path/to/some/program", env, }); diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts index cc6c04bce..7f03f5178 100644 --- a/test/unit-tests/debugger/launch.test.ts +++ b/test/unit-tests/debugger/launch.test.ts @@ -77,7 +77,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -86,7 +87,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -115,7 +117,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -124,7 +127,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -140,7 +144,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -149,7 +154,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -164,7 +170,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -173,7 +180,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -190,7 +198,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -199,7 +208,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -217,7 +227,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -226,7 +237,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ]); @@ -242,7 +254,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -251,7 +264,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/release/executable", + target: "executable", + configuration: "release", preLaunchTask: "swift: Build Release executable", }, ], @@ -267,7 +281,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Debug executable", - program: "${workspaceFolder:folder}/.build/debug/executable", + target: "executable", + configuration: "debug", preLaunchTask: "swift: Build Debug executable", }, { @@ -276,7 +291,8 @@ suite("Launch Configurations Test", () => { args: [], cwd: "${workspaceFolder:folder}", name: "Release executable", - program: "${workspaceFolder:folder}/.build/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..52fa267d1 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"; @@ -121,6 +122,57 @@ export function waitForNoRunningTasks(options?: { timeout: number }): Promise { + this.completedTasks.push(event.execution.task); + }), + ]; + } + + /** Asserts that a task was completed with the given name. */ + assertTaskCompletedByName(name: string): void { + if (this.completedTasks.find(t => t.name.includes(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()); + } +} + +/** 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 diff --git a/userdocs/userdocs.docc/Articles/Features/debugging.md b/userdocs/userdocs.docc/Articles/Features/debugging.md index 0337d4881..b57d623bc 100644 --- a/userdocs/userdocs.docc/Articles/Features/debugging.md +++ b/userdocs/userdocs.docc/Articles/Features/debugging.md @@ -29,11 +29,25 @@ The most basic launch configuration uses the `"launch"` request and provides a p } ``` +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", + "configuration": "debug" +} +``` + 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 | 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.