Skip to content

Commit 7975021

Browse files
authored
Support prelaunchTasks that build with swiftbuild (#1898)
* 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 * swiftPrelaunchBuildTaskArguments shouldn't care about build-system
1 parent 8d896f9 commit 7975021

File tree

4 files changed

+220
-9
lines changed

4 files changed

+220
-9
lines changed

src/debugger/debugAdapterFactory.ts

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

@@ -133,7 +133,8 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration
133133
launchConfig.program = await getTargetBinaryPath(
134134
targetName,
135135
buildConfiguration,
136-
folderContext
136+
folderContext,
137+
await swiftPrelaunchBuildTaskArguments(launchConfig, folderContext.workspaceFolder)
137138
);
138139
delete launchConfig.target;
139140
}

src/debugger/launch.ts

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,17 @@ export async function makeDebugConfigurations(
125125
export async function getTargetBinaryPath(
126126
targetName: string,
127127
buildConfiguration: "debug" | "release",
128-
folderCtx: FolderContext
128+
folderCtx: FolderContext,
129+
extraArgs: string[] = []
129130
): Promise<string> {
130131
try {
131132
// Use dynamic path resolution with --show-bin-path
132133
const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath(
133134
folderCtx.folder.fsPath,
134135
buildConfiguration,
135-
folderCtx.workspaceContext.logger
136+
folderCtx.workspaceContext.logger,
137+
"",
138+
extraArgs
136139
);
137140
return path.join(binPath, targetName);
138141
} catch (error) {
@@ -256,7 +259,8 @@ export async function createSnippetConfiguration(
256259
const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath(
257260
ctx.folder.fsPath,
258261
"debug",
259-
ctx.workspaceContext.logger
262+
ctx.workspaceContext.logger,
263+
"snippet"
260264
);
261265

262266
return {
@@ -328,3 +332,59 @@ function updateConfigWithNewKeys(
328332
oldConfig[key] = newConfig[key];
329333
}
330334
}
335+
336+
/**
337+
* Get the arguments for a launch configuration's preLaunchTask if it's a Swift build task
338+
* @param launchConfig The launch configuration to check
339+
* @param workspaceFolder The workspace folder context (optional)
340+
* @returns Promise<string[] | undefined> the task arguments if it's a Swift build task, undefined otherwise
341+
*/
342+
export async function swiftPrelaunchBuildTaskArguments(
343+
launchConfig: vscode.DebugConfiguration,
344+
workspaceFolder?: vscode.WorkspaceFolder
345+
): Promise<string[] | undefined> {
346+
const preLaunchTask = launchConfig.preLaunchTask;
347+
348+
if (!preLaunchTask || typeof preLaunchTask !== "string") {
349+
return undefined;
350+
}
351+
352+
try {
353+
// Fetch all available tasks
354+
const allTasks = await vscode.tasks.fetchTasks();
355+
356+
// Find the task by name
357+
const task = allTasks.find(t => {
358+
// Check if task name matches (with or without "swift: " prefix)
359+
const taskName = t.name;
360+
const matches =
361+
taskName === preLaunchTask ||
362+
taskName === `swift: ${preLaunchTask}` ||
363+
`swift: ${taskName}` === preLaunchTask;
364+
365+
// If workspace folder is specified, also check scope
366+
if (workspaceFolder && matches) {
367+
return t.scope === workspaceFolder || t.scope === vscode.TaskScope.Workspace;
368+
}
369+
370+
return matches;
371+
});
372+
373+
if (!task) {
374+
return undefined;
375+
}
376+
377+
// Check if task type is "swift"
378+
if (task.definition.type !== "swift") {
379+
return undefined;
380+
}
381+
382+
// Check if args contain "build"
383+
const args = (task.definition.args as string[]) || [];
384+
const hasBuild = args.includes("build");
385+
return hasBuild ? args : undefined;
386+
} catch (error) {
387+
// Log error but don't throw - return undefined for safety
388+
return undefined;
389+
}
390+
}

src/toolchain/BuildFlags.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,14 @@ export class BuildFlags {
236236
async getBuildBinaryPath(
237237
workspacePath: string,
238238
buildConfiguration: "debug" | "release" = "debug",
239-
logger: SwiftLogger
239+
logger: SwiftLogger,
240+
idSuffix: string = "",
241+
extraArgs: string[] = []
240242
): Promise<string> {
241243
// Checking the bin path requires a swift process execution, so we maintain a cache.
242244
// The cache key is based on workspace, configuration, and build arguments.
243245
const buildArgsHash = JSON.stringify(configuration.buildArguments);
244-
const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}`;
246+
const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}${idSuffix}`;
245247

246248
if (BuildFlags.buildPathCache.has(cacheKey)) {
247249
return BuildFlags.buildPathCache.get(cacheKey)!;
@@ -257,7 +259,7 @@ export class BuildFlags {
257259
const baseArgs = ["build", "--show-bin-path", "--configuration", buildConfiguration];
258260
const fullArgs = [
259261
...this.withAdditionalFlags(baseArgs),
260-
...binPathAffectingArgs(configuration.buildArguments),
262+
...binPathAffectingArgs([...configuration.buildArguments, ...extraArgs]),
261263
];
262264

263265
try {

test/unit-tests/debugger/launch.test.ts

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { FolderContext } from "@src/FolderContext";
1818
import { Product, SwiftPackage } from "@src/SwiftPackage";
1919
import configuration, { FolderConfiguration } from "@src/configuration";
2020
import { SWIFT_LAUNCH_CONFIG_TYPE } from "@src/debugger/debugAdapter";
21-
import { makeDebugConfigurations } from "@src/debugger/launch";
21+
import { makeDebugConfigurations, swiftPrelaunchBuildTaskArguments } from "@src/debugger/launch";
2222

2323
import {
2424
MockedObject,
@@ -301,3 +301,151 @@ suite("Launch Configurations Test", () => {
301301
expect(mockLaunchWSConfig.update).to.not.have.been.called;
302302
});
303303
});
304+
305+
suite("Swift PreLaunch Build Task Arguments Test", () => {
306+
const mockTasks = mockGlobalObject(vscode, "tasks");
307+
308+
setup(() => {
309+
// Reset mocks before each test
310+
mockTasks.fetchTasks.reset();
311+
});
312+
313+
test("swiftPrelaunchBuildTaskArguments returns task args for Swift build task", async () => {
314+
const expectedArgs = ["build", "--product", "executable", "--build-system"];
315+
const mockTask = mockObject<vscode.Task>({
316+
name: "swift: Build Debug executable",
317+
definition: {
318+
type: "swift",
319+
args: expectedArgs,
320+
},
321+
scope: vscode.TaskScope.Workspace,
322+
source: "swift",
323+
isBackground: false,
324+
presentationOptions: {},
325+
problemMatchers: [],
326+
runOptions: {},
327+
});
328+
329+
mockTasks.fetchTasks.resolves([instance(mockTask)]);
330+
331+
const launchConfig: vscode.DebugConfiguration = {
332+
type: "swift",
333+
request: "launch",
334+
name: "Debug executable",
335+
preLaunchTask: "swift: Build Debug executable",
336+
};
337+
338+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
339+
expect(result).to.deep.equal(expectedArgs);
340+
});
341+
342+
test("swiftPrelaunchBuildTaskArguments returns undefined for non-Swift task", async () => {
343+
const mockTask = mockObject<vscode.Task>({
344+
name: "npm: build",
345+
definition: {
346+
type: "npm",
347+
args: ["run", "build"],
348+
},
349+
scope: vscode.TaskScope.Workspace,
350+
source: "npm",
351+
isBackground: false,
352+
presentationOptions: {},
353+
problemMatchers: [],
354+
runOptions: {},
355+
});
356+
357+
mockTasks.fetchTasks.resolves([instance(mockTask)]);
358+
359+
const launchConfig: vscode.DebugConfiguration = {
360+
type: "swift",
361+
request: "launch",
362+
name: "Debug executable",
363+
preLaunchTask: "npm: build",
364+
};
365+
366+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
367+
expect(result).to.be.undefined;
368+
});
369+
370+
test("swiftPrelaunchBuildTaskArguments returns undefined for Swift task without build arg", async () => {
371+
const mockTask = mockObject<vscode.Task>({
372+
name: "swift: Test",
373+
definition: {
374+
type: "swift",
375+
args: ["test", "--build-system"],
376+
},
377+
scope: vscode.TaskScope.Workspace,
378+
source: "swift",
379+
isBackground: false,
380+
presentationOptions: {},
381+
problemMatchers: [],
382+
runOptions: {},
383+
});
384+
385+
mockTasks.fetchTasks.resolves([instance(mockTask)]);
386+
387+
const launchConfig: vscode.DebugConfiguration = {
388+
type: "swift",
389+
request: "launch",
390+
name: "Debug executable",
391+
preLaunchTask: "swift: Test",
392+
};
393+
394+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
395+
expect(result).to.be.undefined;
396+
});
397+
398+
test("swiftPrelaunchBuildTaskArguments returns undefined for launch config without preLaunchTask", async () => {
399+
const launchConfig: vscode.DebugConfiguration = {
400+
type: "swift",
401+
request: "launch",
402+
name: "Debug executable",
403+
};
404+
405+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
406+
expect(result).to.be.undefined;
407+
});
408+
409+
test("swiftPrelaunchBuildTaskArguments handles errors gracefully", async () => {
410+
mockTasks.fetchTasks.rejects(new Error("Failed to fetch tasks"));
411+
412+
const launchConfig: vscode.DebugConfiguration = {
413+
type: "swift",
414+
request: "launch",
415+
name: "Debug executable",
416+
preLaunchTask: "swift: Build Debug executable",
417+
};
418+
419+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
420+
expect(result).to.be.undefined;
421+
});
422+
423+
test("swiftPrelaunchBuildTaskArguments handles task name variations", async () => {
424+
const expectedArgs = ["build", "--product", "executable", "--build-system"];
425+
const mockTask = mockObject<vscode.Task>({
426+
name: "Build Debug executable",
427+
definition: {
428+
type: "swift",
429+
args: expectedArgs,
430+
},
431+
scope: vscode.TaskScope.Workspace,
432+
source: "swift",
433+
isBackground: false,
434+
presentationOptions: {},
435+
problemMatchers: [],
436+
runOptions: {},
437+
});
438+
439+
mockTasks.fetchTasks.resolves([instance(mockTask)]);
440+
441+
const launchConfig: vscode.DebugConfiguration = {
442+
type: "swift",
443+
request: "launch",
444+
name: "Debug executable",
445+
preLaunchTask: "swift: Build Debug executable",
446+
};
447+
448+
const result = await swiftPrelaunchBuildTaskArguments(launchConfig);
449+
expect(result).to.deep.equal(expectedArgs);
450+
});
451+
});

0 commit comments

Comments
 (0)