diff --git a/.github/workflows/build-shared-spanner-lib.yml b/.github/workflows/build-shared-spanner-lib.yml new file mode 100644 index 000000000..a129747ae --- /dev/null +++ b/.github/workflows/build-shared-spanner-lib.yml @@ -0,0 +1,67 @@ +name: Build Shared SpannerLib + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + strategy: + matrix: + go-version: ['1.25.x'] + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: ubuntu-latest + target: linux + - os: macos-latest + target: macos + - os: windows-latest + target: windows + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Install Dependencies (Linux) + if: matrix.target == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build Binaries + shell: bash + run: | + cd spannerlib/shared + if [ "${{ matrix.target }}" == "linux" ]; then + export SKIP_MACOS=true + export SKIP_WINDOWS=true + # Enable ARM64 build + export BUILD_LINUX_ARM64=true + export CC_LINUX_ARM64=aarch64-linux-gnu-gcc + elif [ "${{ matrix.target }}" == "macos" ]; then + export SKIP_LINUX=true + export SKIP_WINDOWS=true + # Enable AMD64 build (if possible) + export BUILD_MACOS_AMD64=true + elif [ "${{ matrix.target }}" == "windows" ]; then + export SKIP_LINUX=true + export SKIP_MACOS=true + fi + ./build-binaries.sh + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: spannerlib-binaries-${{ matrix.target }} + path: spannerlib/shared/binaries/ diff --git a/.github/workflows/python-spanner-lib-wrapper-lint.yml b/.github/workflows/python-spanner-lib-wrapper-lint.yml new file mode 100644 index 000000000..c82150874 --- /dev/null +++ b/.github/workflows/python-spanner-lib-wrapper-lint.yml @@ -0,0 +1,37 @@ +name: Python Wrapper Lint + +on: + push: + branches: [ "main" ] + paths: + - 'spannerlib/wrappers/spannerlib-python/**' + pull_request: + branches: [ "main" ] + paths: + - 'spannerlib/wrappers/spannerlib-python/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: spannerlib/wrappers/spannerlib-python/spannerlib-python + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install Nox + run: pip install nox + + - name: Run Lint + run: nox -s lint diff --git a/.github/workflows/python-spanner-lib-wrapper-system-tests.yml b/.github/workflows/python-spanner-lib-wrapper-system-tests.yml new file mode 100644 index 000000000..6a6bdbdc8 --- /dev/null +++ b/.github/workflows/python-spanner-lib-wrapper-system-tests.yml @@ -0,0 +1,48 @@ +name: Python Wrapper System Tests on Emulator + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + system-tests: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + defaults: + run: + working-directory: spannerlib/wrappers/spannerlib-python/spannerlib-python + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.x' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install Nox + run: pip install nox + + - name: Run System Tests + env: + SPANNER_EMULATOR_HOST: localhost:9010 + shell: bash + run: | + docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator + sleep 10 + nox -s system-${{ matrix.python-version }} diff --git a/.github/workflows/python-spanner-lib-wrapper-unit-tests.yml b/.github/workflows/python-spanner-lib-wrapper-unit-tests.yml new file mode 100644 index 000000000..07c163410 --- /dev/null +++ b/.github/workflows/python-spanner-lib-wrapper-unit-tests.yml @@ -0,0 +1,38 @@ +name: Python Wrapper Unit Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + workflow_dispatch: + +permissions: + contents: read + +jobs: + unit-tests: + runs-on: ubuntu-latest + defaults: + run: + working-directory: spannerlib/wrappers/spannerlib-python/spannerlib-python + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.x' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install Nox + run: pip install nox + + - name: Run Unit Tests + run: nox -s unit-3.10 diff --git a/.github/workflows/release-python-spanner-lib-wrapper.yml b/.github/workflows/release-python-spanner-lib-wrapper.yml new file mode 100644 index 000000000..e905c1f52 --- /dev/null +++ b/.github/workflows/release-python-spanner-lib-wrapper.yml @@ -0,0 +1,83 @@ +name: Build and Release Python Wrapper + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + build-shared-lib: + uses: ./.github/workflows/build-shared-spanner-lib.yml + + build-and-publish-wrapper: + needs: build-shared-lib + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/spannerlib-python + timeout-minutes: 10 + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Download Shared Library Artifacts + uses: actions/download-artifact@v4 + with: + path: binaries + pattern: spannerlib* + merge-multiple: true + + - name: Display Downloaded Files + run: ls -R binaries + + - name: Copy Binaries to Lib + run: | + mkdir -p spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/lib + cp -r binaries/* spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/lib/ + ls -R spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/lib + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install Build Dependencies + run: pip install build twine + + - name: Build Wheel + run: | + cd spannerlib/wrappers/spannerlib-python/spannerlib-python + python -m build + + - name: Check Wheels + run: twine check spannerlib/wrappers/spannerlib-python/spannerlib-python/dist/* + + - name: Verify Installation + run: | + # Create a fresh virtual environment + python -m venv test-env + source test-env/bin/activate + + # Install the built wheel + pip install spannerlib/wrappers/spannerlib-python/spannerlib-python/dist/*.whl + + # Verify import + python -c "import google.cloud.spannerlib; print('Successfully imported wrapper from:', google.cloud.spannerlib.__file__)" + + - name: Upload Wheel Artifacts + uses: actions/upload-artifact@v4 + with: + name: python-wheels + path: spannerlib/wrappers/spannerlib-python/spannerlib-python/dist/* + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: spannerlib/wrappers/spannerlib-python/spannerlib-python/dist/ + verbose: true diff --git a/spannerlib/shared/build-binaries.sh b/spannerlib/shared/build-binaries.sh index 0a36f06f6..3973f371e 100755 --- a/spannerlib/shared/build-binaries.sh +++ b/spannerlib/shared/build-binaries.sh @@ -1,48 +1,116 @@ +#!/bin/bash +set -e + # Builds the shared library binaries and copies the binaries to OS/arch specific folders. -# Binaries can be built for linux/x64, darwin/arm64, and windows/x64. -# Which ones are actually built depends on the values of the following variables: -# SKIP_MACOS: If set, will skip the darwin/arm64 build -# SKIP_LINUX: If set, will skip the linux/x64 build that uses the default C compiler on the system -# SKIP_LINUX_CROSS_COMPILE: If set, will skip the linux/x64 build that uses the x86_64-unknown-linux-gnu-gcc C compiler. -# This compiler is used when compiling for linux/x64 on MacOS. -# SKIP_WINDOWS: If set, will skip the windows/x64 build. - -# The binaries are stored in the following files: -# binaries/osx-arm64/spannerlib.dylib -# binaries/linux-x64/spannerlib.so -# binaries/win-x64/spannerlib.dll - -echo "Skip macOS: $SKIP_MACOS" -echo "Skip Linux: $SKIP_LINUX" -echo "Skip Linux cross compile: $SKIP_LINUX_CROSS_COMPILE" -echo "Skip windows: $SKIP_WINDOWS" +# Binaries can be built for linux/x64, linux/arm64, darwin/arm64, darwin/amd64, and windows/x64. +# +# Environment Variables: +# SKIP_MACOS: If set, will skip all macOS builds. +# SKIP_LINUX: If set, will skip all Linux builds. +# SKIP_WINDOWS: If set, will skip all Windows builds. +# SKIP_LINUX_CROSS_COMPILE: If set, will skip the Linux x64 cross-compile (useful when running on Linux). +# BUILD_MACOS_AMD64: If set, will build for macOS AMD64. +# BUILD_LINUX_ARM64: If set, will build for Linux ARM64. +# CC_LINUX_ARM64: Compiler for Linux ARM64 (default: aarch64-linux-gnu-gcc). + +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} + +build_artifact() { + local os=$1 + local arch=$2 + local output_dir=$3 + local output_file=$4 + local cc=$5 + + log "Building for $os/$arch..." + mkdir -p "$output_dir" + + if [ -n "$cc" ]; then + export CC="$cc" + else + unset CC + fi + + GOOS="$os" GOARCH="$arch" CGO_ENABLED=1 go build -o "$output_dir/$output_file" -buildmode=c-shared shared_lib.go + log "Successfully built $output_dir/$output_file" +} +# Detect current OS to set smart defaults +CURRENT_OS=$(uname -s) + +log "Current OS: $CURRENT_OS" +log "Skip macOS: ${SKIP_MACOS:-false}" +log "Skip Linux: ${SKIP_LINUX:-false}" +log "Skip Linux cross compile: ${SKIP_LINUX_CROSS_COMPILE:-false}" +log "Skip Windows: ${SKIP_WINDOWS:-false}" + +# --- MacOS Builds --- if [ -z "$SKIP_MACOS" ]; then - echo "Building for darwin/arm64" - mkdir -p binaries/osx-arm64 - GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -o binaries/osx-arm64/spannerlib.dylib -buildmode=c-shared shared_lib.go + # MacOS ARM64 (Apple Silicon) + build_artifact "darwin" "arm64" "binaries/osx-arm64" "spannerlib.dylib" "" + + # MacOS AMD64 (Intel) + if [ -n "$BUILD_MACOS_AMD64" ]; then + build_artifact "darwin" "amd64" "binaries/osx-x64" "spannerlib.dylib" "" + fi fi -if [ -z "$SKIP_LINUX_CROSS_COMPILE" ]; then - # The following software is needed for this build, assuming that the build runs on MacOS. - #brew tap SergioBenitez/osxct - #brew install x86_64-unknown-linux-gnu - echo "Building for linux/x64 (cross-compile)" - mkdir -p binaries/linux-x64 - CC=x86_64-unknown-linux-gnu-gcc GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o binaries/linux-x64/spannerlib.so -buildmode=c-shared shared_lib.go -elif [ -z "$SKIP_LINUX" ]; then - # The following commands assume that the script is running on linux/x64, or at least on some system that is able - # to compile to linux/x64 with the default C compiler on the system. - echo "Building for linux/x64" - mkdir -p binaries/linux-x64 - GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build -o binaries/linux-x64/spannerlib.so -buildmode=c-shared shared_lib.go +# --- Linux Builds --- +if [ -z "$SKIP_LINUX" ]; then + # Linux x64 + # Logic: If we are on Linux, we prefer native build. + # If we are NOT on Linux (e.g. Mac), we try cross-compile unless skipped. + + if [ "$CURRENT_OS" == "Linux" ]; then + # Native Linux build + build_artifact "linux" "amd64" "binaries/linux-x64" "spannerlib.so" "" + else + # Cross-compile for Linux x64 (e.g. from Mac) + if [ -z "$SKIP_LINUX_CROSS_COMPILE" ]; then + # Check for cross-compiler + if command -v x86_64-unknown-linux-gnu-gcc >/dev/null 2>&1; then + build_artifact "linux" "amd64" "binaries/linux-x64" "spannerlib.so" "x86_64-unknown-linux-gnu-gcc" + else + log "WARNING: x86_64-unknown-linux-gnu-gcc not found. Skipping Linux x64 cross-compile." + fi + elif [ -z "$SKIP_LINUX" ]; then + # Fallback to standard build if cross-compile explicitly skipped but Linux not skipped + # This might fail if no suitable compiler is found, but we'll try. + build_artifact "linux" "amd64" "binaries/linux-x64" "spannerlib.so" "" + fi + fi + + # Linux ARM64 + if [ -n "$BUILD_LINUX_ARM64" ]; then + CC_ARM64=${CC_LINUX_ARM64:-aarch64-linux-gnu-gcc} + if command -v "$CC_ARM64" >/dev/null 2>&1; then + build_artifact "linux" "arm64" "binaries/linux-arm64" "spannerlib.so" "$CC_ARM64" + else + log "WARNING: $CC_ARM64 not found. Skipping Linux ARM64 build." + fi + fi fi +# --- Windows Builds --- if [ -z "$SKIP_WINDOWS" ]; then - # This build requires mingw-w64 or a similar Windows compatible C compiler if it is being executed on a - # non-Windows environment. - # brew install mingw-w64 - echo "Building for windows/x64" - mkdir -p binaries/win-x64 - CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -o binaries/win-x64/spannerlib.dll -buildmode=c-shared shared_lib.go + # Windows x64 + CC_WIN="x86_64-w64-mingw32-gcc" + + # If running on Windows (Git Bash/MSYS2), we might not need the cross-compiler prefix or it might be different. + # But usually 'gcc' on Windows targets Windows. + if [[ "$CURRENT_OS" == *"MINGW"* ]] || [[ "$CURRENT_OS" == *"CYGWIN"* ]] || [[ "$CURRENT_OS" == *"MSYS"* ]]; then + # Native Windows build + build_artifact "windows" "amd64" "binaries/win-x64" "spannerlib.dll" "" + else + # Cross-compile for Windows + if command -v "$CC_WIN" >/dev/null 2>&1; then + build_artifact "windows" "amd64" "binaries/win-x64" "spannerlib.dll" "$CC_WIN" + else + log "WARNING: $CC_WIN not found. Skipping Windows x64 build." + fi + fi fi + +log "Build process completed." diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/build-shared-lib.sh b/spannerlib/wrappers/spannerlib-python/spannerlib-python/build-shared-lib.sh index eeef89569..394696d9e 100755 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/build-shared-lib.sh +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/build-shared-lib.sh @@ -1,81 +1,89 @@ #!/bin/bash +set -e # Builds the shared library and copies the binaries to the appropriate folders for # the Python wrapper. -# -# Binaries can be built for -# linux/x64, -# darwin/arm64, and -# windows/x64. # -# Which ones are actually built depends on the values of the following variables: -# SKIP_MACOS: If set, will skip the darwin/arm64 build -# SKIP_LINUX: If set, will skip the linux/x64 build that uses the default C compiler on the system -# SKIP_LINUX_CROSS_COMPILE: If set, will skip the linux/x64 build that uses the x86_64-unknown-linux-gnu-gcc C compiler. -# This compiler is used when compiling for linux/x64 on MacOS. -# SKIP_WINDOWS: If set, will skip the windows/x64 build. - -# Fail execution if any command errors out -set -e +# This script delegates the actual build process to spannerlib/shared/build-binaries.sh +# and then copies the artifacts to google/cloud/spannerlib/internal/lib. -echo -e "Build Spannerlib Shared Lib" +log() { + echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" +} -echo -e "RUNNER_OS DIR: $RUNNER_OS" -# Determine which builds to skip when the script runs on GitHub Actions. -if [ "$RUNNER_OS" == "Windows" ]; then - # Windows does not support any cross-compiling. - export SKIP_MACOS=true - export SKIP_LINUX=true - export SKIP_LINUX_CROSS_COMPILE=true -elif [ "$RUNNER_OS" == "macOS" ]; then - # When running on macOS, cross-compiling is supported. - # We skip the 'normal' Linux build (the one that does not explicitly set a C compiler). - export SKIP_LINUX=true -elif [ "$RUNNER_OS" == "Linux" ]; then - # Linux does not (yet) support cross-compiling to MacOS. - # In addition, we use the 'normal' Linux build when we are already running on Linux. - export SKIP_MACOS=true - export SKIP_LINUX_CROSS_COMPILE=true -fi - -SHARED_LIB_DIR="../../../shared" -TARGET_WRAPPER_DIR="../wrappers/spannerlib-python/spannerlib-python" -ARTIFACTS_DIR="spannerlib-artifacts" +log "Starting Spannerlib Shared Library Build for Python Wrapper..." -cd "$SHARED_LIB_DIR" || exit 1 +# Resolve absolute paths +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SHARED_LIB_DIR="$(cd "$SCRIPT_DIR/../../../shared" && pwd)" +TARGET_LIB_DIR="$SCRIPT_DIR/google/cloud/spannerlib/internal/lib" -./build-binaries.sh +log "Script Directory: $SCRIPT_DIR" +log "Shared Lib Directory: $SHARED_LIB_DIR" +log "Target Lib Directory: $TARGET_LIB_DIR" +log "Runner OS: ${RUNNER_OS:-Unknown}" -echo -e "PREPARING ARTIFACTS IN: $(pwd)" -# Navigate to the correct wrapper directory -cd "$TARGET_WRAPPER_DIR" || exit 1 - -echo -e "PREPARING ARTIFACTS IN: $(pwd)" +# Auto-detect RUNNER_OS if not set (e.g. local run) +if [ -z "$RUNNER_OS" ]; then + case "$(uname -s)" in + Linux*) RUNNER_OS="Linux";; + Darwin*) RUNNER_OS="macOS";; + CYGWIN*|MINGW*|MSYS*) RUNNER_OS="Windows";; + *) RUNNER_OS="Unknown";; + esac + log "Auto-detected RUNNER_OS: $RUNNER_OS" +fi -# Cleanup old artifacts if they exist -if [ -d "$ARTIFACTS_DIR" ]; then - rm -rf "$ARTIFACTS_DIR" +# Determine which builds to skip/enable based on the runner OS. +# This is primarily for CI (GitHub Actions). Local runs might not have RUNNER_OS set. +if [ -n "$RUNNER_OS" ]; then + if [[ "$RUNNER_OS" == "Windows" ]] || [[ "$RUNNER_OS" == "Windows_NT" ]]; then + # Windows runners usually can't cross-compile to Linux/Mac easily without extra setup + export SKIP_MACOS=true + export SKIP_LINUX=true + export SKIP_LINUX_CROSS_COMPILE=true + elif [[ "$RUNNER_OS" == "macOS" ]]; then + # macOS can cross-compile to Linux x64 (if toolchain exists) but not usually to Windows or Linux ARM64 easily + # We skip the 'native' Linux build + export SKIP_LINUX=true + # We might want to enable macOS AMD64 build if running on ARM64, or vice versa + # For now, let's assume we want both if possible, or rely on build-binaries.sh defaults + export BUILD_MACOS_AMD64=true + elif [[ "$RUNNER_OS" == "Linux" ]]; then + # Linux runners + export SKIP_MACOS=true + export SKIP_LINUX_CROSS_COMPILE=true + # Enable Linux ARM64 build if cross-compiler is available (checked in build-binaries.sh) + export BUILD_LINUX_ARM64=true + fi fi -mkdir -p "$ARTIFACTS_DIR" +# Execute the shared library build script +log "Executing build-binaries.sh in $SHARED_LIB_DIR..." +pushd "$SHARED_LIB_DIR" > /dev/null +./build-binaries.sh +popd > /dev/null -if [ -z "$SKIP_MACOS" ]; then -echo "Copying MacOS binaries..." - mkdir -p "$ARTIFACTS_DIR/osx-arm64" - cp "$SHARED_LIB_DIR/binaries/osx-arm64/spannerlib.dylib" "$ARTIFACTS_DIR/osx-arm64/spannerlib.dylib" - cp "$SHARED_LIB_DIR/binaries/osx-arm64/spannerlib.h" "$ARTIFACTS_DIR/osx-arm64/spannerlib.h" +# Prepare target directory +if [ -d "$TARGET_LIB_DIR" ]; then + log "Cleaning up existing target directory: $TARGET_LIB_DIR" + rm -rf "$TARGET_LIB_DIR" fi +mkdir -p "$TARGET_LIB_DIR" -if [ -z "$SKIP_LINUX_CROSS_COMPILE" ] || [ -z "$SKIP_LINUX" ]; then - echo "Copying Linux binaries..." - mkdir -p "$ARTIFACTS_DIR/linux-x64" - cp "$SHARED_LIB_DIR/binaries/linux-x64/spannerlib.so" "$ARTIFACTS_DIR/linux-x64/spannerlib.so" - cp "$SHARED_LIB_DIR/binaries/linux-x64/spannerlib.h" "$ARTIFACTS_DIR/linux-x64/spannerlib.h" -fi +# Copy artifacts +SOURCE_BINARIES="$SHARED_LIB_DIR/binaries" -if [ -z "$SKIP_WINDOWS" ]; then - echo "Copying Windows binaries..." - mkdir -p "$ARTIFACTS_DIR/win-x64" - cp "$SHARED_LIB_DIR/binaries/win-x64/spannerlib.dll" "$ARTIFACTS_DIR/win-x64/spannerlib.dll" - cp "$SHARED_LIB_DIR/binaries/win-x64/spannerlib.h" "$ARTIFACTS_DIR/win-x64/spannerlib.h" +if [ -d "$SOURCE_BINARIES" ] && [ "$(ls -A $SOURCE_BINARIES)" ]; then + log "Copying binaries from $SOURCE_BINARIES to $TARGET_LIB_DIR..." + cp -r "$SOURCE_BINARIES/"* "$TARGET_LIB_DIR/" + log "Artifacts copied successfully." + ls -R "$TARGET_LIB_DIR" +else + log "WARNING: No binaries found in $SOURCE_BINARIES. The build might have skipped everything or failed silently." + # We don't exit with error here because sometimes we might run this in a context where + # we expect no binaries (e.g. linting only), but usually this is an issue. + # However, since set -e is on, build-binaries.sh should have failed if it errored. fi + +log "Build and copy process completed." diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/spannerlib.py b/spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/spannerlib.py index 75f426263..2a99d509e 100644 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/spannerlib.py +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/google/cloud/spannerlib/internal/spannerlib.py @@ -113,23 +113,34 @@ def _load_library(self) -> None: @staticmethod def _get_lib_filename() -> str: """ - Returns the filename of the shared library based on the OS. + Returns the filename of the shared library based on the OS + and architecture. """ - filename: str = "" - system_name = platform.system() + machine_name = platform.machine().lower() if system_name == "Windows": - filename = "spannerlib.dll" + os_part = "win" + ext = "dll" elif system_name == "Darwin": - filename = "spannerlib.dylib" + os_part = "osx" + ext = "dylib" elif system_name == "Linux": - filename = "spannerlib.so" + os_part = "linux" + ext = "so" else: raise SpannerLibError( f"Unsupported operating system: {system_name}" ) - return filename + + if machine_name in ("amd64", "x86_64"): + arch_part = "x64" + elif machine_name in ("arm64", "aarch64"): + arch_part = "arm64" + else: + raise SpannerLibError(f"Unsupported architecture: {machine_name}") + + return f"{os_part}-{arch_part}/spannerlib.{ext}" def _configure_signatures(self) -> None: """ diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/noxfile.py b/spannerlib/wrappers/spannerlib-python/spannerlib-python/noxfile.py index 46d742102..b66b9ed6b 100644 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/noxfile.py +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/noxfile.py @@ -60,7 +60,6 @@ DIST_DIR = "dist" LIB_DIR = "google/cloud/spannerlib/internal/lib" -ARTIFACT_DIR = "spannerlib-artifacts" # Error if a python version is missing nox.options.error_on_missing_interpreters = True @@ -127,6 +126,52 @@ def unit(session): ) +def run_bash_script(session, script_path): + """Runs a bash script, handling Windows specifics.""" + cmd = ["bash", script_path] + + if platform.system() == "Windows": + bash_path = shutil.which("bash") + if not bash_path: + possible_paths = [ + os.path.join( + os.environ.get("ProgramFiles", r"C:\Program Files"), + "Git", + "bin", + "bash.exe", + ), + os.path.join( + os.environ.get( + "ProgramFiles(x86)", r"C:\Program Files (x86)" + ), + "Git", + "bin", + "bash.exe", + ), + ] + for p in possible_paths: + if os.path.exists(p): + bash_path = p + break + + if bash_path: + cmd[0] = bash_path + else: + session.error( + "Bash not found. Please ensure 'bash' is in your PATH " + "or Git Bash is installed." + ) + + session.run(*cmd, external=True) + + +def _build_artifacts(session): + """Helper to build spannerlib artifacts.""" + session.log("Building spannerlib artifacts...") + session.env["RUNNER_OS"] = platform.system() + run_bash_script(session, "./build-shared-lib.sh") + + @nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS) def system(session): """Run system tests.""" @@ -139,7 +184,8 @@ def system(session): "Credentials or emulator host must be set via environment variable" ) - copy_artifacts(session) + # Build/Copy artifacts using the script + _build_artifacts(session) session.install(*STANDARD_DEPENDENCIES, *SYSTEM_TEST_STANDARD_DEPENDENCIES) session.install("-e", ".") @@ -158,68 +204,13 @@ def system(session): ) -def get_spannerlib_artifacts_binary(session): - """ - Returns spannerlib lib and header files. - """ - header = "spannerlib.h" - - lib = None - folder = None - - buildsystem = platform.system() - if buildsystem == "Darwin": - lib, folder = "spannerlib.dylib", "osx-arm64" - elif buildsystem == "Windows": - lib, folder = "spannerlib.dll", "win-x64" - elif buildsystem == "Linux": - lib, folder = "spannerlib.so", "linux-x64" - - if lib is None or folder is None: - session.error(f"Unsupported platform: {buildsystem}") - - return (lib, folder, header) - - @nox.session def build_spannerlib(session): """ Build SpannerLib artifacts. Used only in dev env to build SpannerLib artifacts. """ - if platform.system() == "Windows": - session.skip("Skipping build_spannerlib on Windows") - - session.log("Building spannerlib artifacts...") - - # Run the build script - session.env["RUNNER_OS"] = platform.system() - session.run("bash", "./build-shared-lib.sh", external=True) - - -def copy_artifacts(session): - """ - Copy correct spannerlib artifact to lib folder - """ - session.log("Copy platform specific artifacts to lib dir") - artifact_dir_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), ARTIFACT_DIR - ) - lib_dir_path = os.path.join( - os.path.dirname(os.path.realpath(__file__)), LIB_DIR - ) - if os.path.exists(LIB_DIR): - shutil.rmtree(LIB_DIR) - os.makedirs(LIB_DIR) - lib, folder, header = get_spannerlib_artifacts_binary(session) - shutil.copy( - os.path.join(artifact_dir_path, folder, lib), - os.path.join(lib_dir_path, lib), - ) - shutil.copy( - os.path.join(artifact_dir_path, folder, header), - os.path.join(lib_dir_path, header), - ) + _build_artifacts(session) @nox.session @@ -234,7 +225,7 @@ def build(session): session.install("build", "twine") # Run the preparation step - copy_artifacts(session) + _build_artifacts(session) # Build the wheel session.log("Building...") diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/pyproject.toml b/spannerlib/wrappers/spannerlib-python/spannerlib-python/pyproject.toml index b0fb646cd..6430c7561 100644 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/pyproject.toml +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=68.0"] build-backend = "setuptools.build_meta" [project] -name = "spannerlib" +name = "spannerlib-python" dynamic = ["version"] authors = [ { name="Google LLC", email="googleapis-packages@google.com" }, diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/setup.py b/spannerlib/wrappers/spannerlib-python/spannerlib-python/setup.py index 6d4283230..d31e7ddcc 100644 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/setup.py +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/setup.py @@ -3,6 +3,5 @@ setup( - has_ext_modules=lambda: True, include_package_data=True, ) diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/spannerlib-artifacts/.gitkeep b/spannerlib/wrappers/spannerlib-python/spannerlib-python/spannerlib-artifacts/.gitkeep deleted file mode 100644 index 8b1378917..000000000 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/spannerlib-artifacts/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/spannerlib/wrappers/spannerlib-python/spannerlib-python/tests/unit/internal/test_spannerlib.py b/spannerlib/wrappers/spannerlib-python/spannerlib-python/tests/unit/internal/test_spannerlib.py index d24a0fe36..8f74ceba1 100644 --- a/spannerlib/wrappers/spannerlib-python/spannerlib-python/tests/unit/internal/test_spannerlib.py +++ b/spannerlib/wrappers/spannerlib-python/spannerlib-python/tests/unit/internal/test_spannerlib.py @@ -109,25 +109,54 @@ def test_initialize_lib_not_found(self, mock_lib_path): SpannerLib() def test_get_lib_filename_linux(self): - """Test _get_lib_filename on Linux.""" + """Test _get_lib_filename on Linux AMD64.""" with mock.patch("platform.system", return_value="Linux"): - # pylint: disable=protected-access - filename = SpannerLib._get_lib_filename() - assert filename == "spannerlib.so" + with mock.patch("platform.machine", return_value="x86_64"): + # pylint: disable=protected-access + filename = SpannerLib._get_lib_filename() + assert filename == "linux-x64/spannerlib.so" + + def test_get_lib_filename_linux_arm64(self): + """Test _get_lib_filename on Linux ARM64.""" + with mock.patch("platform.system", return_value="Linux"): + with mock.patch("platform.machine", return_value="aarch64"): + # pylint: disable=protected-access + filename = SpannerLib._get_lib_filename() + assert filename == "linux-arm64/spannerlib.so" def test_get_lib_filename_darwin(self): - """Test _get_lib_filename on Darwin.""" + """Test _get_lib_filename on Darwin ARM64.""" with mock.patch("platform.system", return_value="Darwin"): - # pylint: disable=protected-access - filename = SpannerLib._get_lib_filename() - assert filename == "spannerlib.dylib" + with mock.patch("platform.machine", return_value="arm64"): + # pylint: disable=protected-access + filename = SpannerLib._get_lib_filename() + assert filename == "osx-arm64/spannerlib.dylib" + + def test_get_lib_filename_darwin_amd64(self): + """Test _get_lib_filename on Darwin AMD64.""" + with mock.patch("platform.system", return_value="Darwin"): + with mock.patch("platform.machine", return_value="x86_64"): + # pylint: disable=protected-access + filename = SpannerLib._get_lib_filename() + assert filename == "osx-x64/spannerlib.dylib" def test_get_lib_filename_windows(self): - """Test _get_lib_filename on Windows.""" + """Test _get_lib_filename on Windows AMD64.""" with mock.patch("platform.system", return_value="Windows"): - # pylint: disable=protected-access - filename = SpannerLib._get_lib_filename() - assert filename == "spannerlib.dll" + with mock.patch("platform.machine", return_value="AMD64"): + # pylint: disable=protected-access + filename = SpannerLib._get_lib_filename() + assert filename == "win-x64/spannerlib.dll" + + def test_get_lib_filename_unsupported_arch(self): + """Test _get_lib_filename on an unsupported architecture.""" + with mock.patch("platform.system", return_value="Linux"): + with mock.patch("platform.machine", return_value="riscv64"): + with pytest.raises( + SpannerLibError, match="Unsupported architecture" + ): + # pylint: disable=protected-access + SpannerLib._get_lib_filename() def test_get_lib_filename_unsupported_os(self): """Test _get_lib_filename on an unsupported OS."""