Skip to content

Commit ede165c

Browse files
[windows] add a check for a matching python architecture
1 parent 03f4816 commit ede165c

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,14 @@ public struct Driver {
864864
if case .subcommand = try Self.invocationRunMode(forArgs: args).mode {
865865
throw Error.subcommandPassedToDriver
866866
}
867+
868+
#if os(Windows)
869+
if args[1] == "-repl" { // a `-` gets automatically added to `swift repl`.
870+
try checkIfMatchingPythonArch(
871+
cwd: ProcessEnv.cwd, env: env, diagnosticsEngine: diagnosticsEngine)
872+
}
873+
#endif
874+
867875
var args = args
868876
if let additional = env["ADDITIONAL_SWIFT_DRIVER_FLAGS"] {
869877
args.append(contentsOf: additional.components(separatedBy: " "))
@@ -955,7 +963,7 @@ public struct Driver {
955963
negative: .disableIncrementalFileHashing,
956964
default: false)
957965
self.recordedInputMetadata = .init(uniqueKeysWithValues:
958-
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
966+
Set(inputFiles).compactMap { inputFile -> (TypedVirtualPath, FileMetadata)? in
959967
guard let modTime = try? fileSystem.lastModificationTime(for: inputFile.file) else { return nil }
960968
if incrementalFileHashes {
961969
guard let data = try? fileSystem.readFileContents(inputFile.file) else { return nil }
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import Foundation
2+
3+
import struct TSCBasic.AbsolutePath
4+
import struct TSCBasic.ByteString
5+
import struct TSCBasic.Diagnostic
6+
import protocol TSCBasic.DiagnosticData
7+
import class TSCBasic.DiagnosticsEngine
8+
import protocol TSCBasic.FileSystem
9+
import protocol TSCBasic.OutputByteStream
10+
import func TSCBasic.getEnvSearchPaths
11+
import func TSCBasic.lookupExecutablePath
12+
13+
/// Check that the architecture of the toolchain matches the architecture
14+
/// of the Python installation.
15+
///
16+
/// When installing the x86 toolchain on ARM64 Windows, if the user does not
17+
/// install an x86 version of Python, they will get acryptic error message
18+
/// when running lldb (`0xC000007B`). Calling this function before invoking
19+
/// lldb gives them a warning to help troublshoot the issue.
20+
///
21+
/// - Parameters:
22+
/// - cwd: The current working directory.
23+
/// - env: A dictionary of the environment variables and their values. Usually of the parent shell.
24+
/// - diagnosticsEngine: DiagnosticsEngine instance to use for printing the warning.
25+
public func checkIfMatchingPythonArch(
26+
cwd: AbsolutePath?, env: [String: String], diagnosticsEngine: DiagnosticsEngine
27+
) throws {
28+
#if arch(arm64)
29+
let toolchainArchitecture = COFFBinaryExecutableArchitecture.arm64
30+
#elseif arch(x86_64)
31+
let toolchainArchitecture = COFFBinaryExecutableArchitecture.x64
32+
#elseif arch(x86)
33+
let toolchainArchitecture = COFFBinaryExecutableArchitecture.x86
34+
#else
35+
return
36+
#endif
37+
let pythonArchitecture = COFFBinaryExecutableArchitecture.readWindowsExecutableArchitecture(
38+
cwd: cwd, env: env, filename: "python.exe")
39+
40+
if toolchainArchitecture != pythonArchitecture {
41+
diagnosticsEngine.emit(
42+
.warning(
43+
"""
44+
There is an architecture mismatch between the installed toolchain and the resolved Python's architecture:
45+
Toolchain: \(toolchainArchitecture)
46+
Python: \(pythonArchitecture)
47+
"""))
48+
}
49+
}
50+
51+
/// Some of the architectures that can be stored in a COFF header.
52+
enum COFFBinaryExecutableArchitecture: String {
53+
case x86 = "X86"
54+
case x64 = "X64"
55+
case arm64 = "ARM64"
56+
case unknown = "Unknown"
57+
58+
static func fromPEMachineByte(machine: UInt16) -> Self {
59+
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
60+
switch machine {
61+
case 0x014c: return .x86
62+
case 0x8664: return .x64
63+
case 0xAA64: return .arm64
64+
default: return .unknown
65+
}
66+
}
67+
68+
/// Resolves the filename from the `Path` environment variable and read its COFF header to determine the architecture
69+
/// of the binary.
70+
///
71+
/// - Parameters:
72+
/// - cwd: The current working directory.
73+
/// - env: A dictionary of the environment variables and their values. Usually of the parent shell.
74+
/// - filename: The name of the file we are resolving the architecture of.
75+
/// - Returns: The architecture of the file which was found in the `Path`.
76+
static func readWindowsExecutableArchitecture(
77+
cwd: AbsolutePath?, env: [String: String], filename: String
78+
) -> Self {
79+
let searchPaths = getEnvSearchPaths(pathString: env["Path"], currentWorkingDirectory: cwd)
80+
guard let filePath = lookupExecutablePath(
81+
filename: filename, currentWorkingDirectory: cwd, searchPaths: searchPaths)
82+
else {
83+
return .unknown
84+
}
85+
guard let fileHandle = FileHandle(forReadingAtPath: filePath.pathString) else {
86+
return .unknown
87+
}
88+
89+
defer { fileHandle.closeFile() }
90+
91+
// Infering the architecture of a Windows executable from its COFF header involves the following:
92+
// 1. Get the COFF header offset from the pointer located at the 0x3C offset (4 bytes long).
93+
// 2. Jump to that offset and read the next 6 bytes.
94+
// 3. The first 4 are the signature which should be equal to 0x50450000.
95+
// 4. The last 2 are the machine architecture which can be infered from the value we get.
96+
//
97+
// The link below provides a visualization of the COFF header and the process to get to it.
98+
// https://upload.wikimedia.org/wikipedia/commons/1/1b/Portable_Executable_32_bit_Structure_in_SVG_fixed.svg
99+
fileHandle.seek(toFileOffset: 0x3C)
100+
guard let offsetPointer = try? fileHandle.read(upToCount: 4),
101+
offsetPointer.count == 4
102+
else {
103+
return .unknown
104+
}
105+
106+
let peHeaderOffset = offsetPointer.withUnsafeBytes { $0.load(as: UInt32.self) }
107+
108+
fileHandle.seek(toFileOffset: UInt64(peHeaderOffset))
109+
guard let coffHeader = try? fileHandle.read(upToCount: 6), coffHeader.count == 6 else {
110+
return .unknown
111+
}
112+
113+
let signature = coffHeader.prefix(4)
114+
let machineBytes = coffHeader.suffix(2)
115+
116+
guard signature == Data([0x50, 0x45, 0x00, 0x00]) else {
117+
return .unknown
118+
}
119+
120+
return .fromPEMachineByte(machine: machineBytes.withUnsafeBytes { $0.load(as: UInt16.self) })
121+
}
122+
}

0 commit comments

Comments
 (0)