From 2a5309f3d52333242191085e626109c0efe0e16c Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 21 Oct 2025 11:04:01 -0400 Subject: [PATCH 1/2] Support prelaunchTasks that build with swiftbuild If a prelaunch task is configured to build with --build-system swiftbuild we need to respect that when setting the program path of the launch configuration. Issue: #1830 --- src/debugger/debugAdapterFactory.ts | 5 +- src/debugger/launch.ts | 72 +++++++++- src/toolchain/BuildFlags.ts | 8 +- test/unit-tests/debugger/launch.test.ts | 178 +++++++++++++++++++++++- 4 files changed, 254 insertions(+), 9 deletions(-) diff --git a/src/debugger/debugAdapterFactory.ts b/src/debugger/debugAdapterFactory.ts index 619bf9391..eca0a3f83 100644 --- a/src/debugger/debugAdapterFactory.ts +++ b/src/debugger/debugAdapterFactory.ts @@ -21,7 +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 { getTargetBinaryPath, swiftPrelaunchBuildTaskArguments } from "./launch"; import { getLLDBLibPath, updateLaunchConfigForCI } from "./lldb"; import { registerLoggingDebugAdapterTracker } from "./logTracker"; @@ -133,7 +133,8 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration launchConfig.program = await getTargetBinaryPath( targetName, buildConfiguration, - folderContext + folderContext, + await swiftPrelaunchBuildTaskArguments(launchConfig, folderContext.workspaceFolder) ); delete launchConfig.target; } diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 36a0a0ae4..87b291d56 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -125,14 +125,17 @@ export async function makeDebugConfigurations( export async function getTargetBinaryPath( targetName: string, buildConfiguration: "debug" | "release", - folderCtx: FolderContext + folderCtx: FolderContext, + extraArgs: string[] = [] ): Promise { try { // Use dynamic path resolution with --show-bin-path const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath( folderCtx.folder.fsPath, buildConfiguration, - folderCtx.workspaceContext.logger + folderCtx.workspaceContext.logger, + "", + extraArgs ); return path.join(binPath, targetName); } catch (error) { @@ -256,7 +259,8 @@ export async function createSnippetConfiguration( const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath( ctx.folder.fsPath, "debug", - ctx.workspaceContext.logger + ctx.workspaceContext.logger, + "snippet" ); return { @@ -328,3 +332,65 @@ function updateConfigWithNewKeys( oldConfig[key] = newConfig[key]; } } + +/** + * Get the arguments for a launch configuration's preLaunchTask if it's a Swift build task + * @param launchConfig The launch configuration to check + * @param workspaceFolder The workspace folder context (optional) + * @returns Promise the task arguments if it's a Swift build task, undefined otherwise + */ +export async function swiftPrelaunchBuildTaskArguments( + launchConfig: vscode.DebugConfiguration, + workspaceFolder?: vscode.WorkspaceFolder +): Promise { + const preLaunchTask = launchConfig.preLaunchTask; + + if (!preLaunchTask || typeof preLaunchTask !== "string") { + return undefined; + } + + try { + // Fetch all available tasks + const allTasks = await vscode.tasks.fetchTasks(); + + // Find the task by name + const task = allTasks.find(t => { + // Check if task name matches (with or without "swift: " prefix) + const taskName = t.name; + const matches = + taskName === preLaunchTask || + taskName === `swift: ${preLaunchTask}` || + `swift: ${taskName}` === preLaunchTask; + + // If workspace folder is specified, also check scope + if (workspaceFolder && matches) { + return t.scope === workspaceFolder || t.scope === vscode.TaskScope.Workspace; + } + + return matches; + }); + + if (!task) { + return undefined; + } + + // Check if task type is "swift" + if (task.definition.type !== "swift") { + return undefined; + } + + // Check if args contain "build" and "--build-system" + const args = (task.definition.args as string[]) || []; + const hasBuild = args.includes("build"); + const hasBuildSystem = args.includes("--build-system"); + + if (hasBuild && hasBuildSystem) { + return args; + } + + return undefined; + } catch (error) { + // Log error but don't throw - return undefined for safety + return undefined; + } +} diff --git a/src/toolchain/BuildFlags.ts b/src/toolchain/BuildFlags.ts index 8211ee8a2..cd7512186 100644 --- a/src/toolchain/BuildFlags.ts +++ b/src/toolchain/BuildFlags.ts @@ -236,12 +236,14 @@ export class BuildFlags { async getBuildBinaryPath( workspacePath: string, buildConfiguration: "debug" | "release" = "debug", - logger: SwiftLogger + logger: SwiftLogger, + idSuffix: string = "", + extraArgs: string[] = [] ): Promise { // Checking the bin path requires a swift process execution, so we maintain a cache. // The cache key is based on workspace, configuration, and build arguments. const buildArgsHash = JSON.stringify(configuration.buildArguments); - const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}`; + const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}${idSuffix}`; if (BuildFlags.buildPathCache.has(cacheKey)) { return BuildFlags.buildPathCache.get(cacheKey)!; @@ -257,7 +259,7 @@ export class BuildFlags { const baseArgs = ["build", "--show-bin-path", "--configuration", buildConfiguration]; const fullArgs = [ ...this.withAdditionalFlags(baseArgs), - ...binPathAffectingArgs(configuration.buildArguments), + ...binPathAffectingArgs([...configuration.buildArguments, ...extraArgs]), ]; try { diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts index 7f03f5178..3c9a47477 100644 --- a/test/unit-tests/debugger/launch.test.ts +++ b/test/unit-tests/debugger/launch.test.ts @@ -18,7 +18,7 @@ import { FolderContext } from "@src/FolderContext"; import { Product, SwiftPackage } from "@src/SwiftPackage"; import configuration, { FolderConfiguration } from "@src/configuration"; import { SWIFT_LAUNCH_CONFIG_TYPE } from "@src/debugger/debugAdapter"; -import { makeDebugConfigurations } from "@src/debugger/launch"; +import { makeDebugConfigurations, swiftPrelaunchBuildTaskArguments } from "@src/debugger/launch"; import { MockedObject, @@ -301,3 +301,179 @@ suite("Launch Configurations Test", () => { expect(mockLaunchWSConfig.update).to.not.have.been.called; }); }); + +suite("Swift PreLaunch Build Task Arguments Test", () => { + const mockTasks = mockGlobalObject(vscode, "tasks"); + + setup(() => { + // Reset mocks before each test + mockTasks.fetchTasks.reset(); + }); + + test("swiftPrelaunchBuildTaskArguments returns task args for Swift build task", async () => { + const expectedArgs = ["build", "--product", "executable", "--build-system"]; + const mockTask = mockObject({ + name: "swift: Build Debug executable", + definition: { + type: "swift", + args: expectedArgs, + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.deep.equal(expectedArgs); + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for non-Swift task", async () => { + const mockTask = mockObject({ + name: "npm: build", + definition: { + type: "npm", + args: ["run", "build"], + }, + scope: vscode.TaskScope.Workspace, + source: "npm", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "npm: build", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for Swift task without build arg", async () => { + const mockTask = mockObject({ + name: "swift: Test", + definition: { + type: "swift", + args: ["test", "--build-system"], + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Test", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for Swift task without --build-system arg", async () => { + const mockTask = mockObject({ + name: "swift: Build Debug executable", + definition: { + type: "swift", + args: ["build", "--product", "executable"], + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments returns undefined for launch config without preLaunchTask", async () => { + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments handles errors gracefully", async () => { + mockTasks.fetchTasks.rejects(new Error("Failed to fetch tasks")); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.be.undefined; + }); + + test("swiftPrelaunchBuildTaskArguments handles task name variations", async () => { + const expectedArgs = ["build", "--product", "executable", "--build-system"]; + const mockTask = mockObject({ + name: "Build Debug executable", + definition: { + type: "swift", + args: expectedArgs, + }, + scope: vscode.TaskScope.Workspace, + source: "swift", + isBackground: false, + presentationOptions: {}, + problemMatchers: [], + runOptions: {}, + }); + + mockTasks.fetchTasks.resolves([instance(mockTask)]); + + const launchConfig: vscode.DebugConfiguration = { + type: "swift", + request: "launch", + name: "Debug executable", + preLaunchTask: "swift: Build Debug executable", + }; + + const result = await swiftPrelaunchBuildTaskArguments(launchConfig); + expect(result).to.deep.equal(expectedArgs); + }); +}); From 27c6b9738da21ea07d1a4515f02af3c0def018e2 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Wed, 22 Oct 2025 09:28:35 -0400 Subject: [PATCH 2/2] swiftPrelaunchBuildTaskArguments shouldn't care about build-system --- src/debugger/launch.ts | 10 ++------- test/unit-tests/debugger/launch.test.ts | 28 ------------------------- 2 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index 87b291d56..4ab865514 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -379,16 +379,10 @@ export async function swiftPrelaunchBuildTaskArguments( return undefined; } - // Check if args contain "build" and "--build-system" + // Check if args contain "build" const args = (task.definition.args as string[]) || []; const hasBuild = args.includes("build"); - const hasBuildSystem = args.includes("--build-system"); - - if (hasBuild && hasBuildSystem) { - return args; - } - - return undefined; + return hasBuild ? args : undefined; } catch (error) { // Log error but don't throw - return undefined for safety return undefined; diff --git a/test/unit-tests/debugger/launch.test.ts b/test/unit-tests/debugger/launch.test.ts index 3c9a47477..39ec0653b 100644 --- a/test/unit-tests/debugger/launch.test.ts +++ b/test/unit-tests/debugger/launch.test.ts @@ -395,34 +395,6 @@ suite("Swift PreLaunch Build Task Arguments Test", () => { expect(result).to.be.undefined; }); - test("swiftPrelaunchBuildTaskArguments returns undefined for Swift task without --build-system arg", async () => { - const mockTask = mockObject({ - name: "swift: Build Debug executable", - definition: { - type: "swift", - args: ["build", "--product", "executable"], - }, - scope: vscode.TaskScope.Workspace, - source: "swift", - isBackground: false, - presentationOptions: {}, - problemMatchers: [], - runOptions: {}, - }); - - mockTasks.fetchTasks.resolves([instance(mockTask)]); - - const launchConfig: vscode.DebugConfiguration = { - type: "swift", - request: "launch", - name: "Debug executable", - preLaunchTask: "swift: Build Debug executable", - }; - - const result = await swiftPrelaunchBuildTaskArguments(launchConfig); - expect(result).to.be.undefined; - }); - test("swiftPrelaunchBuildTaskArguments returns undefined for launch config without preLaunchTask", async () => { const launchConfig: vscode.DebugConfiguration = { type: "swift",