Skip to content

Commit b587be7

Browse files
authored
FEAT: Universal Mac Builds and Support (#88)
### Summary This pull request introduces significant updates to the build pipeline and codebase to support universal2 binaries for macOS and improve artifact organization. The changes include adding a new macOS build job, updating architecture handling for macOS to prioritize universal2 binaries, and refining artifact naming and placement for better clarity. ### Build Pipeline Updates: * Added a new `BuildMacOSWheels` job in `eng/pipelines/build-whl-pipeline.yml` to build universal2 binaries for macOS. This includes steps for setting up Python, installing dependencies, building `.so` files, and publishing artifacts. * Updated artifact paths and names in the build pipeline to reflect the new structure, such as renaming `all-pyds` to `ddbc-bindings` and updating artifact names to `mssql-python-ddbc-bindings`. [[1]](diffhunk://#diff-a871d64acfb78366b4b077045db53551c2df7d622854569f5f6780ad2ec6f911L126-R125) [[2]](diffhunk://#diff-a871d64acfb78366b4b077045db53551c2df7d622854569f5f6780ad2ec6f911L157-R164) ### macOS Universal2 Support: * Modified `CMakeLists.txt` to always use universal2 architecture for macOS, ensuring compatibility with both `arm64` and `x86_64`. Added a new CMake setting to enforce universal builds. [[1]](diffhunk://#diff-dbb5892fbbb28149d1639664797cf3adb48ced28ec11aba95f2e2b338ca46badL31-R32) [[2]](diffhunk://#diff-dbb5892fbbb28149d1639664797cf3adb48ced28ec11aba95f2e2b338ca46badR43-R48) [[3]](diffhunk://#diff-dbb5892fbbb28149d1639664797cf3adb48ced28ec11aba95f2e2b338ca46badL72-R70) * Updated `build.sh` to simplify the build process for macOS by removing architecture-specific logic and focusing on universal2 binaries. Added diagnostics to verify the universal binary output. [[1]](diffhunk://#diff-7cfcd9b5499a176c28ca4d028e9b2b060238ec79c4bea5b062d4b39929f9fb04L2-R31) [[2]](diffhunk://#diff-7cfcd9b5499a176c28ca4d028e9b2b060238ec79c4bea5b062d4b39929f9fb04L87-R85) ### Codebase Adjustments for Universal2: * Updated `ddbc_bindings.py` to prioritize universal2 binaries on macOS, regardless of local architecture. Added error handling for unsupported platforms. * Adjusted `setup.py` to always use the `macosx_15_0_universal2` platform tag for macOS wheels, ensuring consistent naming and compatibility. [[1]](diffhunk://#diff-60f61ab7a8d1910d86d9fda2261620314edcae5894d5aaa236b821c7256badd7L41-R44) [[2]](diffhunk://#diff-60f61ab7a8d1910d86d9fda2261620314edcae5894d5aaa236b821c7256badd7L75-R78) ### Documentation and Examples: * Updated the `README.md` in `mssql_python/pybind` to reflect the new universal2 binary naming convention (e.g., `.so` files named with `universal2`). These changes streamline the build process, enhance cross-platform compatibility, and improve artifact organization, aligning the project with modern macOS development practices. ### Issue Reference Fixes [AB#37761](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/37761)
1 parent 308c79e commit b587be7

File tree

7 files changed

+89
-123
lines changed

7 files changed

+89
-123
lines changed

eng/pipelines/build-whl-pipeline.yml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,19 +187,19 @@ jobs:
187187
displayName: 'Build macOS - '
188188
strategy:
189189
matrix:
190-
# Python 3.13 (only x86_64 for builds)
191-
py313_x86_64:
190+
# Python 3.13 (universal2 for both arm64 and x86_64)
191+
py313_universal2:
192192
pythonVersion: '3.13'
193193
shortPyVer: '313'
194-
# targetArch is not used anywhere since build.sh does auto-detection, kept for cross-compilation of ARM64 Builds
195-
targetArch: 'x86_64'
194+
# Always use universal2 for macOS
195+
targetArch: 'universal2'
196196
steps:
197197
# Use correct Python version and architecture for the current job
198198
- task: UsePythonVersion@0
199199
inputs:
200200
versionSpec: '$(pythonVersion)'
201201
addToPath: true
202-
displayName: 'Use Python $(pythonVersion) (x86_64)'
202+
displayName: 'Use Python $(pythonVersion) (Universal2)'
203203

204204
# Install CMake on macOS
205205
- script: |
@@ -218,7 +218,7 @@ jobs:
218218
- script: |
219219
echo "Python Version: $(pythonVersion)"
220220
echo "Short Tag: $(shortPyVer)"
221-
echo "Architecture: Target=$(targetArch)"
221+
echo "Building Universal2 Binary"
222222
cd "$(Build.SourcesDirectory)/mssql_python/pybind"
223223
# Call build.sh to build the .so file
224224
./build.sh
@@ -232,13 +232,12 @@ jobs:
232232
TargetFolder: '$(Build.ArtifactStagingDirectory)/ddbc-bindings'
233233
displayName: 'Place .so file into artifacts directory'
234234

235-
# Build wheel package for the current architecture
235+
# Build wheel package for universal2
236236
- script: |
237237
python -m pip install --upgrade pip
238238
pip install wheel setuptools
239-
set ARCHITECTURE=$(targetArch)
240239
python setup.py bdist_wheel
241-
displayName: 'Build wheel package for Python $(pythonVersion) ($(targetArch))'
240+
displayName: 'Build $(pythonVersion) universal2 whl'
242241
243242
# Copy the wheel file to the artifacts
244243
- task: CopyFiles@2

mssql_python/ddbc_bindings.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@
55

66
# Get current Python version and architecture
77
python_version = f"cp{sys.version_info.major}{sys.version_info.minor}"
8-
if platform.machine().lower() in ('amd64', 'x86_64', 'x64'):
9-
architecture = "amd64" if sys.platform == 'win32' else "x86_64"
10-
elif platform.machine().lower() in ('arm64', 'aarch64'):
11-
architecture = "arm64"
8+
9+
platform_name = sys.platform.lower()
10+
architecture = platform.machine().lower()
11+
12+
# On macOS, prioritize universal2 binary regardless of the local architecture
13+
if platform_name == 'darwin':
14+
architecture = "universal2"
15+
elif platform_name == 'win32':
16+
if architecture in ('amd64', 'x86_64', 'x64'):
17+
architecture = "amd64" if platform_name == 'win32' else "x86_64"
18+
elif architecture in ('arm64', 'aarch64'):
19+
architecture = "arm64"
20+
else:
21+
raise ImportError(f"Unsupported architecture for mssql-python: {platform_name}-{architecture}")
1222
else:
13-
architecture = platform.machine().lower()
23+
raise ImportError(f"Unsupported architecture for mssql-python: {platform_name}-{architecture}")
1424

1525
# Determine extension based on platform
16-
if sys.platform == 'win32':
26+
if platform_name == 'win32':
1727
extension = '.pyd'
1828
else: # macOS or Linux
1929
extension = '.so'

mssql_python/pybind/CMakeLists.txt

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,8 @@ if(NOT DEFINED ARCHITECTURE)
2828
# Default to x64 for Windows
2929
set(ARCHITECTURE "win64")
3030
elseif(APPLE)
31-
# Check if we're on Apple Silicon or Intel
32-
execute_process(
33-
COMMAND sysctl -n machdep.cpu.brand_string
34-
OUTPUT_VARIABLE CPU_BRAND
35-
OUTPUT_STRIP_TRAILING_WHITESPACE
36-
)
37-
if(CPU_BRAND MATCHES "Apple")
38-
set(ARCHITECTURE "arm64")
39-
else()
40-
set(ARCHITECTURE "x64")
41-
endif()
31+
# Always use universal2 for macOS
32+
set(ARCHITECTURE "universal2")
4233
else()
4334
# Default to x64 for other platforms
4435
set(ARCHITECTURE "x64")
@@ -49,6 +40,12 @@ endif()
4940
add_definitions(-DARCHITECTURE="${ARCHITECTURE}")
5041
add_definitions(-DPLATFORM_NAME="${PLATFORM_NAME}")
5142

43+
# For macOS, always set universal build
44+
if(APPLE)
45+
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures for macOS" FORCE)
46+
message(STATUS "Setting universal2 build for macOS (arm64 + x86_64)")
47+
endif()
48+
5249
# Get Python version and platform info
5350
execute_process(
5451
COMMAND python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')"
@@ -69,12 +66,8 @@ if(WIN32)
6966
message(FATAL_ERROR "Unsupported Windows architecture: ${ARCHITECTURE}. Supported architectures are win32, win64, x86, amd64, arm64.")
7067
endif()
7168
elseif(APPLE)
72-
# macOS architecture mapping
73-
if(ARCHITECTURE STREQUAL "arm64")
74-
set(WHEEL_ARCH "arm64")
75-
else()
76-
set(WHEEL_ARCH "x86_64")
77-
endif()
69+
# macOS always uses universal2
70+
set(WHEEL_ARCH "universal2")
7871
else()
7972
# Linux architecture mapping
8073
if(ARCHITECTURE STREQUAL "arm64")
@@ -209,6 +202,7 @@ else()
209202
set(MODULE_EXTENSION ".so")
210203
endif()
211204

205+
# Set the output name based on the Python version and architecture
212206
set_target_properties(ddbc_bindings PROPERTIES
213207
PREFIX ""
214208
OUTPUT_NAME "ddbc_bindings.cp${PYTHON_VERSION}-${WHEEL_ARCH}"

mssql_python/pybind/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,5 +60,5 @@ This README provides instructions to build the DDBC Bindings PYD for your system
6060
- Detect system architecture
6161
- Configure CMake with appropriate include/library paths
6262
- Compile `ddbc_bindings_mac.cpp` using CMake
63-
- Generate the `.so` file (e.g., `ddbc_bindings.cp313-arm64.so`)
63+
- Generate the `.so` file (e.g., `ddbc_bindings.cp313-universal2.so`)
6464
- Copy the output SO file to the parent `mssql_python` directory

mssql_python/pybind/build.sh

Lines changed: 39 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,34 @@
11
#!/bin/bash
2-
# Build script for macOS to compile the ddbc_bindings C++ code
2+
# Build script for macOS to compile a universal2 (arm64 + x86_64) binary
33
# This script is designed to be run from the mssql_python/pybind directory
4-
# Running this script will require CMake and a C++ compiler installed on your MacOS system
5-
# It will also require Python 3.x, pybind11 and msodbcsql18 and unixODBC to be installed
6-
7-
# Usage: build.sh [ARCH], If ARCH is not specified, it defaults to the current architecture
8-
ARCH=${1:-$(uname -m)}
9-
echo "[DIAGNOSTIC] Target Architecture set to: $ARCH"
10-
11-
# Clean up main build directory if it exists
12-
echo "Checking for main build directory..."
13-
if [ -d "build" ]; then
14-
echo "Removing existing build directory..."
15-
rm -rf build
16-
echo "Build directory removed."
17-
fi
184

195
# Get Python version from active interpreter
206
PYTAG=$(python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')")
217

228
echo "==================================="
23-
echo "Building for: $ARCH / Python $PYTAG"
9+
echo "Building Universal2 Binary for Python $PYTAG"
2410
echo "==================================="
2511

2612
# Save absolute source directory
2713
SOURCE_DIR=$(pwd)
2814

29-
# Go to build output directory
30-
BUILD_DIR="${SOURCE_DIR}/build/${ARCH}/py${PYTAG}"
15+
# Clean up main build directory if it exists
16+
echo "Checking for build directory..."
17+
if [ -d "build" ]; then
18+
echo "Removing existing build directory..."
19+
rm -rf build
20+
echo "Build directory removed."
21+
fi
22+
23+
# Create build directory for universal binary
24+
BUILD_DIR="${SOURCE_DIR}/build"
3125
mkdir -p "${BUILD_DIR}"
3226
cd "${BUILD_DIR}"
3327
echo "[DIAGNOSTIC] Changed to build directory: ${BUILD_DIR}"
3428

35-
# Set platform-specific flags for different architectures
36-
if [ "$ARCH" = "x86_64" ]; then
37-
# x86_64 architecture
38-
echo "[DIAGNOSTIC] Detected Intel Chip x86_64 architecture"
39-
CMAKE_ARCH="x86_64"
40-
echo "[DIAGNOSTIC] Using x86_64 architecture for CMake"
41-
elif [ "$ARCH" = "arm64" ]; then
42-
# arm64 architecture
43-
echo "[DIAGNOSTIC] Detected Apple Silicon Chip arm64 architecture"
44-
CMAKE_ARCH="arm64"
45-
echo "[DIAGNOSTIC] Using arm64 architecture for CMake"
46-
else
47-
echo "[ERROR] Unsupported architecture: $ARCH"
48-
exit 1
49-
fi
50-
51-
echo "[DIAGNOSTIC] Source directory: ${SOURCE_DIR}"
52-
53-
# Special handling for macOS ODBC headers and string conversion issues
54-
if [ "$(uname)" = "Darwin" ]; then
55-
# Check if macOS-specific source file exists
56-
if [ -f "${SOURCE_DIR}/ddbc_bindings_mac.cpp" ]; then
57-
echo "[DIAGNOSTIC] Using macOS-specific source file: ddbc_bindings_mac.cpp"
58-
else
59-
echo "[WARNING] macOS-specific source file ddbc_bindings_mac.cpp not found"
60-
echo "[WARNING] Falling back to standard source file ddbc_bindings.cpp"
61-
fi
62-
63-
# Configure CMake with macOS-specific flags
64-
echo "[DIAGNOSTIC] Running CMake configure with macOS-specific settings"
65-
cmake -DCMAKE_OSX_ARCHITECTURES=${CMAKE_ARCH} \
66-
-DARCHITECTURE=${ARCH} \
67-
-DMACOS_STRING_FIX=ON \
68-
"${SOURCE_DIR}"
69-
else
70-
# Configure CMake for other platforms
71-
echo "[DIAGNOSTIC] Running CMake configure for non-macOS platform"
72-
cmake -DARCHITECTURE=${ARCH} "${SOURCE_DIR}"
73-
fi
29+
# Configure CMake (architecture settings handled in CMakeLists.txt)
30+
echo "[DIAGNOSTIC] Running CMake configure (universal2 is set automatically)"
31+
cmake -DMACOS_STRING_FIX=ON "${SOURCE_DIR}"
7432

7533
# Check if CMake configuration succeeded
7634
if [ $? -ne 0 ]; then
@@ -87,32 +45,41 @@ if [ $? -ne 0 ]; then
8745
echo "[ERROR] CMake build failed"
8846
exit 1
8947
else
90-
echo "[SUCCESS] Build completed successfully"
48+
echo "[SUCCESS] Universal2 build completed successfully"
49+
9150
# List the built files
9251
echo "Built files:"
9352
ls -la *.so
9453

9554
# Copy the built .so file to the mssql_python directory
9655
PARENT_DIR=$(dirname "$SOURCE_DIR")
97-
echo "[ACTION] Copying the .so file to $PARENT_DIR"
56+
echo "[ACTION] Copying the universal2 .so file to $PARENT_DIR"
9857
cp -f *.so "$PARENT_DIR"
9958
if [ $? -eq 0 ]; then
100-
echo "[SUCCESS] .so file copied successfully"
59+
echo "[SUCCESS] Universal2 .so file copied successfully"
10160

102-
# Only on macOS, run the dylib configuration script to fix library paths and codesign
103-
if [ "$(uname)" = "Darwin" ]; then
104-
echo "[ACTION] Configuring and codesigning dylibs for macOS"
105-
chmod +x "${SOURCE_DIR}/configure_dylibs.sh"
106-
"${SOURCE_DIR}/configure_dylibs.sh"
107-
if [ $? -eq 0 ]; then
108-
echo "[SUCCESS] macOS dylibs configured and codesigned successfully"
109-
else
110-
echo "[WARNING] macOS dylib configuration encountered issues"
111-
# Don't exit with error, as the build itself was successful
112-
fi
61+
# Configure dylib paths and codesign
62+
echo "[ACTION] Configuring and codesigning dylibs for macOS"
63+
chmod +x "${SOURCE_DIR}/configure_dylibs.sh"
64+
"${SOURCE_DIR}/configure_dylibs.sh"
65+
if [ $? -eq 0 ]; then
66+
echo "[SUCCESS] macOS dylibs configured and codesigned successfully"
67+
else
68+
echo "[WARNING] macOS dylib configuration encountered issues"
11369
fi
11470
else
115-
echo "[ERROR] Failed to copy .so file"
71+
echo "[ERROR] Failed to copy universal2 .so file"
11672
exit 1
11773
fi
11874
fi
75+
76+
# Check if the file is a universal binary
77+
SO_FILE=$(ls -1 *.so | head -n 1)
78+
echo "[DIAGNOSTIC] Checking if ${SO_FILE} is a universal binary..."
79+
lipo -info "${SO_FILE}"
80+
81+
# Check if the file has the correct naming convention
82+
if [[ "${SO_FILE}" != *universal2* ]]; then
83+
echo "[WARNING] The .so file doesn't have 'universal2' in its name, even though it's a universal binary."
84+
echo "[WARNING] You may need to run the build again after the CMakeLists.txt changes are applied."
85+
fi

mssql_python/pybind/ddbc_bindings_mac.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,13 @@ DriverHandle LoadDriverOrThrowException() {
410410
#elif defined(__APPLE__)
411411
// macOS: Build path to libmsodbcsql.18.dylib - first check in lib subdirectory
412412
// We are supporting both Intel and Apple Silicon architectures, so we need to check the architecture
413-
std::string macosDriverPath = moduleDir + "/libs/macos/" + ARCHITECTURE + "/lib/libmsodbcsql.18.dylib";
413+
std::string runtimeArch =
414+
#ifdef __arm64__
415+
"arm64";
416+
#else
417+
"x86_64";
418+
#endif
419+
std::string macosDriverPath = moduleDir + "/libs/macos/" + runtimeArch + "/lib/libmsodbcsql.18.dylib";
414420

415421
// Check if file exists using traditional C file functions instead of std::filesystem
416422
FILE* file = fopen(macosDriverPath.c_str(), "r");

setup.py

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ def finalize_options(self):
3838
print(f"Setting wheel platform tag to: {self.plat_name}")
3939

4040
# Force platform-specific paths if bdist_dir is already set
41-
if self.bdist_dir and "win-amd64" in self.bdist_dir:
42-
self.bdist_dir = self.bdist_dir.replace("win-amd64", f"win-{platform_dir}")
43-
print(f"Using build directory: {self.bdist_dir}")
41+
elif sys.platform.startswith('darwin'):
42+
# For macOS, always use universal2
43+
self.plat_name = "macosx_15_0_universal2"
44+
print(f"Setting wheel platform tag to: {self.plat_name} (universal2)")
4445

4546
# Find all packages in the current directory
4647
packages = find_packages()
@@ -72,20 +73,9 @@ def finalize_options(self):
7273
f'mssql_python.libs.{arch}.vcredist'
7374
])
7475
elif sys.platform.startswith('darwin'):
75-
# macOS platform
76-
import platform
77-
arch = os.environ.get('ARCHITECTURE', None)
78-
79-
# Auto-detect architecture if not specified
80-
if arch is None:
81-
if platform.machine() == 'arm64':
82-
arch = 'arm64'
83-
platform_tag = 'macosx_15_0_arm64'
84-
elif platform.machine() == 'x86_64':
85-
arch = 'x86_64'
86-
platform_tag = 'macosx_15_0_x86_64'
87-
else:
88-
raise Exception("Unsupported architecture for macOS.")
76+
# macOS platform - always use universal2
77+
arch = 'universal2'
78+
platform_tag = 'macosx_15_0_universal2' # Use macOS 15.0 (Monterey) as minimum for universal2
8979

9080
# Add architecture-specific packages for macOS
9181
packages.extend([

0 commit comments

Comments
 (0)