Skip to content

Commit 2a3ba9f

Browse files
authored
Use swift build --show-bin-path to compute paths (#1831)
* Use `swift build --show-bin-path` to compute paths Several paths are hard coded to use the `debug` folder inside `.build`. While this works for the native build system, the swiftbuild build system stores build artifacts in a different set of folders. Use `--show-bin-path` to determine where `swift build` is going to put the binaries and rely on that instead of hard coding. Ensure that any additional user arguments that affect the bin path are passed to the command so the true bin path is calculated. Issue: #1830
1 parent 025d33a commit 2a3ba9f

File tree

6 files changed

+407
-54
lines changed

6 files changed

+407
-54
lines changed

src/SwiftSnippets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export async function debugSnippetWithOptions(
9898
},
9999
folderContext.toolchain
100100
);
101-
const snippetDebugConfig = createSnippetConfiguration(snippetName, folderContext);
101+
const snippetDebugConfig = await createSnippetConfiguration(snippetName, folderContext);
102102
try {
103103
ctx.buildStarted(snippetName, snippetDebugConfig, options);
104104

src/WorkspaceContext.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { DocCDocumentationRequest, ReIndexProjectRequest } from "./sourcekit-lsp
3232
import { SwiftPluginTaskProvider } from "./tasks/SwiftPluginTaskProvider";
3333
import { SwiftTaskProvider } from "./tasks/SwiftTaskProvider";
3434
import { TaskManager } from "./tasks/TaskManager";
35+
import { BuildFlags } from "./toolchain/BuildFlags";
3536
import { SwiftToolchain } from "./toolchain/toolchain";
3637
import { StatusItem } from "./ui/StatusItem";
3738
import { SwiftBuildStatus } from "./ui/SwiftBuildStatus";
@@ -103,6 +104,17 @@ export class WorkspaceContext implements vscode.Disposable {
103104
this.commentCompletionProvider = new CommentCompletionProviders();
104105

105106
const onChangeConfig = vscode.workspace.onDidChangeConfiguration(async event => {
107+
// Clear build path cache when build-related configurations change
108+
if (
109+
event.affectsConfiguration("swift.buildArguments") ||
110+
event.affectsConfiguration("swift.buildPath") ||
111+
event.affectsConfiguration("swift.sdk") ||
112+
event.affectsConfiguration("swift.swiftSDK")
113+
) {
114+
// Clear the build path cache since configuration affects paths
115+
BuildFlags.clearBuildPathCache();
116+
}
117+
106118
// on runtime path config change, regenerate launch.json
107119
if (event.affectsConfiguration("swift.runtimePath")) {
108120
if (!(await this.needToAutoGenerateLaunchConfig())) {
@@ -122,14 +134,20 @@ export class WorkspaceContext implements vscode.Disposable {
122134
}
123135
});
124136
}
125-
// on change of swift build path, regenerate launch.json
126-
if (event.affectsConfiguration("swift.buildPath")) {
137+
// on change of swift build path or build arguments, regenerate launch.json
138+
if (
139+
event.affectsConfiguration("swift.buildPath") ||
140+
event.affectsConfiguration("swift.buildArguments")
141+
) {
127142
if (!(await this.needToAutoGenerateLaunchConfig())) {
128143
return;
129144
}
145+
const configType = event.affectsConfiguration("swift.buildPath")
146+
? "build path"
147+
: "build arguments";
130148
void vscode.window
131149
.showInformationMessage(
132-
`Launch configurations need to be updated after changing the Swift build path. Do you want to update?`,
150+
`Launch configurations need to be updated after changing the Swift ${configType}. Do you want to update?`,
133151
"Update",
134152
"Cancel"
135153
)

src/commands/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export async function debugBuildWithOptions(
107107
return;
108108
}
109109

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

src/debugger/launch.ts

Lines changed: 132 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -121,26 +121,44 @@ export async function makeDebugConfigurations(
121121
}
122122

123123
// Return debug launch configuration for an executable in the given folder
124-
export function getLaunchConfiguration(
124+
export async function getLaunchConfiguration(
125125
target: string,
126126
folderCtx: FolderContext
127-
): vscode.DebugConfiguration | undefined {
127+
): Promise<vscode.DebugConfiguration | undefined> {
128128
const wsLaunchSection = vscode.workspace.workspaceFile
129129
? vscode.workspace.getConfiguration("launch")
130130
: vscode.workspace.getConfiguration("launch", folderCtx.workspaceFolder);
131131
const launchConfigs = wsLaunchSection.get<vscode.DebugConfiguration[]>("configurations") || [];
132132
const { folder } = getFolderAndNameSuffix(folderCtx);
133-
const targetPath = path.join(
134-
BuildFlags.buildDirectoryFromWorkspacePath(folder, true),
135-
"debug",
136-
target
137-
);
138-
// Users could be on different platforms with different path annotations,
139-
// so normalize before we compare.
140-
const launchConfig = launchConfigs.find(
141-
config => path.normalize(config.program) === path.normalize(targetPath)
142-
);
143-
return launchConfig;
133+
134+
try {
135+
// Use dynamic path resolution with --show-bin-path
136+
const binPath = await folderCtx.toolchain.buildFlags.getBuildBinaryPath(
137+
folderCtx.folder.fsPath,
138+
folder,
139+
"debug",
140+
folderCtx.workspaceContext.logger
141+
);
142+
const targetPath = path.join(binPath, target);
143+
144+
// Users could be on different platforms with different path annotations,
145+
// so normalize before we compare.
146+
const launchConfig = launchConfigs.find(
147+
config => path.normalize(config.program) === path.normalize(targetPath)
148+
);
149+
return launchConfig;
150+
} catch (error) {
151+
// Fallback to traditional path construction if dynamic resolution fails
152+
const targetPath = path.join(
153+
BuildFlags.buildDirectoryFromWorkspacePath(folder, true),
154+
"debug",
155+
target
156+
);
157+
const launchConfig = launchConfigs.find(
158+
config => path.normalize(config.program) === path.normalize(targetPath)
159+
);
160+
return launchConfig;
161+
}
144162
}
145163

146164
// Return array of DebugConfigurations for executables based on what is in Package.swift
@@ -152,30 +170,73 @@ async function createExecutableConfigurations(
152170
// Windows understand the forward slashes, so make the configuration unified as posix path
153171
// to make it easier for users switching between platforms.
154172
const { folder, nameSuffix } = getFolderAndNameSuffix(ctx, undefined, "posix");
155-
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix");
156173

157-
return executableProducts.flatMap(product => {
158-
const baseConfig = {
159-
type: SWIFT_LAUNCH_CONFIG_TYPE,
160-
request: "launch",
161-
args: [],
162-
cwd: folder,
163-
};
164-
return [
165-
{
166-
...baseConfig,
167-
name: `Debug ${product.name}${nameSuffix}`,
168-
program: path.posix.join(buildDirectory, "debug", product.name),
169-
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
170-
},
171-
{
172-
...baseConfig,
173-
name: `Release ${product.name}${nameSuffix}`,
174-
program: path.posix.join(buildDirectory, "release", product.name),
175-
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
176-
},
177-
];
178-
});
174+
try {
175+
// Get dynamic build paths for both debug and release configurations
176+
const [debugBinPath, releaseBinPath] = await Promise.all([
177+
ctx.toolchain.buildFlags.getBuildBinaryPath(
178+
ctx.folder.fsPath,
179+
folder,
180+
"debug",
181+
ctx.workspaceContext.logger
182+
),
183+
ctx.toolchain.buildFlags.getBuildBinaryPath(
184+
ctx.folder.fsPath,
185+
folder,
186+
"release",
187+
ctx.workspaceContext.logger
188+
),
189+
]);
190+
191+
return executableProducts.flatMap(product => {
192+
const baseConfig = {
193+
type: SWIFT_LAUNCH_CONFIG_TYPE,
194+
request: "launch",
195+
args: [],
196+
cwd: folder,
197+
};
198+
return [
199+
{
200+
...baseConfig,
201+
name: `Debug ${product.name}${nameSuffix}`,
202+
program: path.posix.join(debugBinPath, product.name),
203+
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
204+
},
205+
{
206+
...baseConfig,
207+
name: `Release ${product.name}${nameSuffix}`,
208+
program: path.posix.join(releaseBinPath, product.name),
209+
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
210+
},
211+
];
212+
});
213+
} catch (error) {
214+
// Fallback to traditional path construction if dynamic resolution fails
215+
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true, "posix");
216+
217+
return executableProducts.flatMap(product => {
218+
const baseConfig = {
219+
type: SWIFT_LAUNCH_CONFIG_TYPE,
220+
request: "launch",
221+
args: [],
222+
cwd: folder,
223+
};
224+
return [
225+
{
226+
...baseConfig,
227+
name: `Debug ${product.name}${nameSuffix}`,
228+
program: path.posix.join(buildDirectory, "debug", product.name),
229+
preLaunchTask: `swift: Build Debug ${product.name}${nameSuffix}`,
230+
},
231+
{
232+
...baseConfig,
233+
name: `Release ${product.name}${nameSuffix}`,
234+
program: path.posix.join(buildDirectory, "release", product.name),
235+
preLaunchTask: `swift: Build Release ${product.name}${nameSuffix}`,
236+
},
237+
];
238+
});
239+
}
179240
}
180241

181242
/**
@@ -184,22 +245,44 @@ async function createExecutableConfigurations(
184245
* @param ctx Folder context for project
185246
* @returns Debug configuration for running Swift Snippet
186247
*/
187-
export function createSnippetConfiguration(
248+
export async function createSnippetConfiguration(
188249
snippetName: string,
189250
ctx: FolderContext
190-
): vscode.DebugConfiguration {
251+
): Promise<vscode.DebugConfiguration> {
191252
const { folder } = getFolderAndNameSuffix(ctx);
192-
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true);
193-
194-
return {
195-
type: SWIFT_LAUNCH_CONFIG_TYPE,
196-
request: "launch",
197-
name: `Run ${snippetName}`,
198-
program: path.posix.join(buildDirectory, "debug", snippetName),
199-
args: [],
200-
cwd: folder,
201-
runType: "snippet",
202-
};
253+
254+
try {
255+
// Use dynamic path resolution with --show-bin-path
256+
const binPath = await ctx.toolchain.buildFlags.getBuildBinaryPath(
257+
ctx.folder.fsPath,
258+
folder,
259+
"debug",
260+
ctx.workspaceContext.logger
261+
);
262+
263+
return {
264+
type: SWIFT_LAUNCH_CONFIG_TYPE,
265+
request: "launch",
266+
name: `Run ${snippetName}`,
267+
program: path.posix.join(binPath, snippetName),
268+
args: [],
269+
cwd: folder,
270+
runType: "snippet",
271+
};
272+
} catch (error) {
273+
// Fallback to traditional path construction if dynamic resolution fails
274+
const buildDirectory = BuildFlags.buildDirectoryFromWorkspacePath(folder, true);
275+
276+
return {
277+
type: SWIFT_LAUNCH_CONFIG_TYPE,
278+
request: "launch",
279+
name: `Run ${snippetName}`,
280+
program: path.posix.join(buildDirectory, "debug", snippetName),
281+
args: [],
282+
cwd: folder,
283+
runType: "snippet",
284+
};
285+
}
203286
}
204287

205288
/**

src/toolchain/BuildFlags.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import * as path from "path";
1515

1616
import configuration from "../configuration";
17+
import { SwiftLogger } from "../logging/SwiftLogger";
18+
import { execSwift } from "../utilities/utilities";
1719
import { Version } from "../utilities/version";
1820
import { DarwinCompatibleTarget, SwiftToolchain, getDarwinTargetTriple } from "./toolchain";
1921

@@ -30,6 +32,8 @@ export interface ArgumentFilter {
3032
}
3133

3234
export class BuildFlags {
35+
private static buildPathCache = new Map<string, string>();
36+
3337
constructor(public toolchain: SwiftToolchain) {}
3438

3539
/**
@@ -221,6 +225,70 @@ export class BuildFlags {
221225
}
222226
}
223227

228+
/**
229+
* Get the build binary path using swift build --show-bin-path.
230+
* This respects all build configuration including buildArguments, buildSystem, etc.
231+
*
232+
* @param workspacePath Path to the workspace
233+
* @param configuration Build configuration (debug or release)
234+
* @returns Promise resolving to the build binary path
235+
*/
236+
async getBuildBinaryPath(
237+
cwd: string,
238+
workspacePath: string,
239+
buildConfiguration: "debug" | "release" = "debug",
240+
logger: SwiftLogger
241+
): Promise<string> {
242+
// Checking the bin path requires a swift process execution, so we maintain a cache.
243+
// The cache key is based on workspace, configuration, and build arguments.
244+
const buildArgsHash = JSON.stringify(configuration.buildArguments);
245+
const cacheKey = `${workspacePath}:${buildConfiguration}:${buildArgsHash}`;
246+
247+
if (BuildFlags.buildPathCache.has(cacheKey)) {
248+
return BuildFlags.buildPathCache.get(cacheKey)!;
249+
}
250+
251+
// Filters down build arguments to those affecting the bin path
252+
const binPathAffectingArgs = (args: string[]) =>
253+
BuildFlags.filterArguments(args, [
254+
{ argument: "--scratch-path", include: 1 },
255+
{ argument: "--build-system", include: 1 },
256+
]);
257+
258+
const baseArgs = ["build", "--show-bin-path", "--configuration", buildConfiguration];
259+
const fullArgs = [
260+
...this.withAdditionalFlags(baseArgs),
261+
...binPathAffectingArgs(configuration.buildArguments),
262+
];
263+
264+
try {
265+
// Execute swift build --show-bin-path
266+
const result = await execSwift(fullArgs, this.toolchain, { cwd });
267+
const binPath = result.stdout.trim();
268+
269+
// Cache the result
270+
BuildFlags.buildPathCache.set(cacheKey, binPath);
271+
return binPath;
272+
} catch (error) {
273+
logger.warn(
274+
`Failed to get build binary path using 'swift ${fullArgs.join(" ")}. Falling back to traditional path construction. error: ${error}`
275+
);
276+
// Fallback to traditional path construction if command fails
277+
const fallbackPath = path.join(
278+
BuildFlags.buildDirectoryFromWorkspacePath(workspacePath, true),
279+
buildConfiguration
280+
);
281+
return fallbackPath;
282+
}
283+
}
284+
285+
/**
286+
* Clear the build path cache. Should be called when build configuration changes.
287+
*/
288+
static clearBuildPathCache(): void {
289+
BuildFlags.buildPathCache.clear();
290+
}
291+
224292
withAdditionalFlags(args: string[]): string[] {
225293
return this.withSwiftPackageFlags(
226294
this.withDisableSandboxFlags(this.withSwiftSDKFlags(args))

0 commit comments

Comments
 (0)