Skip to content

Fix Several Windows Debugging Issues #1083

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/debugger/buildConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,19 @@ export class TestingConfigurationFactory {
...swiftRuntimeEnv(),
...configuration.folder(this.ctx.workspaceFolder).testEnvironmentVariables,
};
// On Windows, add XCTest.dll to the Path
// On Windows, add XCTest.dll/Testing.dll to the Path
// and run the .xctest executable from the .build directory.
const runtimePath = this.ctx.workspaceContext.toolchain.runtimePath;
const xcTestPath = this.ctx.workspaceContext.toolchain.xcTestPath;
if (xcTestPath && xcTestPath !== runtimePath) {
testEnv.Path = `${xcTestPath};${testEnv.Path ?? process.env.Path}`;
}

const swiftTestingPath = this.ctx.workspaceContext.toolchain.swiftTestingPath;
if (swiftTestingPath && swiftTestingPath !== runtimePath) {
testEnv.Path = `${swiftTestingPath};${testEnv.Path ?? process.env.Path}`;
}

return {
...this.baseConfig,
program: await this.testExecutableOutputPath(),
Expand Down Expand Up @@ -507,7 +512,9 @@ export class TestingConfigurationFactory {
}

private get artifactFolderForTestKind(): string {
return isRelease(this.testKind) ? "release" : "debug";
const mode = isRelease(this.testKind) ? "release" : "debug";
const triple = this.ctx.workspaceContext.toolchain.unversionedTriple;
return triple ? path.join(triple, mode) : mode;
}

private xcTestOutputPath(): string {
Expand Down
7 changes: 6 additions & 1 deletion src/debugger/debugAdapterFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,14 @@ export class LLDBDebugConfigurationProvider implements vscode.DebugConfiguration
): Promise<vscode.DebugConfiguration> {
launchConfig.env = this.convertEnvironmentVariables(launchConfig.env);
// Fix the program path on Windows to include the ".exe" extension
if (this.platform === "win32" && path.extname(launchConfig.program) !== ".exe") {
if (
this.platform === "win32" &&
launchConfig.testType === undefined &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works for now, but it would be nice to have a more "universal" way to determine if the .exe suffix needs to be added. I didn't anticipate this use case when I added this code. It could definitely use some work.

Perhaps we can have the auto-generated launch configurations use a target parameter instead of program, for example. That way the launch config can be used across all platforms while retaining the ability to target a specific executable for the tests.

I'm going to open up a separate issue from this since this needs to go in to unblock test debugging on Windows.

path.extname(launchConfig.program) !== ".exe"
) {
launchConfig.program += ".exe";
}

// Delegate to CodeLLDB if that's the debug adapter we have selected
if (DebugAdapter.getDebugAdapterType(this.swiftVersion) === "lldb-vscode") {
launchConfig.type = "lldb";
Expand Down
208 changes: 132 additions & 76 deletions src/toolchain/toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Sanitizer } from "./Sanitizer";
interface InfoPlist {
DefaultProperties: {
XCTEST_VERSION: string | undefined;
SWIFT_TESTING_VERSION: string | undefined;
};
}

Expand All @@ -50,6 +51,7 @@ interface SwiftTargetInfo {
compilerVersion: string;
target?: {
triple: string;
unversionedTriple: string;
[name: string]: string | string[];
};
paths: {
Expand Down Expand Up @@ -97,18 +99,22 @@ export function getDarwinTargetTriple(target: DarwinCompatibleTarget): string |
}

export class SwiftToolchain {
public swiftVersionString: string;

constructor(
public swiftFolderPath: string, // folder swift executable in $PATH was found in
public toolchainPath: string, // toolchain folder. One folder up from swift bin folder. This is to support toolchains without usr folder
public swiftVersionString: string, // Swift version as a string, including description
private targetInfo: SwiftTargetInfo,
public swiftVersion: Version, // Swift version as semVar variable
public runtimePath?: string, // runtime library included in output from `swift -print-target-info`
private defaultTarget?: string,
public defaultSDK?: string,
public customSDK?: string,
public xcTestPath?: string,
public swiftTestingPath?: string,
public swiftPMTestingHelperPath?: string
) {}
) {
this.swiftVersionString = targetInfo.compilerVersion;
}

static async create(): Promise<SwiftToolchain> {
const swiftFolderPath = await this.getSwiftFolderPath();
Expand All @@ -125,22 +131,32 @@ export class SwiftToolchain {
runtimePath,
customSDK ?? defaultSDK
);
const swiftTestingPath = await this.getSwiftTestingPath(
targetInfo,
swiftVersion,
runtimePath,
customSDK ?? defaultSDK
);
const swiftPMTestingHelperPath = await this.getSwiftPMTestingHelperPath(toolchainPath);

return new SwiftToolchain(
swiftFolderPath,
toolchainPath,
targetInfo.compilerVersion,
targetInfo,
swiftVersion,
runtimePath,
targetInfo.target?.triple,
defaultSDK,
customSDK,
xcTestPath,
swiftTestingPath,
swiftPMTestingHelperPath
);
}

public get unversionedTriple(): string | undefined {
return this.targetInfo.target?.unversionedTriple;
}

/** build flags */
public get buildFlags(): BuildFlags {
return new BuildFlags(this);
Expand Down Expand Up @@ -445,8 +461,8 @@ export class SwiftToolchain {
if (this.runtimePath) {
str += `\nRuntime Library Path: ${this.runtimePath}`;
}
if (this.defaultTarget) {
str += `\nDefault Target: ${this.defaultTarget}`;
if (this.targetInfo.target?.triple) {
str += `\nDefault Target: ${this.targetInfo.target?.triple}`;
}
if (this.defaultSDK) {
str += `\nDefault SDK: ${this.defaultSDK}`;
Expand Down Expand Up @@ -638,6 +654,31 @@ export class SwiftToolchain {
return undefined;
}

/**
* @param targetInfo swift target info
* @param swiftVersion parsed swift version
* @param runtimePath path to Swift runtime
* @param sdkroot path to swift SDK
* @returns path to folder where xctest can be found
*/
private static async getSwiftTestingPath(
targetInfo: SwiftTargetInfo,
swiftVersion: Version,
runtimePath: string | undefined,
sdkroot: string | undefined
): Promise<string | undefined> {
if (process.platform !== "win32") {
return undefined;
}
return this.getWindowsPlatformDLLPath(
"Testing",
targetInfo,
swiftVersion,
runtimePath,
sdkroot
);
}

/**
* @param targetInfo swift target info
* @param swiftVersion parsed swift version
Expand All @@ -663,80 +704,95 @@ export class SwiftToolchain {
return path.join(developerDir, "usr", "bin");
}
case "win32": {
// look up runtime library directory for XCTest alternatively
const fallbackPath =
runtimePath !== undefined &&
(await pathExists(path.join(runtimePath, "XCTest.dll")))
? runtimePath
: undefined;
if (!sdkroot) {
return fallbackPath;
}
const platformPath = path.dirname(path.dirname(path.dirname(sdkroot)));
const platformManifest = path.join(platformPath, "Info.plist");
if ((await pathExists(platformManifest)) !== true) {
if (fallbackPath) {
return fallbackPath;
}
vscode.window.showWarningMessage(
"XCTest not found due to non-standardized library layout. Tests explorer won't work as expected."
);
return undefined;
}
const data = await fs.readFile(platformManifest, "utf8");
let infoPlist;
try {
infoPlist = plist.parse(data) as unknown as InfoPlist;
} catch (error) {
vscode.window.showWarningMessage(
`Unable to parse ${platformManifest}: ${error}`
);
return undefined;
}
const version = infoPlist.DefaultProperties.XCTEST_VERSION;
if (!version) {
throw Error("Info.plist is missing the XCTEST_VERSION key.");
}

if (swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0))) {
let bindir: string;
const arch = targetInfo.target?.triple.split("-", 1)[0];
switch (arch) {
case "x86_64":
bindir = "bin64";
break;
case "i686":
bindir = "bin32";
break;
case "aarch64":
bindir = "bin64a";
break;
default:
throw Error(`unsupported architecture ${arch}`);
}
return path.join(
platformPath,
"Developer",
"Library",
`XCTest-${version}`,
"usr",
bindir
);
} else {
return path.join(
platformPath,
"Developer",
"Library",
`XCTest-${version}`,
"usr",
"bin"
);
}
return await this.getWindowsPlatformDLLPath(
"XCTest",
targetInfo,
swiftVersion,
runtimePath,
sdkroot
);
}
}
return undefined;
}

private static async getWindowsPlatformDLLPath(
type: "XCTest" | "Testing",
targetInfo: SwiftTargetInfo,
swiftVersion: Version,
runtimePath: string | undefined,
sdkroot: string | undefined
): Promise<string | undefined> {
// look up runtime library directory for XCTest/Testing alternatively
const fallbackPath =
runtimePath !== undefined && (await pathExists(path.join(runtimePath, `${type}.dll`)))
? runtimePath
: undefined;
if (!sdkroot) {
return fallbackPath;
}

const platformPath = path.dirname(path.dirname(path.dirname(sdkroot)));
const platformManifest = path.join(platformPath, "Info.plist");
if ((await pathExists(platformManifest)) !== true) {
if (fallbackPath) {
return fallbackPath;
}
vscode.window.showWarningMessage(
`${type} not found due to non-standardized library layout. Tests explorer won't work as expected.`
);
return undefined;
}
const data = await fs.readFile(platformManifest, "utf8");
let infoPlist;
try {
infoPlist = plist.parse(data) as unknown as InfoPlist;
} catch (error) {
vscode.window.showWarningMessage(`Unable to parse ${platformManifest}: ${error}`);
return undefined;
}
const plistKey = type === "XCTest" ? "XCTEST_VERSION" : "SWIFT_TESTING_VERSION";
const version = infoPlist.DefaultProperties[plistKey];
if (!version) {
throw Error(`Info.plist is missing the ${plistKey} key.`);
}

if (swiftVersion.isGreaterThanOrEqual(new Version(5, 7, 0))) {
let bindir: string;
const arch = targetInfo.target?.triple.split("-", 1)[0];
switch (arch) {
case "x86_64":
bindir = "bin64";
break;
case "i686":
bindir = "bin32";
break;
case "aarch64":
bindir = "bin64a";
break;
default:
throw Error(`unsupported architecture ${arch}`);
}
return path.join(
platformPath,
"Developer",
"Library",
`${type}-${version}`,
"usr",
bindir
);
} else {
return path.join(
platformPath,
"Developer",
"Library",
`${type}-${version}`,
"usr",
"bin"
);
}
}

/** @returns swift target info */
private static async getSwiftTargetInfo(): Promise<SwiftTargetInfo> {
try {
Expand Down
7 changes: 6 additions & 1 deletion test/suite/tasks/SwiftTaskProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ suite("SwiftTaskProvider Test Suite", () => {
new SwiftToolchain(
"/invalid/swift/path",
"/invalid/toolchain/path",
"1.2.3",
{
compilerVersion: "1.2.3",
paths: {
runtimeLibraryPaths: [],
},
},
new Version(1, 2, 3)
)
);
Expand Down