diff --git a/.gitignore b/.gitignore index ed401757..f4c36b51 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ mssql_python/pybind/build/ mssql_python/pybind/pymsbuild/build/ -# Ignore all pyd files -**.pyd +# Ignore pyd file +mssql_python/ddbc_bindings.pyd # Ignore pycache files and folders __pycache__/ @@ -26,11 +26,25 @@ mssql_python/.vs test-*.xml **/test-**.xml -# Ignore coverage files -coverage.xml -.coverage -.coverage.* - # Ignore the build & mssql_python.egg-info directories build/ -mssql_python.egg-info/ \ No newline at end of file +mssql_python.egg-info/ + +# Python bytecode +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +dist/ +build/ +*.egg-info/ + +# C extensions +*.so +*.pyd + +# IDE files +.vscode/ +.idea/ +*.swp \ No newline at end of file diff --git a/eng/pipelines/build-whl-pipeline.yml b/eng/pipelines/build-whl-pipeline.yml new file mode 100644 index 00000000..5487772d --- /dev/null +++ b/eng/pipelines/build-whl-pipeline.yml @@ -0,0 +1,164 @@ +# Pipeline name shown in ADO UI +name: build-whl-pipeline + +# Trigger the pipeline on pushes to these branches +trigger: + branches: + include: + - main + - dev + +# Use Microsoft-hosted Windows VM +pool: + vmImage: 'windows-latest' + +jobs: +- job: BuildPYDs + displayName: 'Build -' + # Strategy matrix to build all combinations + strategy: + matrix: + # Python 3.10 (only x64) + py310_x64: + pythonVersion: '3.10' # Host Python version + shortPyVer: '310' # Used in filenames like cp310 + architecture: 'x64' # Host Python architecture + targetArch: 'x64' # Target architecture to pass to build.bat + + # Python 3.11 + py311_x64: + pythonVersion: '3.11' # Host Python version + shortPyVer: '311' # Used in filenames like cp311 + architecture: 'x64' # Host Python architecture + targetArch: 'x64' # Target architecture to pass to build.bat + py311_arm64: + pythonVersion: '3.11' + shortPyVer: '311' + architecture: 'x64' # No arm64 Python, use x64 host + targetArch: 'arm64' + + # Python 3.12 + py312_x64: + pythonVersion: '3.12' + shortPyVer: '312' + architecture: 'x64' + targetArch: 'x64' + py312_arm64: + pythonVersion: '3.12' + shortPyVer: '312' + architecture: 'x64' + targetArch: 'arm64' + + # Python 3.13 + py313_x64: + pythonVersion: '3.13' + shortPyVer: '313' + architecture: 'x64' + targetArch: 'x64' + py313_arm64: + pythonVersion: '3.13' + shortPyVer: '313' + architecture: 'x64' + targetArch: 'arm64' + + steps: + # Use correct Python version and architecture for the current job + - task: UsePythonVersion@0 + inputs: + versionSpec: '$(pythonVersion)' + architecture: '$(architecture)' + addToPath: true + displayName: 'Use Python $(pythonVersion) ($(architecture))' + + # Install required packages: pip, CMake, pybind11 + - script: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install cmake pybind11 + displayName: 'Install dependencies' + + - task: DownloadPipelineArtifact@2 + condition: eq(variables['targetArch'], 'arm64') + inputs: + buildType: 'specific' + project: '$(System.TeamProject)' + definition: 2134 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/bewithgaurav/build_whl_pipeline' # Or 'refs/heads/dev' + artifactName: 'mssql-python-arm64-libs' + targetPath: '$(Build.SourcesDirectory)\mssql_python\pybind\python_libs\arm64' + displayName: 'Download ARM64 Python libs from latest successful run on branches' + + # Build the PYD file by calling build.bat + - script: | + echo "Python Version: $(pythonVersion)" + echo "Short Tag: $(shortPyVer)" + echo "Architecture: Host=$(architecture), Target=$(targetArch)" + + cd "$(Build.SourcesDirectory)\mssql_python\pybind" + + REM Optional: override lib path if building for ARM64 since we cannot install arm64 python on x64 host + if "$(targetArch)"=="arm64" ( + echo Using arm64-specific Python library... + set CUSTOM_PYTHON_LIB_DIR=$(Build.SourcesDirectory)\mssql_python\pybind\python_libs\arm64 + ) + + REM Call build.bat to build the PYD file + call build.bat $(targetArch) + + REM Calling keep_single_arch.bat to remove ODBC libs of other architectures + call keep_single_arch.bat $(targetArch) + + cd ..\.. + displayName: 'Build PYD for $(targetArch)' + + # Copy the built .pyd file to staging folder for artifacts + - task: CopyFiles@2 + inputs: + SourceFolder: '$(Build.SourcesDirectory)\mssql_python\pybind\build\$(targetArch)\py$(shortPyVer)\Release' + Contents: 'ddbc_bindings.cp$(shortPyVer)-*.pyd' + TargetFolder: '$(Build.ArtifactStagingDirectory)\all-pyds' + displayName: 'Place PYD file into artifacts directory' + + # Build wheel package for the current architecture + - script: | + python -m pip install --upgrade pip + pip install wheel setuptools + set ARCHITECTURE=$(targetArch) + python setup.py bdist_wheel + displayName: 'Build wheel package for Python $(pythonVersion) ($(targetArch))' + + # Copy the wheel file to the artifacts + - task: CopyFiles@2 + inputs: + SourceFolder: '$(Build.SourcesDirectory)\dist' + Contents: '*.whl' + TargetFolder: '$(Build.ArtifactStagingDirectory)\dist' + displayName: 'Collect wheel package' + + # Publish the collected .pyd file(s) as build artifacts + - task: PublishBuildArtifacts@1 + condition: succeededOrFailed() + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)\all-pyds' + ArtifactName: 'mssql-python-pyds' + publishLocation: 'Container' + displayName: 'Publish all PYDs as artifacts' + + # Publish the python arm64 libraries as build artifacts for next builds if ARM64 + - task: PublishBuildArtifacts@1 + condition: eq(variables['targetArch'], 'arm64') + inputs: + PathtoPublish: '$(Build.SourcesDirectory)\mssql_python\pybind\python_libs\arm64' + ArtifactName: 'mssql-python-arm64-libs' + publishLocation: 'Container' + displayName: 'Publish arm64 libs as artifacts' + + # Publish the collected wheel file(s) as build artifacts + - task: PublishBuildArtifacts@1 + condition: succeededOrFailed() + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)\dist' + ArtifactName: 'mssql-python-wheels-dist' + publishLocation: 'Container' + displayName: 'Publish all wheels as artifacts' diff --git a/eng/pipelines/test-pipeline.yml b/eng/pipelines/test-pipeline.yml index 6688e518..dd9397c9 100644 --- a/eng/pipelines/test-pipeline.yml +++ b/eng/pipelines/test-pipeline.yml @@ -36,11 +36,7 @@ jobs: - script: | cd mssql_python\pybind - mkdir build - cd build - cmake -DPython3_EXECUTABLE="python3" -DCMAKE_BUILD_TYPE=Debug .. - cmake --build . --config Debug - copy Debug\ddbc_bindings.pyd ..\..\ddbc_bindings.pyd + build.bat x64 displayName: 'Build .pyd file' - script: | @@ -51,7 +47,7 @@ jobs: - task: PublishBuildArtifacts@1 inputs: - PathtoPublish: 'mssql_python/ddbc_bindings.pyd' + PathtoPublish: 'mssql_python/ddbc_bindings.cp313-amd64.pyd' ArtifactName: 'ddbc_bindings' publishLocation: 'Container' displayName: 'Publish pyd file as artifact' diff --git a/mssql_python/connection.py b/mssql_python/connection.py index bab00e43..8031a26a 100644 --- a/mssql_python/connection.py +++ b/mssql_python/connection.py @@ -95,7 +95,6 @@ def _construct_connection_string(self, connection_str: str = "", **kwargs) -> st else: continue conn_str += f"{key}={value};" - print(f"Connection string after adding driver: {conn_str}") if ENABLE_LOGGING: logger.info("Final connection string: %s", conn_str) diff --git a/mssql_python/ddbc_bindings.py b/mssql_python/ddbc_bindings.py new file mode 100644 index 00000000..488f2c7f --- /dev/null +++ b/mssql_python/ddbc_bindings.py @@ -0,0 +1,38 @@ +import os +import importlib.util +import sys +import platform + +# Get current Python version and architecture +python_version = f"cp{sys.version_info.major}{sys.version_info.minor}" +if platform.machine().lower() in ('amd64', 'x86_64', 'x64'): + architecture = "amd64" +elif platform.machine().lower() in ('arm64', 'aarch64'): + architecture = "arm64" +else: + architecture = platform.machine().lower() + +# Find the specifically matching PYD file +module_dir = os.path.dirname(__file__) +expected_pyd = f"ddbc_bindings.{python_version}-{architecture}.pyd" +pyd_path = os.path.join(module_dir, expected_pyd) + +if not os.path.exists(pyd_path): + # Fallback to searching for any matching PYD if the specific one isn't found + pyd_files = [f for f in os.listdir(module_dir) if f.startswith('ddbc_bindings.') and f.endswith('.pyd')] + if not pyd_files: + raise ImportError(f"No ddbc_bindings PYD module found for {python_version}-{architecture}") + pyd_path = os.path.join(module_dir, pyd_files[0]) + print(f"Warning: Using fallback PYD file {pyd_files[0]} instead of {expected_pyd}") + +# Use the original module name 'ddbc_bindings' that the C extension was compiled with +name = "ddbc_bindings" +spec = importlib.util.spec_from_file_location(name, pyd_path) +module = importlib.util.module_from_spec(spec) +sys.modules[name] = module +spec.loader.exec_module(module) + +# Copy all attributes from the loaded module to this module +for attr in dir(module): + if not attr.startswith('__'): + globals()[attr] = getattr(module, attr) \ No newline at end of file diff --git a/mssql_python/helpers.py b/mssql_python/helpers.py index c60a3f3e..e8e03a4d 100644 --- a/mssql_python/helpers.py +++ b/mssql_python/helpers.py @@ -46,7 +46,7 @@ def add_driver_to_connection_str(connection_str): # Insert the driver attribute at the beginning of the connection string final_connection_attributes.insert(0, driver_name) connection_str = ";".join(final_connection_attributes) - print(f"Connection string after adding driver: {connection_str}") + except Exception as e: raise Exception( "Invalid connection string, Please follow the format: " diff --git a/mssql_python/libs/win/1033/msodbcsqlr18.rll b/mssql_python/libs/x64/1033/msodbcsqlr18.rll similarity index 100% rename from mssql_python/libs/win/1033/msodbcsqlr18.rll rename to mssql_python/libs/x64/1033/msodbcsqlr18.rll diff --git a/mssql_python/libs/win/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt b/mssql_python/libs/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt similarity index 100% rename from mssql_python/libs/win/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt rename to mssql_python/libs/x64/MICROSOFT_ODBC_DRIVER_FOR_SQL_SERVER_LICENSE.txt diff --git a/mssql_python/libs/win/MICROSOFT_VISUAL_STUDIO_LICENSE.txt b/mssql_python/libs/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt similarity index 100% rename from mssql_python/libs/win/MICROSOFT_VISUAL_STUDIO_LICENSE.txt rename to mssql_python/libs/x64/MICROSOFT_VISUAL_STUDIO_LICENSE.txt diff --git a/mssql_python/libs/win/msodbcdiag18.dll b/mssql_python/libs/x64/msodbcdiag18.dll similarity index 100% rename from mssql_python/libs/win/msodbcdiag18.dll rename to mssql_python/libs/x64/msodbcdiag18.dll diff --git a/mssql_python/libs/win/msodbcsql18.dll b/mssql_python/libs/x64/msodbcsql18.dll similarity index 100% rename from mssql_python/libs/win/msodbcsql18.dll rename to mssql_python/libs/x64/msodbcsql18.dll diff --git a/mssql_python/libs/win/mssql-auth.dll b/mssql_python/libs/x64/mssql-auth.dll similarity index 100% rename from mssql_python/libs/win/mssql-auth.dll rename to mssql_python/libs/x64/mssql-auth.dll diff --git a/mssql_python/libs/x64/vcredist/msvcp140.dll b/mssql_python/libs/x64/vcredist/msvcp140.dll new file mode 100644 index 00000000..0a9b13d7 Binary files /dev/null and b/mssql_python/libs/x64/vcredist/msvcp140.dll differ diff --git a/mssql_python/pybind/CMakeLists.txt b/mssql_python/pybind/CMakeLists.txt index 60dd975e..1c226f0c 100644 --- a/mssql_python/pybind/CMakeLists.txt +++ b/mssql_python/pybind/CMakeLists.txt @@ -1,68 +1,135 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.15) project(ddbc_bindings) -# Set the C++ standard -set(CMAKE_CXX_STANDARD 11) +# Set C++ standard +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Locate Python interpreter -find_package(Python3 REQUIRED COMPONENTS Interpreter Development) +# Set default architecture if not provided +if(NOT DEFINED ARCHITECTURE) + set(ARCHITECTURE "win64") +endif() + +# Add architecture to compiler definitions +add_definitions(-DARCHITECTURE="${ARCHITECTURE}") -# Dynamically determine the path to pybind11 +# Get Python version and platform info execute_process( - COMMAND "${Python3_EXECUTABLE}" -m pybind11 --cmakedir - OUTPUT_VARIABLE pybind11_DIR + COMMAND python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')" + OUTPUT_VARIABLE PYTHON_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE ) -# Normalize the path to ensure platform independence -file(TO_CMAKE_PATH "${pybind11_DIR}" pybind11_DIR) +# Map the architecture to a format similar to what wheels use +if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR DEFINED ENV{BUILD_ARM64}) + set(WHEEL_ARCH "arm64") +elseif(ARCHITECTURE STREQUAL "win64" OR ARCHITECTURE STREQUAL "amd64" OR ARCHITECTURE STREQUAL "x64") + set(WHEEL_ARCH "amd64") +elseif(ARCHITECTURE STREQUAL "win32" OR ARCHITECTURE STREQUAL "x86") + set(WHEEL_ARCH "win32") +else() + message(FATAL_ERROR "Unsupported architecture: ${ARCHITECTURE}. Supported architectures are win32, win64, x86, amd64, arm64.") +endif() -# Strip double quotes from the path -string(REPLACE "\"" "" pybind11_DIR "${pybind11_DIR}") +# Get Python and pybind11 include paths (needed for all architectures) +execute_process( + COMMAND python -c "import sysconfig; print(sysconfig.get_path('include'))" + OUTPUT_VARIABLE PYTHON_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) -message(STATUS "pybind11 directory: ${pybind11_DIR}") +# Add debug messages for Python paths +message(STATUS "Python version: ${PYTHON_VERSION}") +message(STATUS "Python include directory: ${PYTHON_INCLUDE_DIR}") -# Add pybind11 module path -if(NOT EXISTS "${pybind11_DIR}/pybind11Config.cmake") - message(FATAL_ERROR "pybind11 not found. Ensure pybind11 is installed in the Python environment.") +# Get Python library directory with fallbacks if one method returns None +execute_process( + COMMAND python -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))" + OUTPUT_VARIABLE PYTHON_LIB_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) +message(STATUS "Python LIBDIR from sysconfig: ${PYTHON_LIB_DIR}") + +# If LIBDIR is None or empty, try alternative methods +if("${PYTHON_LIB_DIR}" STREQUAL "None" OR "${PYTHON_LIB_DIR}" STREQUAL "") + message(STATUS "LIBDIR is None or empty, trying alternative methods") + + # Get the directory of the Python executable + execute_process( + COMMAND python -c "import sys, os; print(os.path.dirname(sys.executable))" + OUTPUT_VARIABLE PYTHON_EXEC_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + message(STATUS "Python executable directory: ${PYTHON_EXEC_DIR}") + + # Check if the libs directory exists, and set PYTHON_LIB_DIR accordingly + execute_process( + COMMAND python -c "import sys, os; libs_dir = os.path.join(os.path.dirname(sys.executable), 'libs'); print('EXISTS' if os.path.exists(libs_dir) else 'NOT_EXISTS')" + OUTPUT_VARIABLE PYTHON_LIBS_EXISTS + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if("${PYTHON_LIBS_EXISTS}" STREQUAL "EXISTS") + set(PYTHON_LIB_DIR "${PYTHON_EXEC_DIR}/libs") + message(STATUS "Setting Python libs directory from sys.executable: ${PYTHON_LIB_DIR}") + endif() endif() -list(APPEND CMAKE_PREFIX_PATH "${pybind11_DIR}") -find_package(pybind11 REQUIRED) - -# Allow specifying build type (default to Release) -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type: Debug or Release" FORCE) +# If CUSTOM_PYTHON_LIB_DIR is set and exists, use it +if(DEFINED ENV{CUSTOM_PYTHON_LIB_DIR} AND NOT "$ENV{CUSTOM_PYTHON_LIB_DIR}" STREQUAL "" AND NOT "$ENV{CUSTOM_PYTHON_LIB_DIR}" STREQUAL "None") + message(STATUS "CUSTOM_PYTHON_LIB_DIR is set, using it") + set(PYTHON_LIB_DIR $ENV{CUSTOM_PYTHON_LIB_DIR}) + message(STATUS "Using custom Python library directory: ${PYTHON_LIB_DIR}") +else() + message(STATUS "Custom Path is not set or empty, finally using: ${PYTHON_LIB_DIR}") endif() -# Set the output name (for Python extension, it should be a .pyd on Windows) -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) -set(PYTHON_EXTENSION_NAME ddbc_bindings) +execute_process( + COMMAND python -c "import pybind11; print(pybind11.get_include())" + OUTPUT_VARIABLE PYBIND11_INCLUDE_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE +) -# Add the shared library (DLL or .pyd) instead of an executable -add_library(${PYTHON_EXTENSION_NAME} MODULE ddbc_bindings.cpp) +# Add module library +add_library(ddbc_bindings MODULE ddbc_bindings.cpp) -# Set output directories based on build type -set_target_properties(${PYTHON_EXTENSION_NAME} PROPERTIES +# Set the output name to include Python version and architecture +set_target_properties(ddbc_bindings PROPERTIES PREFIX "" + OUTPUT_NAME "ddbc_bindings.cp${PYTHON_VERSION}-${WHEEL_ARCH}" SUFFIX ".pyd" - RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}/Debug - RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}/Release ) -# Link pybind11 library -target_link_libraries(${PYTHON_EXTENSION_NAME} PRIVATE pybind11::module) +# Include directories for all architectures +target_include_directories(ddbc_bindings PRIVATE + ${PYTHON_INCLUDE_DIR} + ${PYBIND11_INCLUDE_DIR} +) -# Link the Python library -target_link_libraries(${PYTHON_EXTENSION_NAME} PRIVATE ${Python3_LIBRARIES}) +# Special handling for ARM64 builds +if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64" OR DEFINED ENV{BUILD_ARM64}) + message(STATUS "Building for ARM64 architecture") + set(CMAKE_SYSTEM_PROCESSOR "ARM64") + set(CMAKE_VS_PLATFORM_NAME "ARM64") + add_definitions(-DTARGET_ARM64=1) + +elseif(ARCHITECTURE STREQUAL "win32" OR ARCHITECTURE STREQUAL "x86") + message(STATUS "Building for x86 architecture") + set(CMAKE_SYSTEM_PROCESSOR "x86") + set(CMAKE_VS_PLATFORM_NAME "Win32") + add_definitions(-DTARGET_X86=1) +endif() -# Compiler flags for Debug and Release builds -target_compile_definitions(${PYTHON_EXTENSION_NAME} PRIVATE - $<$:_DEBUG> - $<$:NDEBUG> +set(PYTHON_LIBRARIES "${PYTHON_LIB_DIR}/python${PYTHON_VERSION}.lib") + +# Link Python library +message(STATUS "Using Python library: ${PYTHON_LIBRARIES}") +message(STATUS "Output PYD name: ddbc_bindings.cp${PYTHON_VERSION}-${WHEEL_ARCH}.pyd") +target_link_libraries(ddbc_bindings PRIVATE ${PYTHON_LIBRARIES}) + +# Compiler definitions +target_compile_definitions(ddbc_bindings PRIVATE + HAVE_SNPRINTF + _USE_MATH_DEFINES + PYBIND11_COMPILER_TYPE="_MSC_VER" + NOMINMAX ) - -# Print the current build type and pybind11 path for debugging purposes -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -message(STATUS "pybind11 directory: ${pybind11_DIR}") diff --git a/mssql_python/pybind/README.md b/mssql_python/pybind/README.md index 4b50e9b4..26e6708e 100644 --- a/mssql_python/pybind/README.md +++ b/mssql_python/pybind/README.md @@ -1,23 +1,32 @@ -# PyBind11 Build Instructions +# Build Instructions for Developers -This README provides instructions to build a project using PyBind11. +This README provides instructions to build the DDBC Bindings PYD for your system (supports Windows x64 and arm64). ## Prerequisites -1. **CMake**: Install CMake on your system - https://cmake.org/download/ -2. **PyBind11**: Install PyBind11 using pip. +1. **PyBind11**: Install PyBind11 using pip. ```sh pip install pybind11 ``` -3. **Visual Studio**: Install Visual Studio with the C++ development tools. - - Download Visual Studio from https://visualstudio.microsoft.com/ - - During installation, select the "Desktop development with C++" workload. +2. **Visual Studio Build Tools**: + - Download Visual Studio Build Tools from https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022 + - During installation, select the **`Desktop development with C++`** workload, this should also install **CMake**. -## Build Instructions +## Build Steps -1. Go to pybind folder and run `build.bat`: +1. Start **Developer Command Prompt for VS 2022**. + +2. Inside the Developer Command Prompt window, navigate to the pybind folder and run: ```sh build.bat ``` -2. The built `ddbc_bindings.pyd` file will be moved to the `mssql_python` directory. +### What happens inside the build script? + +- The script will: + - Clean up existing build directories + - Detect VS Build Tools Installation, and start compilation for your Python version and Windows architecture + - Compile `ddbc_bindings.cpp` using CMake and create properly versioned PYD file (`ddbc_bindings.cp313-amd64.pyd`) + - Move the built PYD file to the parent `mssql_python` directory + +- Finally, you can now run **main.py** to test \ No newline at end of file diff --git a/mssql_python/pybind/build.bat b/mssql_python/pybind/build.bat index cf7f4a57..e96c6a57 100644 --- a/mssql_python/pybind/build.bat +++ b/mssql_python/pybind/build.bat @@ -1,21 +1,169 @@ -:: Usage - -:: 1) "cd mssql_python/pybind" -:: 2)a) "build.bat" - Generates debug build of DDBC bindings -:: 2)b) "build.bat Release" - Generates release build of DDBC bindings -:: 2)c) "build.bat Debug" - Generates debug build of DDBC bindings - -rmdir /s /q build -mkdir build -cd build -del CMakeCache.txt - -IF "%1"=="" ( - set BUILD_TYPE=Debug -) ELSE ( - set BUILD_TYPE=%1 -) - -cmake -DPython3_EXECUTABLE="python3" -DCMAKE_BUILD_TYPE=%BUILD_TYPE% .. -msbuild ddbc_bindings.sln /p:Configuration=%BUILD_TYPE% -move /Y %BUILD_TYPE%\ddbc_bindings.pyd ..\..\ddbc_bindings.pyd -cd .. +@echo off +setlocal enabledelayedexpansion + +REM Usage: build.bat [ARCH], If ARCH is not specified, it defaults to x64. +set ARCH=%1 +if "%ARCH%"=="" set ARCH=x64 +echo [DIAGNOSTIC] Target Architecture set to: %ARCH% + +REM Clean up main build directory if it exists +echo Checking for main build directory... +if exist "build" ( + echo Removing existing build directory... + rd /s /q build + echo Build directory removed. +) + +REM Get Python version from active interpreter +for /f %%v in ('python -c "import sys; print(f'{sys.version_info.major}{sys.version_info.minor}')"') do set PYTAG=%%v + +echo =================================== +echo Building for: %ARCH% / Python %PYTAG% +echo =================================== + +REM Save absolute source directory +set SOURCE_DIR=%~dp0 + +REM Go to build output directory +set BUILD_DIR=%SOURCE_DIR%build\%ARCH%\py%PYTAG% +if exist "%BUILD_DIR%" rd /s /q "%BUILD_DIR%" +mkdir "%BUILD_DIR%" +cd /d "%BUILD_DIR%" +echo [DIAGNOSTIC] Changed to build directory: "%BUILD_DIR%" + +REM Set CMake platform name +set PLATFORM_NAME=%ARCH% +echo [DIAGNOSTIC] Setting up for architecture: %ARCH% +REM Set CMake platform name +if "%ARCH%"=="x64" ( + set PLATFORM_NAME=x64 + echo [DIAGNOSTIC] Using platform name: x64 +) else if "%ARCH%"=="x86" ( + set PLATFORM_NAME=Win32 + echo [DIAGNOSTIC] Using platform name: Win32 +) else if "%ARCH%"=="arm64" ( + set PLATFORM_NAME=ARM64 + echo [DIAGNOSTIC] Using platform name: ARM64 +) else ( + echo [ERROR] Invalid architecture: %ARCH% + exit /b 1 +) + +echo [DIAGNOSTIC] Source directory: "%SOURCE_DIR%" + +REM Check for Visual Studio - look in standard paths or check if vswhere is available +echo [DIAGNOSTIC] Searching for Visual Studio installation... + +set VS_PATH= + +REM Try direct paths first (most common locations) +for %%p in ( + "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" + "%ProgramFiles(x86)%\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" + "%ProgramFiles%\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" +) do ( + echo [DIAGNOSTIC] Checking path: %%p + if exist %%p ( + set VS_PATH=%%p + echo [SUCCESS] Found Visual Studio at: %%p + goto vs_found + ) +) + +REM If we reach here, we didn't find Visual Studio in the standard paths +echo [DIAGNOSTIC] Visual Studio not found in standard paths +echo [DIAGNOSTIC] Looking for vswhere.exe... + +REM Try using vswhere if available (common on Azure DevOps agents) +set VSWHERE_PATH="%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" +if exist %VSWHERE_PATH% ( + echo [DIAGNOSTIC] Found vswhere at: %VSWHERE_PATH% + for /f "usebackq tokens=*" %%i in (`%VSWHERE_PATH% -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do ( + set VS_DIR=%%i + if exist "%%i\VC\Auxiliary\Build\vcvarsall.bat" ( + set VS_PATH="%%i\VC\Auxiliary\Build\vcvarsall.bat" + echo [SUCCESS] Found Visual Studio using vswhere at: %%i\VC\Auxiliary\Build\vcvarsall.bat + goto vs_found + ) + ) +) + +echo [WARNING] Visual Studio not found in standard paths or using vswhere +echo [DIAGNOSTIC] Current directory structure: +dir "%ProgramFiles(x86)%\Microsoft Visual Studio" /s /b | findstr "vcvarsall.bat" +dir "%ProgramFiles%\Microsoft Visual Studio" /s /b | findstr "vcvarsall.bat" +echo [ERROR] Could not find Visual Studio installation +exit /b 1 + +:vs_found +REM Initialize MSVC toolchain +echo [DIAGNOSTIC] Initializing MSVC toolchain with: call %VS_PATH% %ARCH% +call %VS_PATH% %ARCH% +echo [DIAGNOSTIC] MSVC initialization exit code: %errorlevel% +if errorlevel 1 ( + echo [ERROR] Failed to initialize MSVC toolchain + exit /b 1 +) + +REM Now invoke CMake with correct source path (options first, path last!) +echo [DIAGNOSTIC] Running CMake configure with: cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% "%SOURCE_DIR:~0,-1%" +cmake -A %PLATFORM_NAME% -DARCHITECTURE=%ARCH% "%SOURCE_DIR:~0,-1%" +echo [DIAGNOSTIC] CMake configure exit code: %errorlevel% +if errorlevel 1 ( + echo [ERROR] CMake configuration failed + exit /b 1 +) + +echo [DIAGNOSTIC] Running CMake build with: cmake --build . --config Release +cmake --build . --config Release +echo [DIAGNOSTIC] CMake build exit code: %errorlevel% +if errorlevel 1 ( + echo [ERROR] CMake build failed + exit /b 1 +) +echo [SUCCESS] Build completed successfully. +echo ===== Build completed for %ARCH% Python %PYTAG% ====== + +@REM REM Call the external script to preserve only the target architecture odbc libs +@REM REM This is commented out to avoid running it automatically. Uncomment if needed. +@REM call "%SOURCE_DIR%keep_single_arch.bat" "%ARCH%" + +REM Copy the built .pyd file to source directory +set WHEEL_ARCH=%ARCH% +if "%WHEEL_ARCH%"=="x64" set WHEEL_ARCH=amd64 +if "%WHEEL_ARCH%"=="arm64" set WHEEL_ARCH=arm64 +if "%WHEEL_ARCH%"=="x86" set WHEEL_ARCH=win32 + +set PYD_NAME=ddbc_bindings.cp%PYTAG%-%WHEEL_ARCH%.pyd +set OUTPUT_DIR=%BUILD_DIR%\Release + +if exist "%OUTPUT_DIR%\%PYD_NAME%" ( + copy /Y "%OUTPUT_DIR%\%PYD_NAME%" "%SOURCE_DIR%\.." + echo [SUCCESS] Copied %PYD_NAME% to %SOURCE_DIR%.. + + setlocal enabledelayedexpansion + for %%I in ("%SOURCE_DIR%..") do ( + set PARENT_DIR=%%~fI + ) + echo [DIAGNOSTIC] Parent is: !PARENT_DIR! + + set VCREDIST_DLL_PATH=!PARENT_DIR!\libs\!ARCH!\vcredist\msvcp140.dll + echo [DIAGNOSTIC] Looking for msvcp140.dll at "!VCREDIST_DLL_PATH!" + + if exist "!VCREDIST_DLL_PATH!" ( + copy /Y "!VCREDIST_DLL_PATH!" "%SOURCE_DIR%\.." + echo [SUCCESS] Copied msvcp140.dll from !VCREDIST_DLL_PATH! to "%SOURCE_DIR%\.." + ) else ( + echo [ERROR] Could not find msvcp140.dll at "!VCREDIST_DLL_PATH!" + exit /b 1 + ) +) else ( + echo [ERROR] Could not find built .pyd file: %PYD_NAME% + REM Exit with an error code here if the .pyd file is not found + exit /b 1 +) + +endlocal diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index c237e2f0..742cd556 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -12,6 +12,10 @@ #include #include // std::forward +// Replace std::filesystem usage with Windows-specific headers +#include +#pragma comment(lib, "shlwapi.lib") + #include #include #include @@ -38,6 +42,11 @@ using namespace pybind11::literals; case x: \ return #x +// Architecture-specific defines +#ifndef ARCHITECTURE +#define ARCHITECTURE "win64" // Default to win64 if not defined during compilation +#endif + //------------------------------------------------------------------------------------------------- // Class definitions //------------------------------------------------------------------------------------------------- @@ -201,6 +210,18 @@ SQLFreeStmtFunc SQLFreeStmt_ptr = nullptr; // Diagnostic APIs SQLGetDiagRecFunc SQLGetDiagRec_ptr = nullptr; +// Move GetModuleDirectory outside namespace to resolve ambiguity +std::string GetModuleDirectory() { + py::object module = py::module::import("mssql_python"); + py::object module_path = module.attr("__file__"); + std::string module_file = module_path.cast(); + + char path[MAX_PATH]; + strncpy_s(path, MAX_PATH, module_file.c_str(), module_file.length()); + PathRemoveFileSpecA(path); + return std::string(path); +} + // Smart wrapper around SQLHANDLE class SqlHandle { public: @@ -247,48 +268,69 @@ void ThrowStdException(const std::string& message) { throw std::runtime_error(me // Helper to load the driver // TODO: We don't need to do explicit linking using LoadLibrary. We can just use implicit // linking to load this DLL. It will simplify the code a lot. -void LoadDriverOrThrowException() { - HMODULE hDdbcModule; - wchar_t ddbcModulePath[MAX_PATH]; - // Get the path to DDBC module: - // GetModuleHandleExW returns a handle to current shared library (ddbc_bindings.pyd) given a - // function from the library (LoadDriverOrThrowException). GetModuleFileNameW takes in the - // library handle (hDdbcModule) & returns the full path to this library (ddbcModulePath) - if (GetModuleHandleExW( - GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - (LPWSTR)&LoadDriverOrThrowException, &hDdbcModule) && - GetModuleFileNameW(hDdbcModule, ddbcModulePath, MAX_PATH)) { - // Look for last occurence of '\' in the path and set it to null - wchar_t* lastBackSlash = wcsrchr(ddbcModulePath, L'\\'); - if (lastBackSlash == nullptr) { - LOG("Invalid DDBC module path - %S", ddbcModulePath); - ThrowStdException("Failed to load driver"); - } - *lastBackSlash = 0; +std::wstring LoadDriverOrThrowException(const std::wstring& modulePath = L"") { + std::wstring ddbcModulePath = modulePath; + if (ddbcModulePath.empty()) { + // Get the module path if not provided + std::string path = GetModuleDirectory(); + ddbcModulePath = std::wstring(path.begin(), path.end()); + } + + std::wstring dllDir = ddbcModulePath; + dllDir += L"\\libs\\"; + + // Convert ARCHITECTURE macro to wstring + std::wstring archStr(ARCHITECTURE, ARCHITECTURE + strlen(ARCHITECTURE)); + + // Map architecture identifiers to correct subdirectory names + std::wstring archDir; + if (archStr == L"win64" || archStr == L"amd64" || archStr == L"x64") { + archDir = L"x64"; + } else if (archStr == L"arm64") { + archDir = L"arm64"; } else { - LOG("Failed to get DDBC module path. Error code - %d", GetLastError()); - ThrowStdException("Failed to load driver"); + archDir = L"x86"; } + dllDir += archDir; + std::wstring mssqlauthDllPath = dllDir + L"\\mssql-auth.dll"; + dllDir += L"\\msodbcsql18.dll"; // Preload mssql-auth.dll from the same path if available // TODO: Only load mssql-auth.dll if using Entra ID Authentication modes (Active Directory modes) - std::wstring authDllDir = std::wstring(ddbcModulePath) + L"\\libs\\win\\mssql-auth.dll"; - HMODULE hAuthModule = LoadLibraryW(authDllDir.c_str()); + HMODULE hAuthModule = LoadLibraryW(mssqlauthDllPath.c_str()); if (hAuthModule) { - LOG("Authentication library loaded successfully from - {}", authDllDir.c_str()); + LOG("Authentication library loaded successfully from - {}", mssqlauthDllPath.c_str()); } else { - LOG("Note: Authentication library not found at - {}. This is OK if you're not using Entra ID Authentication.", authDllDir.c_str()); + LOG("Note: Authentication library not found at - {}. This is OK if you're not using Entra ID Authentication.", mssqlauthDllPath.c_str()); } - // Look for msodbcsql18.dll in a path relative to DDBC module - std::wstring dllDir = std::wstring(ddbcModulePath) + L"\\libs\\win\\msodbcsql18.dll"; + // Convert wstring to string for logging + std::string dllDirStr(dllDir.begin(), dllDir.end()); + LOG("Attempting to load driver from - {}", dllDirStr); + HMODULE hModule = LoadLibraryW(dllDir.c_str()); if (!hModule) { - LOG("LoadLibraryW failed to load driver from - %S", dllDir.c_str()); - ThrowStdException("Failed to load driver"); + // Failed to load the DLL, get the error message + DWORD error = GetLastError(); + char* messageBuffer = nullptr; + size_t size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + error, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&messageBuffer, + 0, + NULL + ); + std::string errorMessage = messageBuffer ? std::string(messageBuffer, size) : "Unknown error"; + LocalFree(messageBuffer); + + // Log the error message + LOG("Failed to load the driver with error code: {} - {}", error, errorMessage); + ThrowStdException("Failed to load the ODBC driver. Please check that it is installed correctly."); } - LOG("Driver loaded successfully from - {}", dllDir.c_str()); + // If we got here, we've successfully loaded the DLL. Now get the function pointers. // Environment and handle function loading SQLAllocHandle_ptr = (SQLAllocHandleFunc)GetProcAddress(hModule, "SQLAllocHandle"); SQLSetEnvAttr_ptr = (SQLSetEnvAttrFunc)GetProcAddress(hModule, "SQLSetEnvAttr"); @@ -331,16 +373,18 @@ void LoadDriverOrThrowException() { SQLSetStmtAttr_ptr && SQLGetConnectAttr_ptr && SQLDriverConnect_ptr && SQLExecDirect_ptr && SQLPrepare_ptr && SQLBindParameter_ptr && SQLExecute_ptr && SQLRowCount_ptr && SQLGetStmtAttr_ptr && SQLSetDescField_ptr && SQLFetch_ptr && - SQLFetchScroll_ptr && SQLGetData_ptr && SQLNumResultCols_ptr && - SQLBindCol_ptr && SQLDescribeCol_ptr && SQLMoreResults_ptr && - SQLColAttribute_ptr && SQLEndTran_ptr && SQLFreeHandle_ptr && - SQLDisconnect_ptr && SQLFreeStmt_ptr && SQLGetDiagRec_ptr; + SQLFetchScroll_ptr && SQLGetData_ptr && SQLNumResultCols_ptr && + SQLBindCol_ptr && SQLDescribeCol_ptr && SQLMoreResults_ptr && + SQLColAttribute_ptr && SQLEndTran_ptr && SQLFreeHandle_ptr && + SQLDisconnect_ptr && SQLFreeStmt_ptr && SQLGetDiagRec_ptr; if (!success) { - LOG("Failed to load required function pointers from driver - %S", dllDir.c_str()); + LOG("Failed to load required function pointers from driver - {}", dllDirStr); ThrowStdException("Failed to load required function pointers from driver"); } - LOG("Sucessfully loaded function pointers from driver"); + LOG("Successfully loaded function pointers from driver"); + + return dllDir; } const char* GetSqlCTypeAsString(const SQLSMALLINT cType) { @@ -369,7 +413,7 @@ const char* GetSqlCTypeAsString(const SQLSMALLINT cType) { STRINGIFY_FOR_CASE(SQL_C_GUID); STRINGIFY_FOR_CASE(SQL_C_DEFAULT); default: - return "Unkown"; + return "Unknown"; } } @@ -2028,10 +2072,25 @@ SQLLEN SQLRowCount_wrap(SqlHandlePtr StatementHandle) { return rowCount; } +// Architecture-specific defines +#ifndef ARCHITECTURE +#define ARCHITECTURE "win64" // Default to win64 if not defined during compilation +#endif + // Functions/data to be exposed to Python as a part of ddbc_bindings module PYBIND11_MODULE(ddbc_bindings, m) { m.doc() = "msodbcsql driver api bindings for Python"; + + // Add architecture information as module attribute + m.attr("__architecture__") = ARCHITECTURE; + + // Expose architecture-specific constants + m.attr("ARCHITECTURE") = ARCHITECTURE; + + // Expose the C++ functions to Python m.def("ThrowStdException", &ThrowStdException); + + // Define parameter info class py::class_(m, "ParamInfo") .def(py::init<>()) .def_readwrite("inputOutputType", &ParamInfo::inputOutputType) @@ -2039,6 +2098,8 @@ PYBIND11_MODULE(ddbc_bindings, m) { .def_readwrite("paramSQLType", &ParamInfo::paramSQLType) .def_readwrite("columnSize", &ParamInfo::columnSize) .def_readwrite("decimalDigits", &ParamInfo::decimalDigits); + + // Define numeric data class py::class_(m, "NumericData") .def(py::init<>()) .def(py::init()) @@ -2046,11 +2107,15 @@ PYBIND11_MODULE(ddbc_bindings, m) { .def_readwrite("scale", &NumericData::scale) .def_readwrite("sign", &NumericData::sign) .def_readwrite("val", &NumericData::val); + + // Define error info class py::class_(m, "ErrorInfo") .def_readwrite("sqlState", &ErrorInfo::sqlState) .def_readwrite("ddbcErrorMsg", &ErrorInfo::ddbcErrorMsg); + py::class_(m, "SqlHandle") .def("free", &SqlHandle::free); + m.def("DDBCSQLAllocHandle", [](SQLSMALLINT HandleType, SqlHandlePtr InputHandle = nullptr) { SqlHandlePtr OutputHandle; SQLRETURN rc = SQLAllocHandle_wrap(HandleType, InputHandle, OutputHandle); @@ -2085,4 +2150,15 @@ PYBIND11_MODULE(ddbc_bindings, m) { m.def("DDBCSQLFreeHandle", &SQLFreeHandle_wrap, "Free a handle"); m.def("DDBCSQLDisconnect", &SQLDisconnect_wrap, "Disconnect from a data source"); m.def("DDBCSQLCheckError", &SQLCheckError_Wrap, "Check for driver errors"); + + // Add a version attribute + m.attr("__version__") = "1.0.0"; + + try { + // Try loading the ODBC driver when the module is imported + LoadDriverOrThrowException(); + } catch (const std::exception& e) { + // Log the error but don't throw - let the error happen when functions are called + LOG("Failed to load ODBC driver during module initialization: {}", e.what()); + } } diff --git a/mssql_python/pybind/keep_single_arch.bat b/mssql_python/pybind/keep_single_arch.bat new file mode 100644 index 00000000..a6368fd1 --- /dev/null +++ b/mssql_python/pybind/keep_single_arch.bat @@ -0,0 +1,38 @@ +@echo off +REM keep_single_arch.bat - Preserves only the target architecture odbc libs and removes others - for packaging +REM This script is intended to be run after the build process to clean up unnecessary architecture libraries. +REM Usage: keep_single_arch.bat [Architecture] + +setlocal + +set ARCH=%1 + +if "%ARCH%"=="" ( + echo [ERROR] Architecture must be provided + exit /b 1 +) + +echo Removing unnecessary architecture libraries for packaging... + +REM Get the directory where this script is located +set SCRIPT_DIR=%~dp0 +set LIBS_BASE_DIR=%SCRIPT_DIR%..\libs + +if "%ARCH%"=="x64" ( + echo Removing "%LIBS_BASE_DIR%\x86" and "%LIBS_BASE_DIR%\arm64" directories + if exist "%LIBS_BASE_DIR%\x86" rd /s /q "%LIBS_BASE_DIR%\x86" + if exist "%LIBS_BASE_DIR%\arm64" rd /s /q "%LIBS_BASE_DIR%\arm64" + echo Kept x64, removed other architectures. +) else if "%ARCH%"=="x86" ( + echo Removing "%LIBS_BASE_DIR%\x64" and "%LIBS_BASE_DIR%\arm64" directories + if exist "%LIBS_BASE_DIR%\x64" rd /s /q "%LIBS_BASE_DIR%\x64" + if exist "%LIBS_BASE_DIR%\arm64" rd /s /q "%LIBS_BASE_DIR%\arm64" + echo Kept x86, removed other architectures. +) else if "%ARCH%"=="arm64" ( + echo Removing "%LIBS_BASE_DIR%\x64" and "%LIBS_BASE_DIR%\x86" directories + if exist "%LIBS_BASE_DIR%\x64" rd /s /q "%LIBS_BASE_DIR%\x64" + if exist "%LIBS_BASE_DIR%\x86" rd /s /q "%LIBS_BASE_DIR%\x86" + echo Kept arm64, removed other architectures. +) + +endlocal diff --git a/setup.py b/setup.py index 457b5682..37dbb5a2 100644 --- a/setup.py +++ b/setup.py @@ -1,65 +1,112 @@ import os import sys -import subprocess -from setuptools import setup, Extension, find_packages -from setuptools.command.build_ext import build_ext +from setuptools import setup, find_packages +from setuptools.dist import Distribution +from wheel.bdist_wheel import bdist_wheel -class CMakeExtension(Extension): - def __init__(self, name, sourcedir=''): - # No sources; CMake handles the build. - super().__init__(name, sources=[]) - self.sourcedir = os.path.abspath(sourcedir) +# Custom distribution to force platform-specific wheel +class BinaryDistribution(Distribution): + def has_ext_modules(self): + return True -class CMakeBuild(build_ext): - def run(self): - # Check if CMake is installed - try: - subprocess.check_output(['cmake', '--version']) - except OSError: - raise RuntimeError("CMake must be installed to build these extensions.") - for ext in self.extensions: - self.build_extension(ext) +# Custom bdist_wheel command to override platform tag +class CustomBdistWheel(bdist_wheel): + def finalize_options(self): + # Call the original finalize_options first to initialize self.bdist_dir + bdist_wheel.finalize_options(self) + + # Override the platform tag with our custom one based on ARCHITECTURE env var + if sys.platform.startswith('win'): + # Strip quotes if present + arch = os.environ.get('ARCHITECTURE', 'x64') + if isinstance(arch, str): + arch = arch.strip('"\'') + + print(f"Architecture from environment: '{arch}'") + + if arch in ['x86', 'win32']: + self.plat_name = "win32" + platform_dir = "win32" + elif arch == 'arm64': + self.plat_name = "win_arm64" + platform_dir = "win_arm64" + else: # Default to x64/amd64 + self.plat_name = "win_amd64" + platform_dir = "win_amd64" + + # Override the plat_name for the wheel + print(f"Setting wheel platform tag to: {self.plat_name}") + + # Force platform-specific paths if bdist_dir is already set + if self.bdist_dir and "win-amd64" in self.bdist_dir: + self.bdist_dir = self.bdist_dir.replace("win-amd64", f"win-{platform_dir}") + print(f"Using build directory: {self.bdist_dir}") - def build_extension(self, ext): - # Calculate the directory where the final .pyd will be placed (inside mssql_python) - extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) - cfg = 'Debug' if self.debug else 'Release' - cmake_args = [ - '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir, - '-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE=' + extdir, - '-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE=' + extdir, - '-DPYTHON_EXECUTABLE=' + sys.executable, - '-DCMAKE_BUILD_TYPE=' + cfg - ] - build_args = ['--config', cfg] +# Find all packages in the current directory +packages = find_packages() + +# Determine the architecture and platform tag for the wheel +if sys.platform.startswith('win'): + # Get architecture from environment variable or default to x64 + arch = os.environ.get('ARCHITECTURE', 'x64') + # Strip quotes if present + if isinstance(arch, str): + arch = arch.strip('"\'') + + # Normalize architecture values + if arch in ['x86', 'win32']: + arch = 'x86' + platform_tag = 'win32' + elif arch == 'arm64': + platform_tag = 'win_arm64' + else: # Default to x64/amd64 + arch = 'x64' + platform_tag = 'win_amd64' - if not os.path.exists(self.build_temp): - os.makedirs(self.build_temp) + print(f"Detected architecture: {arch} (platform tag: {platform_tag})") - # Configure CMake project - subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp) - # Build the target defined in your CMakeLists.txt - subprocess.check_call(['cmake', '--build', '.', '--target', 'ddbc_bindings'] + build_args, cwd=self.build_temp) + # Add architecture-specific packages + packages.extend([ + f'mssql_python.libs.{arch}', + f'mssql_python.libs.{arch}.1033', + f'mssql_python.libs.{arch}.vcredist' + ]) +else: + platform_tag = 'any' # Fallback setup( name='mssql-python', - version='0.1.5', + version='0.1.6', description='A Python library for interacting with Microsoft SQL Server', long_description=open('README.md', encoding='utf-8').read(), long_description_content_type='text/markdown', author='Microsoft Corporation', author_email='pysqldriver@microsoft.com', url='https://github.com/microsoft/mssql-python', - packages=find_packages(), + packages=packages, package_data={ - # Include DLL files inside mssql_python - 'mssql_python': ['libs/*', 'libs/**/*', '*.dll'] + # Include PYD and DLL files inside mssql_python, exclude YML files + 'mssql_python': [ + 'ddbc_bindings.cp*.pyd', # Include all PYD files + 'libs/*', + 'libs/**/*', + '*.dll' + ] }, include_package_data=True, - # Requires Python 3.13 - python_requires='==3.13.*', - # Naming the extension as mssql_python.ddbc_bindings puts the .pyd directly in mssql_python - ext_modules=[CMakeExtension('mssql_python.ddbc_bindings', sourcedir='mssql_python/pybind')], - cmdclass={'build_ext': CMakeBuild}, + # Requires >= Python 3.9 + python_requires='>=3.9', zip_safe=False, + # Force binary distribution + distclass=BinaryDistribution, + exclude_package_data={ + '': ['*.yml', '*.yaml'], # Exclude YML files + 'mssql_python': [ + 'libs/*/vcredist/*', 'libs/*/vcredist/**/*', # Exclude vcredist directories, added here since `'libs/*' is already included` + ], + }, + # Register custom commands + cmdclass={ + 'bdist_wheel': CustomBdistWheel, + }, )