Skip to content

Commit a0d7658

Browse files
Merge pull request #1942 from charles-zablit/charles-zablit/windows/warn-architecture-mismatch-python
[windows] add a check for a matching python architecture
2 parents 35d84e3 + b8a065b commit a0d7658

File tree

3 files changed

+223
-0
lines changed

3 files changed

+223
-0
lines changed

Sources/SwiftDriver/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ add_library(SwiftDriver
117117
Utilities/FileMetadata.swift
118118
Utilities/FileType.swift
119119
Utilities/PredictableRandomNumberGenerator.swift
120+
Utilities/PythonArchitecture.swift
120121
Utilities/RelativePathAdditions.swift
121122
Utilities/Sanitizer.swift
122123
Utilities/StringAdditions.swift

Sources/SwiftDriver/Driver/Driver.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,9 +875,23 @@ public struct Driver {
875875

876876
self.executor = executor
877877

878+
if args.count > 1 && args[1] == "-repl" {
879+
#if arch(arm64)
880+
checkIfMatchingPythonArch(
881+
cwd: ProcessEnv.cwd, envBlock: envBlock, toolchainArchitecture: .arm64, diagnosticsEngine: diagnosticsEngine)
882+
#elseif arch(x86_64)
883+
checkIfMatchingPythonArch(
884+
cwd: ProcessEnv.cwd, envBlock: envBlock, toolchainArchitecture: .x64, diagnosticsEngine: diagnosticsEngine)
885+
#elseif arch(x86)
886+
checkIfMatchingPythonArch(
887+
cwd: ProcessEnv.cwd, envBlock: envBlock, toolchainArchitecture: .x86, diagnosticsEngine: diagnosticsEngine)
888+
#endif
889+
}
890+
878891
if case .subcommand = try Self.invocationRunMode(forArgs: args).mode {
879892
throw Error.subcommandPassedToDriver
880893
}
894+
881895
var args = args
882896
if let additional = env["ADDITIONAL_SWIFT_DRIVER_FLAGS"] {
883897
args.append(contentsOf: additional.components(separatedBy: " "))
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
//===--------------- Driver.swift - Swift Driver --------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import Foundation
13+
14+
import struct TSCBasic.AbsolutePath
15+
import struct TSCBasic.ByteString
16+
import struct TSCBasic.Diagnostic
17+
import protocol TSCBasic.DiagnosticData
18+
import class TSCBasic.DiagnosticsEngine
19+
import protocol TSCBasic.FileSystem
20+
import protocol TSCBasic.OutputByteStream
21+
import typealias TSCBasic.ProcessEnvironmentBlock
22+
import func TSCBasic.getEnvSearchPaths
23+
import func TSCBasic.lookupExecutablePath
24+
25+
#if os(Windows)
26+
import WinSDK
27+
#endif
28+
29+
/// Check that the architecture of the toolchain matches the architecture
30+
/// of the Python installation.
31+
///
32+
/// When installing the x86 toolchain on ARM64 Windows, if the user does not
33+
/// install an x86 version of Python, they will get a cryptic error message
34+
/// when running lldb (`0xC000007B`). Calling this function before invoking
35+
/// lldb gives them a warning to help troubleshoot the issue.
36+
///
37+
/// - Parameters:
38+
/// - cwd: The current working directory.
39+
/// - env: The parent shell's ProcessEnvironmentBlock.
40+
/// - diagnosticsEngine: DiagnosticsEngine instance to use for printing the warning.
41+
public func checkIfMatchingPythonArch(
42+
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock,
43+
toolchainArchitecture: ExecutableArchitecture, diagnosticsEngine: DiagnosticsEngine
44+
) {
45+
#if os(Windows) || os(macOS)
46+
#if os(Windows)
47+
let pythonArchitecture = Process.readWindowsExecutableArchitecture(
48+
cwd: cwd, envBlock: envBlock, filename: "python.exe")
49+
#elseif os(macOS)
50+
let pythonArchitecture = Process.readDarwinExecutableArchitecture(
51+
cwd: cwd, envBlock: envBlock, filename: "python3")
52+
if pythonArchitecture == .universal {
53+
return
54+
}
55+
#endif
56+
57+
guard toolchainArchitecture == pythonArchitecture else {
58+
diagnosticsEngine.emit(
59+
.warning(
60+
"""
61+
There is an architecture mismatch between the installed toolchain and the resolved Python's architecture:
62+
Toolchain: \(toolchainArchitecture)
63+
Python: \(pythonArchitecture)
64+
"""))
65+
return
66+
}
67+
#endif
68+
}
69+
70+
/// Some of the architectures that can be stored in a COFF header.
71+
public enum ExecutableArchitecture: String {
72+
case x86 = "X86"
73+
case x64 = "X64"
74+
case arm64 = "ARM64"
75+
case universal = "Universal"
76+
case unknown = "Unknown"
77+
78+
#if os(Windows)
79+
static func fromPEMachineByte(machine: Int32) -> Self {
80+
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
81+
switch machine {
82+
case IMAGE_FILE_MACHINE_I386: return .x86
83+
case IMAGE_FILE_MACHINE_AMD64: return .x64
84+
case IMAGE_FILE_MACHINE_ARM64: return .arm64
85+
default: return .unknown
86+
}
87+
}
88+
#endif
89+
90+
#if os(macOS)
91+
static func fromMachoCPUType(cpuType: Int32) -> Self {
92+
// https://en.wikipedia.org/wiki/Mach-O
93+
switch cpuType {
94+
case 0x0100_0007: return .x86
95+
case 0x0100_000c: return .arm64
96+
default: return .unknown
97+
}
98+
}
99+
#endif
100+
}
101+
102+
extension Process {
103+
#if os(Windows)
104+
/// Resolves the filename from the `Path` environment variable and read its COFF header to determine the architecture
105+
/// of the binary.
106+
///
107+
/// - Parameters:
108+
/// - cwd: The current working directory.
109+
/// - env: A dictionary of the environment variables and their values. Usually of the parent shell.
110+
/// - filename: The name of the file we are resolving the architecture of.
111+
/// - Returns: The architecture of the file which was found in the `Path`.
112+
static func readWindowsExecutableArchitecture(
113+
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock, filename: String
114+
) -> ExecutableArchitecture {
115+
let searchPaths = getEnvSearchPaths(
116+
pathString: envBlock["Path"], currentWorkingDirectory: cwd)
117+
guard
118+
let filePath = lookupExecutablePath(
119+
filename: filename, currentWorkingDirectory: cwd, searchPaths: searchPaths)
120+
else {
121+
return .unknown
122+
}
123+
guard let fileHandle = FileHandle(forReadingAtPath: filePath.pathString) else {
124+
return .unknown
125+
}
126+
127+
defer { fileHandle.closeFile() }
128+
129+
// Infering the architecture of a Windows executable from its COFF header involves the following:
130+
// 1. Get the COFF header offset from the pointer located at the 0x3C offset (4 bytes long).
131+
// 2. Jump to that offset and read the next 6 bytes.
132+
// 3. The first 4 are the signature which should be equal to 0x50450000.
133+
// 4. The last 2 are the machine architecture which can be infered from the value we get.
134+
//
135+
// The link below provides a visualization of the COFF header and the process to get to it.
136+
// https://upload.wikimedia.org/wikipedia/commons/1/1b/Portable_Executable_32_bit_Structure_in_SVG_fixed.svg
137+
guard (try? fileHandle.seek(toOffset: 0x3C)) != nil else {
138+
return .unknown
139+
}
140+
guard let offsetPointer = try? fileHandle.read(upToCount: 4),
141+
offsetPointer.count == 4
142+
else {
143+
return .unknown
144+
}
145+
146+
let peHeaderOffset = offsetPointer.withUnsafeBytes { $0.load(as: UInt32.self) }
147+
148+
guard (try? fileHandle.seek(toOffset: UInt64(peHeaderOffset))) != nil else {
149+
return .unknown
150+
}
151+
guard let coffHeader = try? fileHandle.read(upToCount: 6), coffHeader.count == 6 else {
152+
return .unknown
153+
}
154+
155+
let signature = coffHeader.prefix(4)
156+
let machineBytes = coffHeader.suffix(2)
157+
158+
guard signature == Data([0x50, 0x45, 0x00, 0x00]) else {
159+
return .unknown
160+
}
161+
162+
let machine = machineBytes.withUnsafeBytes { $0.load(as: UInt16.self) }
163+
return .fromPEMachineByte(machine: Int32(machine))
164+
}
165+
#endif
166+
167+
#if os(macOS)
168+
static func readDarwinExecutableArchitecture(
169+
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock, filename: String
170+
) -> ExecutableArchitecture {
171+
let magicNumber: UInt32 = 0xcafe_babe
172+
173+
let searchPaths = getEnvSearchPaths(
174+
pathString: envBlock["PATH"], currentWorkingDirectory: cwd)
175+
guard
176+
let filePath = lookupExecutablePath(
177+
filename: filename, currentWorkingDirectory: cwd, searchPaths: searchPaths)
178+
else {
179+
return .unknown
180+
}
181+
guard let fileHandle = FileHandle(forReadingAtPath: filePath.pathString) else {
182+
return .unknown
183+
}
184+
185+
defer {
186+
try? fileHandle.close()
187+
}
188+
189+
// The first 4 bytes of a Mach-O header contain the magic number. We use it to determine if the binary is
190+
// universal.
191+
// https://github.com/apple/darwin-xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h
192+
let magicData = fileHandle.readData(ofLength: 4)
193+
let magic = magicData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
194+
195+
if magic == magicNumber {
196+
return .universal
197+
}
198+
199+
// If the binary is not universal, the next 4 bytes contain the CPU type.
200+
guard (try? fileHandle.seek(toOffset: 4)) != nil else {
201+
return .unknown
202+
}
203+
let cpuTypeData = fileHandle.readData(ofLength: 4)
204+
let cpuType = cpuTypeData.withUnsafeBytes { $0.load(as: Int32.self) }
205+
return .fromMachoCPUType(cpuType: cpuType)
206+
}
207+
#endif
208+
}

0 commit comments

Comments
 (0)