Skip to content

Commit b8a065b

Browse files
[darwin] add a check for a matching python architecture
1 parent 0b3e613 commit b8a065b

File tree

2 files changed

+179
-90
lines changed

2 files changed

+179
-90
lines changed

Sources/SwiftDriver/Driver/Driver.swift

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

876876
self.executor = executor
877877

878-
let invocationMode = try Self.invocationRunMode(forArgs: args).mode
879-
if case .subcommand = invocationMode {
880-
throw Error.subcommandPassedToDriver
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
881889
}
882890

883-
#if os(Windows)
884-
if case .normal(isRepl: true) = invocationMode {
885-
checkIfMatchingPythonArch(
886-
cwd: ProcessEnv.cwd, envBlock: envBlock, diagnosticsEngine: diagnosticsEngine)
887-
}
888-
#endif
891+
if case .subcommand = try Self.invocationRunMode(forArgs: args).mode {
892+
throw Error.subcommandPassedToDriver
893+
}
889894

890895
var args = args
891896
if let additional = env["ADDITIONAL_SWIFT_DRIVER_FLAGS"] {
Lines changed: 165 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
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+
//===----------------------------------------------------------------------===//
112
import Foundation
213

314
import struct TSCBasic.AbsolutePath
@@ -11,114 +22,187 @@ import typealias TSCBasic.ProcessEnvironmentBlock
1122
import func TSCBasic.getEnvSearchPaths
1223
import func TSCBasic.lookupExecutablePath
1324

25+
#if os(Windows)
26+
import WinSDK
27+
#endif
28+
1429
/// Check that the architecture of the toolchain matches the architecture
1530
/// of the Python installation.
1631
///
1732
/// When installing the x86 toolchain on ARM64 Windows, if the user does not
18-
/// install an x86 version of Python, they will get acryptic error message
33+
/// install an x86 version of Python, they will get a cryptic error message
1934
/// when running lldb (`0xC000007B`). Calling this function before invoking
20-
/// lldb gives them a warning to help troublshoot the issue.
35+
/// lldb gives them a warning to help troubleshoot the issue.
2136
///
2237
/// - Parameters:
2338
/// - cwd: The current working directory.
24-
/// - env: A dictionary of the environment variables and their values. Usually of the parent shell.
39+
/// - env: The parent shell's ProcessEnvironmentBlock.
2540
/// - diagnosticsEngine: DiagnosticsEngine instance to use for printing the warning.
2641
public func checkIfMatchingPythonArch(
27-
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock, diagnosticsEngine: DiagnosticsEngine
42+
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock,
43+
toolchainArchitecture: ExecutableArchitecture, diagnosticsEngine: DiagnosticsEngine
2844
) {
29-
#if arch(arm64)
30-
let toolchainArchitecture = COFFBinaryExecutableArchitecture.arm64
31-
#elseif arch(x86_64)
32-
let toolchainArchitecture = COFFBinaryExecutableArchitecture.x64
33-
#elseif arch(x86)
34-
let toolchainArchitecture = COFFBinaryExecutableArchitecture.x86
35-
#else
36-
return
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+
}
3767
#endif
38-
let pythonArchitecture = COFFBinaryExecutableArchitecture.readWindowsExecutableArchitecture(
39-
cwd: cwd, envBlock: envBlock, filename: "python.exe")
40-
41-
if toolchainArchitecture != pythonArchitecture {
42-
diagnosticsEngine.emit(
43-
.warning(
44-
"""
45-
There is an architecture mismatch between the installed toolchain and the resolved Python's architecture:
46-
Toolchain: \(toolchainArchitecture)
47-
Python: \(pythonArchitecture)
48-
"""))
49-
}
5068
}
5169

5270
/// Some of the architectures that can be stored in a COFF header.
53-
enum COFFBinaryExecutableArchitecture: String {
71+
public enum ExecutableArchitecture: String {
5472
case x86 = "X86"
5573
case x64 = "X64"
5674
case arm64 = "ARM64"
75+
case universal = "Universal"
5776
case unknown = "Unknown"
5877

59-
static func fromPEMachineByte(machine: UInt16) -> Self {
60-
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
61-
switch machine {
62-
case 0x014c: return .x86
63-
case 0x8664: return .x64
64-
case 0xAA64: return .arm64
65-
default: return .unknown
66-
}
67-
}
68-
69-
/// Resolves the filename from the `Path` environment variable and read its COFF header to determine the architecture
70-
/// of the binary.
71-
///
72-
/// - Parameters:
73-
/// - cwd: The current working directory.
74-
/// - env: A dictionary of the environment variables and their values. Usually of the parent shell.
75-
/// - filename: The name of the file we are resolving the architecture of.
76-
/// - Returns: The architecture of the file which was found in the `Path`.
77-
static func readWindowsExecutableArchitecture(
78-
cwd: AbsolutePath?, envBlock: ProcessEnvironmentBlock, filename: String
79-
) -> Self {
80-
let searchPaths = getEnvSearchPaths(pathString: envBlock["Path"], currentWorkingDirectory: cwd)
81-
guard
82-
let filePath = lookupExecutablePath(
83-
filename: filename, currentWorkingDirectory: cwd, searchPaths: searchPaths)
84-
else {
85-
return .unknown
86-
}
87-
guard let fileHandle = FileHandle(forReadingAtPath: filePath.pathString) else {
88-
return .unknown
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+
}
8987
}
88+
#endif
9089

91-
defer { fileHandle.closeFile() }
92-
93-
// Infering the architecture of a Windows executable from its COFF header involves the following:
94-
// 1. Get the COFF header offset from the pointer located at the 0x3C offset (4 bytes long).
95-
// 2. Jump to that offset and read the next 6 bytes.
96-
// 3. The first 4 are the signature which should be equal to 0x50450000.
97-
// 4. The last 2 are the machine architecture which can be infered from the value we get.
98-
//
99-
// The link below provides a visualization of the COFF header and the process to get to it.
100-
// https://upload.wikimedia.org/wikipedia/commons/1/1b/Portable_Executable_32_bit_Structure_in_SVG_fixed.svg
101-
fileHandle.seek(toFileOffset: 0x3C)
102-
guard let offsetPointer = try? fileHandle.read(upToCount: 4),
103-
offsetPointer.count == 4
104-
else {
105-
return .unknown
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+
}
10698
}
99+
#endif
100+
}
107101

108-
let peHeaderOffset = offsetPointer.withUnsafeBytes { $0.load(as: UInt32.self) }
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+
}
109126

110-
fileHandle.seek(toFileOffset: UInt64(peHeaderOffset))
111-
guard let coffHeader = try? fileHandle.read(upToCount: 6), coffHeader.count == 6 else {
112-
return .unknown
113-
}
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) }
114147

115-
let signature = coffHeader.prefix(4)
116-
let machineBytes = coffHeader.suffix(2)
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+
}
117154

118-
guard signature == Data([0x50, 0x45, 0x00, 0x00]) else {
119-
return .unknown
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))
120164
}
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+
}
121184

122-
return .fromPEMachineByte(machine: machineBytes.withUnsafeBytes { $0.load(as: UInt16.self) })
123-
}
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
124208
}

0 commit comments

Comments
 (0)