From 03e554d27ae6db03b0e63ac2301c0b59b5a81016 Mon Sep 17 00:00:00 2001 From: Vincenzo Ruotolo Date: Wed, 14 Oct 2026 22:36:22 +0000 Subject: [PATCH 1/4] Add macOS (Apple Silicon / arm64) build, packaging, and CI support Enable building, testing, and packaging TensorFlow Quantum on macOS (Apple Silicon / arm64) with TensorFlow 2.19.1: - configure.sh & third_party/tf/: - Detect `libtensorflow_framework*.dylib` on Darwin and configure `@local_config_tf//:libtensorflow_framework` to output `libtensorflow_framework.dylib` on macOS. - Guard Linux-only `libstdc++` ABI and GCC warning flags (`-Wno-stringop-overflow`, `-Wno-maybe-uninitialized`) on macOS. - Add macOS Bazel flags (`--macos_minimum_os=12.0`, `--host_cxxopt=-std=c++17`, `-DGRPC_BAZEL_BUILD`, `-Wl,-undefined,dynamic_lookup`, and `@loader_path` rpaths to `tensorflow`). - Enforce CPU build (`TF_NEED_CUDA=0`) when configuring on macOS. - Packaging & custom op loading: - Update `release/build_pip_package.sh` to dereference Bazel runfiles symlinks portably on BSD/macOS `cp` (`-LR`), avoid clobbering the macOS `TMPDIR` environment variable, and normalize relative destination paths. - Replace removed-in-3.12 `distutils.sysconfig.get_python_lib` with stdlib `sysconfig.get_path("platlib")` in `tensorflow_quantum/core/ops/load_module.py`. - Add native macOS build and `delocate-wheel` repair paths to `release/build_distribution.sh`, `release/clean_distribution.sh`, and `release/build_release.sh`. - CI & scripts: - Update `scripts/ci_install.sh` to install Bazel 6.5.0 on Darwin (`arm64`/`x86_64`). - Guard x86-only `-msse2`/`-msse3`/`-msse4` flags behind `uname -m == x86_64` in test and benchmark scripts. - Add `macos-14` jobs to `.github/workflows/ci.yaml` and `.github/workflows/ci-nightly-build-test.yaml`. - Docs & metadata: - Add macOS and Linux OS classifiers in `release/setup.py` and update `README.md`, `docs/install.md`, and `release/README.md`. --- .github/workflows/ci-nightly-build-test.yaml | 43 +++++++++ .github/workflows/ci.yaml | 31 ++++++- README.md | 3 +- configure.sh | 52 ++++++++--- docs/install.md | 8 +- release/README.md | 11 +-- release/build_distribution.sh | 93 ++++++++++++++------ release/build_pip_package.sh | 30 ++++--- release/build_release.sh | 17 ++-- release/clean_distribution.sh | 53 +++++++---- release/setup.py | 2 + scripts/benchmark_all.sh | 9 +- scripts/build_pip_package_test.sh | 13 ++- scripts/ci_install.sh | 25 +++++- scripts/run_example.sh | 2 + scripts/test_all.sh | 6 +- scripts/test_benchmarks.sh | 6 +- tensorflow_quantum/core/ops/load_module.py | 4 +- third_party/tf/BUILD.tpl | 2 +- third_party/tf/tf_configure.bzl | 17 +++- 20 files changed, 332 insertions(+), 95 deletions(-) diff --git a/.github/workflows/ci-nightly-build-test.yaml b/.github/workflows/ci-nightly-build-test.yaml index 2dda68e2a..9e727196b 100644 --- a/.github/workflows/ci-nightly-build-test.yaml +++ b/.github/workflows/ci-nightly-build-test.yaml @@ -156,3 +156,46 @@ jobs: !/home/runner/.bazel/execroot/__main__/bazel-out/**/*.o !/home/runner/.bazel/execroot/__main__/bazel-out/**/_objs !/home/runner/.bazel/execroot/__main__/bazel-out/**/_solib_k8 + + Nightly-macOS: + if: needs.Decision.outputs.run == 'true' + name: Build and test (macOS) + needs: Decision + runs-on: macos-14 + steps: + - name: Check out a copy of the TFQ git repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Python ${{inputs.py_version || env.py_version}} + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{inputs.py_version || env.py_version}} + + - name: Set up Bazel + uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 + with: + bazelrc: ${{env.bazelrc_additions}} + + - name: Build wheel + run: | + pip install --upgrade pip setuptools wheel + ./scripts/build_pip_package_test.sh + + - name: Test wheel + run: | + ./scripts/run_example.sh + + - name: Test rest of TFQ + run: | + set -o pipefail + ./scripts/test_all.sh 2>&1 | tee test_all.log + + - if: failure() || inputs.save_artifacts == 'true' + name: Make artifacts downloadable + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: test-artifacts-macos + retention-days: 7 + include-hidden-files: true + path: | + test_all.log diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7c61107f1..c2fc07895 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,8 +66,12 @@ jobs: run: ./scripts/lint_all.sh wheel-build: - name: Wheel test - runs-on: ubuntu-22.04 + name: Wheel test (${{matrix.os}}) + runs-on: ${{matrix.os}} + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-14] timeout-minutes: 60 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -110,7 +114,28 @@ jobs: run: ./scripts/ci_install.sh - name: Configure CI TF - run: echo "Y\n" | ./configure.sh + run: printf "Y\n" | ./configure.sh + + - name: Full Library Test + run: ./scripts/test_all.sh + + bazel-tests-macos: + name: Library tests (macOS) + runs-on: macos-14 + timeout-minutes: 60 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{env.python-version}} + cache: pip + cache-dependency-path: requirements.txt + + - name: Install Bazel on CI + run: ./scripts/ci_install.sh + + - name: Configure CI TF + run: printf "Y\n" | ./configure.sh - name: Full Library Test run: ./scripts/test_all.sh diff --git a/README.md b/README.md index 85c382eec..5b81b2617 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ Please see the [installation instructions](https://www.tensorflow.org/quantum/install) in the documentation. _Compatibility_: At this time, TensorFlow Quantum is built and -tested on Linux with the following systems and software: +tested on Linux (`x86_64`) and macOS (Apple Silicon / `arm64`) with the +following systems and software: * Python 3.10–3.12 * TensorFlow 2.19.1 diff --git a/configure.sh b/configure.sh index f448c6aaf..881167cdd 100755 --- a/configure.sh +++ b/configure.sh @@ -107,7 +107,15 @@ while [[ -z "${TF_NEED_CUDA}" ]]; do read -p "${y_for_cpu}" INPUT || true case "${INPUT:-Y}" in [Yy]* ) echo "CPU build selected."; TF_NEED_CUDA=0;; - [Nn]* ) echo "GPU build selected."; TF_NEED_CUDA=1;; + [Nn]* ) + if is_macos; then + echo "GPU (CUDA) build is not supported on macOS; selecting CPU build." + TF_NEED_CUDA=0 + else + echo "GPU build selected." + TF_NEED_CUDA=1 + fi + ;; * ) echo "Please answer y or n.";; esac done @@ -141,10 +149,17 @@ lib_path = sc.get_lib() lib_dir = lib_path if os.path.isdir(lib_path) else os.path.dirname(lib_path) print(lib_dir) -cands = (glob.glob(os.path.join(lib_dir, 'libtensorflow_framework.so*')) or - glob.glob(os.path.join(lib_dir, 'libtensorflow.so*')) or - glob.glob(os.path.join(lib_dir, '_pywrap_tensorflow_internal.*'))) -print(os.path.basename(cands[0]) if cands else 'libtensorflow_framework.so.2') +if sys.platform == 'darwin': + cands = (glob.glob(os.path.join(lib_dir, 'libtensorflow_framework*.dylib')) or + glob.glob(os.path.join(lib_dir, 'libtensorflow*.dylib')) or + glob.glob(os.path.join(lib_dir, '_pywrap_tensorflow_internal.*'))) + default_lib = 'libtensorflow_framework.2.dylib' +else: + cands = (glob.glob(os.path.join(lib_dir, 'libtensorflow_framework.so*')) or + glob.glob(os.path.join(lib_dir, 'libtensorflow.so*')) or + glob.glob(os.path.join(lib_dir, '_pywrap_tensorflow_internal.*'))) + default_lib = 'libtensorflow_framework.so.2' +print(os.path.basename(sorted(cands)[0]) if cands else default_lib) PY ) @@ -189,11 +204,26 @@ write_bazelrc "common --experimental_repo_remote_exec" write_bazelrc "build --spawn_strategy=standalone" write_bazelrc "build --strategy=Genrule=standalone" write_bazelrc "build -c opt" -write_bazelrc "build --cxxopt=-D_GLIBCXX_USE_CXX11_ABI=1" +if ! is_macos; then + write_bazelrc "build --cxxopt=-D_GLIBCXX_USE_CXX11_ABI=1" +fi write_bazelrc "build --cxxopt=-std=c++17" +write_bazelrc "build --host_cxxopt=-std=c++17" write_bazelrc "build --action_env=TF_USE_LEGACY_KERAS=1" write_bazelrc "build --action_env=PYTHON_BIN_PATH=${PYTHON_BIN_PATH}" +if is_macos; then + write_bazelrc "build --macos_minimum_os=12.0" + write_bazelrc "build --host_macos_minimum_os=12.0" + write_bazelrc "build --copt=-DGRPC_BAZEL_BUILD" + write_bazelrc "build --host_copt=-DGRPC_BAZEL_BUILD" + write_bazelrc "build --features=archive_param_file" + write_bazelrc "build --linkopt=-Wl,-undefined,dynamic_lookup" + write_bazelrc "build --host_linkopt=-Wl,-undefined,dynamic_lookup" + write_bazelrc "build --linkopt=-Wl,-rpath,@loader_path/../../../tensorflow" + write_bazelrc "build --linkopt=-Wl,-rpath,@loader_path/../../../../tensorflow" +fi + # rpath so the dynamic linker finds TF’s shared lib if ! is_windows; then write_bazelrc "build --linkopt=-Wl,-rpath,${LIBDIR}" @@ -210,10 +240,12 @@ write_bazelrc 'build --per_file_copt=external/.*[.]c$@-Wno-deprecated-non-protot write_bazelrc 'build --host_per_file_copt=external/.*[.]c$@-Wno-deprecated-non-prototype' write_bazelrc "build --per_file_copt=external/com_google_protobuf/.*@-Wno-unused-function" write_bazelrc "build --host_per_file_copt=external/com_google_protobuf/.*@-Wno-unused-function" -write_bazelrc "build --per_file_copt=external/com_google_protobuf/.*@-Wno-stringop-overflow" -write_bazelrc "build --host_per_file_copt=external/com_google_protobuf/.*@-Wno-stringop-overflow" -write_bazelrc "build --per_file_copt=external/eigen/.*@-Wno-maybe-uninitialized" -write_bazelrc "build --host_per_file_copt=external/eigen/.*@-Wno-maybe-uninitialized" +if ! is_macos; then + write_bazelrc "build --per_file_copt=external/com_google_protobuf/.*@-Wno-stringop-overflow" + write_bazelrc "build --host_per_file_copt=external/com_google_protobuf/.*@-Wno-stringop-overflow" + write_bazelrc "build --per_file_copt=external/eigen/.*@-Wno-maybe-uninitialized" + write_bazelrc "build --host_per_file_copt=external/eigen/.*@-Wno-maybe-uninitialized" +fi # The following warnings come from qsim. # TODO: fix the code in qsim & update TFQ to use the updated version. diff --git a/docs/install.md b/docs/install.md index 7e90943e0..6402c1154 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,7 +16,7 @@ TensorFlow Quantum is supported on Python version 3.10 through 3.12 and depends ### Requirements -* pip 19.0 or later (requires `manylinux2014` support) +* pip 19.0 or later (requires `manylinux2014` support on Linux or macOS 12.0+ `arm64` on Apple Silicon) * [TensorFlow == 2.19.1](https://www.tensorflow.org/install/pip) See the [TensorFlow install guide](https://www.tensorflow.org/install/pip) to @@ -53,7 +53,7 @@ Success: TensorFlow Quantum is now installed. ## Build from source -The following steps are tested for Ubuntu-like systems. +The following steps are tested for Ubuntu-like Linux systems and macOS (Apple Silicon / `arm64`). ### 1. Set up a Python 3 development environment @@ -105,7 +105,9 @@ Download and install `bazel` version 6.5.0: -To prevent automatic updating of `bazel` to an incompatible version, run the following: +On macOS (Apple Silicon), you can install `bazelisk` via Homebrew (`brew install bazelisk`), which automatically selects Bazel 6.5.0 from `.bazelversion`, or download the `bazel-6.5.0-darwin-arm64` binary directly from the Bazel releases page. + +To prevent automatic updating of `bazel` to an incompatible version on Ubuntu/Debian, run the following:
diff --git a/release/README.md b/release/README.md
index f8cb277fb..8727ec11a 100644
--- a/release/README.md
+++ b/release/README.md
@@ -10,7 +10,7 @@ documents the process used by the TensorFlow Quantum maintainers to build and
 release the packages for public TensorFlow Quantum distributions.
 
 At present, binary distributions in the form of Python wheel files are made
-available only for Linux systems.
+available for Linux (`x86_64`) and macOS (Apple Silicon / `arm64`) systems.
 
 ## Version numbering policy
 
@@ -33,7 +33,8 @@ on, including:
 
 *   The version of Python and the local Python environment.
 *   The version of TensorFlow.
-*   The TensorFlow build container used.
+*   The TensorFlow build container used (on Linux) or Xcode/Clang toolchain
+    used (on macOS).
 *   The Crosstool configuration used.
 *   The dependency requirements implied by Cirq, TF-Keras, NumPy, Protobuf, and
     other Python packages that TFQ and TF depend on.
@@ -61,8 +62,8 @@ into the upstream repository before proceeding further.
 
 ### Preliminary steps
 
-1.  Make sure you have `docker`, `pyenv`, `pip`, and `jq` installed on your
-    system.
+1.  Make sure you have `pyenv`, `pip`, and `jq` installed on your system, plus
+    `docker` on Linux or Xcode Command Line Tools and Bazel 6.5.0 on macOS.
 
 2.  Git clone the TensorFlow Quantum repo to a directory on your computer.
 
@@ -94,7 +95,7 @@ into the upstream repository before proceeding further.
 For each Python version _X.Y.Z_ that will be supported in the TFQ release, do
 the following:
 
-1.  Run `./release/build_release.sh X.Y.Z`.
+1.  Run `./release/build_release.sh -p X.Y.Z`.
 
 2.  If the previous step fails, resolve the error.
 
diff --git a/release/build_distribution.sh b/release/build_distribution.sh
index fab8ddf02..a4859a287 100755
--- a/release/build_distribution.sh
+++ b/release/build_distribution.sh
@@ -71,21 +71,61 @@ while getopts "b:ehp:" opt; do
 done
 shift $((OPTIND -1))
 
-# See https://hub.docker.com/r/tensorflow/build/tags for available containers.
-docker_image="tensorflow/build:2.18-python${py_version}"
-
-# The next values must match what is used by the TF release being targeted. Look
-# in the TF .bazelrc file for the Linux CPU builds, specifically rbe_linux_cpu.
-tf_compiler="/usr/lib/llvm-18/bin/clang"
-tf_sysroot="/dt9"
-tf_crosstool="@local_config_cuda//crosstool:toolchain"
-
-# Create a script to be run by the shell inside the Docker container.
-build_script=$(mktemp /tmp/tfq_build.XXXXXX)
-trap 'rm -f "${build_script}" || true' EXIT
-
-# The printf'ed section dividers are to make it easier to search the output.
-cat < "${build_script}"
+if [[ "$(uname -s)" == "Darwin" ]]; then
+  cd "${repo_dir}"
+
+  if [[ -n "${PYTHON_BIN_PATH:-}" ]]; then
+    py_bin="${PYTHON_BIN_PATH}"
+  elif command -v "python${py_version}" > /dev/null 2>&1; then
+    py_bin="$(command -v "python${py_version}")"
+  else
+    py_bin="$(command -v python3)"
+  fi
+
+  printf ":::::::: Configuring Python environment (macOS) ::::::::\n\n"
+  "${py_bin}" -m pip install --upgrade pip
+  "${py_bin}" -m pip install -r requirements.txt
+
+  printf "\n\n:::::::: Configuring TFQ build (macOS) ::::::::\n\n"
+  printf "Y\n" | PYTHON_BIN_PATH="${py_bin}" ./configure.sh
+
+  printf "\n\n:::::::: Build configuration on macOS ::::::::\n"
+  printf "  Platform:         $(uname -s) ($(uname -m))\n"
+  printf "  Python binary:    ${py_bin}\n"
+  printf "  Python version:   "
+  "${py_bin}" --version | cut -d' ' -f2
+
+  printf "\n:::::::: Starting Bazel build ::::::::\n\n"
+  # shellcheck disable=SC2086
+  bazel build \
+    --cxxopt=-O3 \
+    ${extra_bazel_options} \
+    //release:build_pip_package
+
+  printf "\n:::::::: Creating Python wheel ::::::::\n\n"
+  mkdir -p /tmp/tensorflow_quantum
+  PYTHON_BIN_PATH="${py_bin}" ./bazel-bin/release/build_pip_package /tmp/tensorflow_quantum/
+
+  if [[ "${cleanup}" == "true" ]]; then
+    printf "\n:::::::: Cleaning up ::::::::\n\n"
+    bazel clean --async
+  fi
+else
+  # See https://hub.docker.com/r/tensorflow/build/tags for available containers.
+  docker_image="tensorflow/build:2.18-python${py_version}"
+
+  # The next values must match what is used by the TF release being targeted. Look
+  # in the TF .bazelrc file for the Linux CPU builds, specifically rbe_linux_cpu.
+  tf_compiler="/usr/lib/llvm-18/bin/clang"
+  tf_sysroot="/dt9"
+  tf_crosstool="@local_config_cuda//crosstool:toolchain"
+
+  # Create a script to be run by the shell inside the Docker container.
+  build_script=$(mktemp /tmp/tfq_build.XXXXXX)
+  trap 'rm -f "${build_script}" || true' EXIT
+
+  # The printf'ed section dividers are to make it easier to search the output.
+  cat < "${build_script}"
 #!/bin/bash
 set -o errexit
 cd /tfq
@@ -127,17 +167,18 @@ if [[ "${cleanup}" == "true" ]]; then
 fi
 EOF
 
-chmod +x "${build_script}"
-
-echo "Spinning up a Docker container with ${docker_image} …"
-docker run -it --rm --network host \
-  -w /tfq \
-  -v "${repo_dir}":/tfq \
-  -v /tmp/tensorflow_quantum:/build_output \
-  -v "${build_script}:/tmp/build_script.sh" \
-  -e HOST_PERMS="$(id -u):$(id -g)" \
-  "${docker_image}" \
-  /tmp/build_script.sh
+  chmod +x "${build_script}"
+
+  echo "Spinning up a Docker container with ${docker_image} …"
+  docker run -it --rm --network host \
+    -w /tfq \
+    -v "${repo_dir}":/tfq \
+    -v /tmp/tensorflow_quantum:/build_output \
+    -v "${build_script}:/tmp/build_script.sh" \
+    -e HOST_PERMS="$(id -u):$(id -g)" \
+    "${docker_image}" \
+    /tmp/build_script.sh
+fi
 
 echo "Done. Look for wheel in /tmp/tensorflow_quantum/."
 ls -l /tmp/tensorflow_quantum/
diff --git a/release/build_pip_package.sh b/release/build_pip_package.sh
index d8654e719..be05f1d93 100755
--- a/release/build_pip_package.sh
+++ b/release/build_pip_package.sh
@@ -45,8 +45,8 @@ echo "Using Python: ${PY}"
 EXPORT_DIR="bazel-bin/release/build_pip_package.runfiles/__main__"
 
 main() {
-  DEST="${1}"
-  EXTRA_FLAGS="${2}"
+  DEST="${1:-}"
+  EXTRA_FLAGS="${2:-}"
 
   if [[ -z "${DEST}" ]]; then
     echo "No destination directory provided."
@@ -54,26 +54,32 @@ main() {
   fi
 
   mkdir -p "${DEST}"
+  DEST="$(cd "${DEST}" && pwd)"
   echo "=== Destination directory for wheel file: ${DEST}"
 
   # Build the pip package in a temporary directory.
-  TMPDIR="$(mktemp -d -t tmp.XXXXXXXXXX)"
-  echo "$(date) : === Using tmpdir: ${TMPDIR}"
+  # Avoid naming this variable TMPDIR because macOS exports TMPDIR in the
+  # environment, and overwriting it would cause `python -m build` to create its
+  # temporary isolated environment inside the package staging directory.
+  BUILD_TMPDIR="$(mktemp -d -t tmp.XXXXXXXXXX)"
+  echo "$(date) : === Using tmpdir: ${BUILD_TMPDIR}"
   echo "=== Copying TFQ files"
 
-  # Copy over files necessary to run setup.py
-  cp "${EXPORT_DIR}/README.md" "${TMPDIR}"
-  cp "${EXPORT_DIR}/release/setup.py" "${TMPDIR}"
-  cp "${EXPORT_DIR}/release/MANIFEST.in" "${TMPDIR}"
-  mkdir "${TMPDIR}/tensorflow_quantum"
-  cp -r -v "${EXPORT_DIR}/tensorflow_quantum/"* "${TMPDIR}/tensorflow_quantum/"
+  # Copy over files necessary to run setup.py. Use -L so symlinks in Bazel's
+  # runfiles tree are dereferenced on both Linux (GNU cp) and macOS (BSD cp).
+  cp -L "${EXPORT_DIR}/README.md" "${BUILD_TMPDIR}"
+  cp -L "${EXPORT_DIR}/release/setup.py" "${BUILD_TMPDIR}"
+  cp -L "${EXPORT_DIR}/release/MANIFEST.in" "${BUILD_TMPDIR}"
+  mkdir "${BUILD_TMPDIR}/tensorflow_quantum"
+  cp -LR -v "${EXPORT_DIR}/tensorflow_quantum/"* "${BUILD_TMPDIR}/tensorflow_quantum/"
+  chmod -R u+w "${BUILD_TMPDIR}"
 
-  pushd "${TMPDIR}"
+  pushd "${BUILD_TMPDIR}"
   echo "$(date) : === Building wheel"
   "${PY}" -m build -v --wheel ${EXTRA_FLAGS} > /dev/null
   cp dist/*.whl "${DEST}"
   popd
-  rm -rf "${TMPDIR}"
+  rm -rf "${BUILD_TMPDIR}"
   echo $(date) : "=== Output wheel file is in: ${DEST}"
 }
 
diff --git a/release/build_release.sh b/release/build_release.sh
index 3d65ec25e..38cc4974c 100755
--- a/release/build_release.sh
+++ b/release/build_release.sh
@@ -71,15 +71,19 @@ if ! [[ "${py_version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
   quit "The Python version must be a full version number in the form x.y.z."
 fi
 
-setup_file="${repo_dir}/release/setup.py"
-if ! [[ -r "${setup_file}" ]]; then
-  quit "Cannot read ${setup_file}"
+init_file="${repo_dir}/tensorflow_quantum/__init__.py"
+if ! [[ -r "${init_file}" ]]; then
+  quit "Cannot read ${init_file}"
 fi
-tfq_cur_version=$(grep -m 1 CUR_VERSION "${setup_file}" | cut -f2 -d'"')
+tfq_cur_version=$(grep -E '^__version__' "${init_file}" | sed -E "s/.*['\"]([^'\"]+)['\"].*/\1/")
 tfq_version=${tfq_cur_version}${build_number}
 
 # Test these upfront to avoid failing mid-way through a long process.
-for program in docker pyenv jq; do
+required_programs=(pyenv jq)
+if [[ "$(uname -s)" != "Darwin" ]]; then
+  required_programs+=(docker)
+fi
+for program in "${required_programs[@]}"; do
   if ! command -v "${program}" > /dev/null 2>&1; then
     quit "Cannot run ${program} -- maybe it is not installed?"
   fi
@@ -112,6 +116,9 @@ pyenv activate tfq-build-venv
 
 pip install --upgrade pip
 pip install wheel-inspect check-wheel-contents
+if [[ "$(uname -s)" == "Darwin" ]]; then
+  pip install delocate
+fi
 
 # ~~~~~~~~ Build & clean the wheel ~~~~~~~~
 
diff --git a/release/clean_distribution.sh b/release/clean_distribution.sh
index 333aac357..eae9b258c 100755
--- a/release/clean_distribution.sh
+++ b/release/clean_distribution.sh
@@ -70,21 +70,42 @@ fi
 wheel_path="$(cd "$(dirname "${1}")" && pwd)/$(basename "${1}")"
 wheel_name="$(basename "${1}")"
 
-auditwheel_args=()
-if [[ "${action}" == "repair" ]]; then
-    auditwheel_args+=(
-        "--exclude" "libtensorflow_framework.so.2"
-        "--plat" "${platform}"
-        "-w" "/tfq/wheelhouse"
-    )
-fi
+if [[ "$(uname -s)" == "Darwin" ]]; then
+  if ! command -v delocate-wheel > /dev/null 2>&1; then
+    echo "Installing delocate..."
+    python3 -m pip install delocate
+  fi
+
+  if [[ "${action}" == "show" ]]; then
+    echo "Running 'delocate-listdeps' on ${wheel_path}"
+    delocate-listdeps --all --depending "${wheel_path}"
+  else
+    mkdir -p "${repo_dir}/wheelhouse"
+    echo "Running 'delocate-wheel' on ${wheel_path}"
+    delocate-wheel \
+      -w "${repo_dir}/wheelhouse" \
+      -e "libtensorflow_framework" \
+      -v \
+      "${wheel_path}"
+    echo "Done. New wheel file written to ${repo_dir}/wheelhouse"
+  fi
+else
+  auditwheel_args=()
+  if [[ "${action}" == "repair" ]]; then
+      auditwheel_args+=(
+          "--exclude" "libtensorflow_framework.so.2"
+          "--plat" "${platform}"
+          "-w" "/tfq/wheelhouse"
+      )
+  fi
 
-echo "Running 'auditwheel ${action}' in Docker with image ${docker_image}"
-docker run -it --rm --network host \
-  -w /tfq \
-  -v "${repo_dir}":/tfq \
-  -v "${wheel_path}":"/tmp/${wheel_name}" \
-  "${docker_image}" \
-  auditwheel "${action}" "${auditwheel_args[@]}" "/tmp/${wheel_name}"
+  echo "Running 'auditwheel ${action}' in Docker with image ${docker_image}"
+  docker run -it --rm --network host \
+    -w /tfq \
+    -v "${repo_dir}":/tfq \
+    -v "${wheel_path}":"/tmp/${wheel_name}" \
+    "${docker_image}" \
+    auditwheel "${action}" "${auditwheel_args[@]}" "/tmp/${wheel_name}"
 
-echo "Done. New wheel file written to ${repo_dir}/wheelhouse"
+  echo "Done. New wheel file written to ${repo_dir}/wheelhouse"
+fi
diff --git a/release/setup.py b/release/setup.py
index 83b94358e..78f60df6f 100644
--- a/release/setup.py
+++ b/release/setup.py
@@ -153,6 +153,8 @@ def has_ext_modules(self):
         "Intended Audience :: Developers",
         "Intended Audience :: Education",
         "Intended Audience :: Science/Research",
+        "Operating System :: MacOS :: MacOS X",
+        "Operating System :: POSIX :: Linux",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
diff --git a/scripts/benchmark_all.sh b/scripts/benchmark_all.sh
index 642496aef..49ee3a8ec 100755
--- a/scripts/benchmark_all.sh
+++ b/scripts/benchmark_all.sh
@@ -14,7 +14,11 @@
 # limitations under the License.
 # ==============================================================================
 echo "Testing benchmarks.";
-test_outputs=$(bazel test -c opt --cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4" --test_output=errors $(bazel query //benchmarks/...))
+arch_cxxopts=()
+if [[ "$(uname -m)" == "x86_64" ]]; then
+	arch_cxxopts+=(--cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4")
+fi
+test_outputs=$(bazel test -c opt "${arch_cxxopts[@]}" --test_output=errors $(bazel query //benchmarks/...))
 exit_code=$?
 
 if [ "$exit_code" == "0" ]; then
@@ -26,5 +30,4 @@ else
 fi
 
 echo "Running preconfigured benchmarks.";
-bazel_run=${bazel run -c opt --cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4"}
-bazel_run benchmarks/scripts:benchmark_clifford_circuit -- --op_density 1 --n_moments 10 --n_qubits 4
+bazel run -c opt "${arch_cxxopts[@]}" benchmarks/scripts:benchmark_clifford_circuit -- --op_density 1 --n_moments 10 --n_qubits 4
diff --git a/scripts/build_pip_package_test.sh b/scripts/build_pip_package_test.sh
index c77091945..a25100ee1 100755
--- a/scripts/build_pip_package_test.sh
+++ b/scripts/build_pip_package_test.sh
@@ -14,12 +14,19 @@
 # limitations under the License.
 # ==============================================================================
 
+set -e
+
 pip install -r requirements.txt
 
 # cd tensorflow_quantum
-echo "Y\n" | ./configure.sh
+printf "Y\n" | ./configure.sh
+
+arch_cxxopts=()
+if [[ "$(uname -m)" == "x86_64" ]]; then
+  arch_cxxopts+=(--cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4")
+fi
 
-bazel build -c opt --cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4" release:build_pip_package
-rm /tmp/tensorflow_quantum/* || echo ok
+bazel build -c opt "${arch_cxxopts[@]}" release:build_pip_package
+rm -f /tmp/tensorflow_quantum/* || echo ok
 bazel-bin/release/build_pip_package /tmp/tensorflow_quantum/
 pip install -U /tmp/tensorflow_quantum/*.whl
diff --git a/scripts/ci_install.sh b/scripts/ci_install.sh
index 28c58c2d7..2c2637bc6 100755
--- a/scripts/ci_install.sh
+++ b/scripts/ci_install.sh
@@ -13,7 +13,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ==============================================================================
-wget https://github.com/bazelbuild/bazel/releases/download/6.5.0/bazel_6.5.0-linux-x86_64.deb
-sudo dpkg -i bazel_6.5.0-linux-x86_64.deb
+set -e
+
+BAZEL_VERSION="6.5.0"
+
+if [[ "$(uname -s)" == "Darwin" ]]; then
+  if ! command -v bazel >/dev/null 2>&1 || [[ "$(bazel --version 2>/dev/null)" != *"${BAZEL_VERSION}"* ]]; then
+    ARCH="$(uname -m)"
+    if [[ "${ARCH}" == "arm64" ]]; then
+      BAZEL_ARCH="arm64"
+    else
+      BAZEL_ARCH="x86_64"
+    fi
+    curl -fsSL -o /tmp/bazel \
+      "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel-${BAZEL_VERSION}-darwin-${BAZEL_ARCH}"
+    chmod +x /tmp/bazel
+    sudo mkdir -p /usr/local/bin
+    sudo mv /tmp/bazel /usr/local/bin/bazel
+  fi
+else
+  wget "https://github.com/bazelbuild/bazel/releases/download/${BAZEL_VERSION}/bazel_${BAZEL_VERSION}-linux-x86_64.deb"
+  sudo dpkg -i "bazel_${BAZEL_VERSION}-linux-x86_64.deb"
+fi
+
 pip install --upgrade pip setuptools wheel
 pip install -r requirements.txt
\ No newline at end of file
diff --git a/scripts/run_example.sh b/scripts/run_example.sh
index bb86edc22..be6953112 100755
--- a/scripts/run_example.sh
+++ b/scripts/run_example.sh
@@ -13,6 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # ==============================================================================
+set -e
+export TF_USE_LEGACY_KERAS=1
 cd ..
 cp quantum/scripts/import_test.py import_test.py
 python import_test.py
\ No newline at end of file
diff --git a/scripts/test_all.sh b/scripts/test_all.sh
index 1601a1607..7d7066a5e 100755
--- a/scripts/test_all.sh
+++ b/scripts/test_all.sh
@@ -14,7 +14,11 @@
 # limitations under the License.
 # ==============================================================================
 echo "Testing All Bazel py_test and cc_tests.";
-test_outputs=$(bazel test -c opt --experimental_repo_remote_exec --test_output=errors --cxxopt="-std=c++17" --cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4" //tensorflow_quantum/...)
+arch_cxxopts=()
+if [[ "$(uname -m)" == "x86_64" ]]; then
+	arch_cxxopts+=(--cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4")
+fi
+test_outputs=$(bazel test -c opt --experimental_repo_remote_exec --test_output=errors --cxxopt="-std=c++17" "${arch_cxxopts[@]}" //tensorflow_quantum/...)
 exit_code=$?
 if [ "$exit_code" == "0" ]; then
 	echo "Testing Complete!";
diff --git a/scripts/test_benchmarks.sh b/scripts/test_benchmarks.sh
index 620676c4e..7ac92411a 100755
--- a/scripts/test_benchmarks.sh
+++ b/scripts/test_benchmarks.sh
@@ -14,7 +14,11 @@
 # limitations under the License.
 # ==============================================================================
 echo "Testing all Benchmarks.";
-bazel test -c opt --cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4" --test_output=errors $(bazel query //benchmarks/scripts:all)
+arch_cxxopts=()
+if [[ "$(uname -m)" == "x86_64" ]]; then
+	arch_cxxopts+=(--cxxopt="-msse2" --cxxopt="-msse3" --cxxopt="-msse4")
+fi
+bazel test -c opt "${arch_cxxopts[@]}" --test_output=errors $(bazel query //benchmarks/scripts:all)
 exit_code=$?
 if [ "$exit_code" == "0" ]; then
 	echo "Testing Complete!";
diff --git a/tensorflow_quantum/core/ops/load_module.py b/tensorflow_quantum/core/ops/load_module.py
index 6fa350523..47b629e4c 100644
--- a/tensorflow_quantum/core/ops/load_module.py
+++ b/tensorflow_quantum/core/ops/load_module.py
@@ -15,8 +15,8 @@
 """Module to load python op libraries."""
 
 import os
+import sysconfig
 import types
-from distutils.sysconfig import get_python_lib
 
 from tensorflow.python.framework import errors
 from tensorflow.python.framework import load_library
@@ -49,7 +49,7 @@ def _load(self):
                 path = resource_loader.get_path_to_datafile(self._name)
                 self._module = load_library.load_op_library(path)
             except (IOError, errors.NotFoundError):
-                path = os.path.join(get_python_lib(),
+                path = os.path.join(sysconfig.get_path("platlib"),
                                     "tensorflow_quantum/core/ops", self._name)
                 self._module = load_library.load_op_library(path)
         return self._module
diff --git a/third_party/tf/BUILD.tpl b/third_party/tf/BUILD.tpl
index f84d92959..0324951e2 100644
--- a/third_party/tf/BUILD.tpl
+++ b/third_party/tf/BUILD.tpl
@@ -9,7 +9,7 @@ cc_library(
 
 cc_library(
     name = "libtensorflow_framework",
-    srcs = [":libtensorflow_framework.so"],
+    srcs = [":%{TF_SHARED_LIBRARY_OUTPUT}"],
     #data = ["lib/libtensorflow_framework.so"],
     visibility = ["//visibility:public"],
 )
diff --git a/third_party/tf/tf_configure.bzl b/third_party/tf/tf_configure.bzl
index fedca1e38..ceb843126 100644
--- a/third_party/tf/tf_configure.bzl
+++ b/third_party/tf/tf_configure.bzl
@@ -41,6 +41,13 @@ def _is_windows(repository_ctx):
         return True
     return False
 
+def _is_macos(repository_ctx):
+    """Returns true if the host operating system is macOS."""
+    os_name = repository_ctx.os.name.lower()
+    if os_name.find("mac os") != -1 or os_name.find("darwin") != -1:
+        return True
+    return False
+
 def _execute(
         repository_ctx,
         cmdline,
@@ -205,13 +212,20 @@ def _tf_pip_impl(repository_ctx):
     tf_shared_library_name = repository_ctx.os.environ[_TF_SHARED_LIBRARY_NAME]
     tf_shared_library_path = "%s/%s" % (tf_shared_library_dir, tf_shared_library_name)
 
+    if _is_windows(repository_ctx):
+        tf_shared_library_output = "_pywrap_tensorflow_internal.lib"
+    elif _is_macos(repository_ctx):
+        tf_shared_library_output = "libtensorflow_framework.dylib"
+    else:
+        tf_shared_library_output = "libtensorflow_framework.so"
+
     tf_shared_library_rule = _symlink_genrule_for_dir(
         repository_ctx,
         None,
         "",
         "libtensorflow_framework_so",
         [tf_shared_library_path],
-        ["_pywrap_tensorflow_internal.lib"  if _is_windows(repository_ctx) else "libtensorflow_framework.so"],
+        [tf_shared_library_output],
     )
 
     tf_test_log_proto_path = "%s/core/util/test_log_pb2.py" % tf_shared_library_dir
@@ -228,6 +242,7 @@ def _tf_pip_impl(repository_ctx):
     _tpl(repository_ctx, "BUILD", {
         "%{TF_HEADER_GENRULE}": tf_header_rule,
         "%{TF_SHARED_LIBRARY_GENRULE}": tf_shared_library_rule,
+        "%{TF_SHARED_LIBRARY_OUTPUT}": tf_shared_library_output,
         "%{TF_PROTO_GENRULE}": tf_proto_rule,
     })
 

From 687836c53eb78f6a7fa7d6303851577d03095af1 Mon Sep 17 00:00:00 2001
From: Vincenzo Ruotolo 
Date: Thu, 15 Oct 2026 18:36:57 +0000
Subject: [PATCH 2/4] Fix workflow permissions and single-precision norm
 tolerance on arm64

---
 .github/workflows/ci-nightly-build-test.yaml    | 3 ++-
 .github/workflows/ci.yaml                       | 3 ++-
 tensorflow_quantum/datasets/spin_system_test.py | 9 ++++++---
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/ci-nightly-build-test.yaml b/.github/workflows/ci-nightly-build-test.yaml
index 9e727196b..9a748d476 100644
--- a/.github/workflows/ci-nightly-build-test.yaml
+++ b/.github/workflows/ci-nightly-build-test.yaml
@@ -69,7 +69,8 @@ concurrency:
   cancel-in-progress: true
   group: ${{github.workflow}}-${{github.event.pull_request.number||github.ref}}
 
-permissions: read-all
+permissions:
+  contents: read
 
 defaults:
   run:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index c2fc07895..e2c6283e7 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -34,7 +34,8 @@ concurrency:
   cancel-in-progress: true
   group: ${{github.workflow}}-${{github.event.pull_request.number||github.ref}}
 
-permissions: read-all
+permissions:
+  contents: read
 
 jobs:
   format-and-lint-checks:
diff --git a/tensorflow_quantum/datasets/spin_system_test.py b/tensorflow_quantum/datasets/spin_system_test.py
index 682870c25..5ae3c3553 100644
--- a/tensorflow_quantum/datasets/spin_system_test.py
+++ b/tensorflow_quantum/datasets/spin_system_test.py
@@ -91,7 +91,8 @@ def test_paulisum(self):
             }
             for n in self.random_subset_tfi_chain:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(phi, qubit_map)
+                e = pauli_sums[n].expectation_from_state_vector(
+                    phi, qubit_map, atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=1e-4)
 
     def test_returned_objects(self):
@@ -190,7 +191,8 @@ def test_paulisum(self):
             }
             for n in self.random_subset_xxz_chain:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(phi, qubit_map)
+                e = pauli_sums[n].expectation_from_state_vector(
+                    phi, qubit_map, atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=5e-3)
 
     def test_returned_objects(self):
@@ -293,7 +295,8 @@ def test_paulisum(self):
             }
             for n in self.random_subset_tfi_rectangular:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(phi, qubit_map)
+                e = pauli_sums[n].expectation_from_state_vector(
+                    phi, qubit_map, atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=5e-3)
 
     def test_returned_objects(self):

From 918d77feafae18fc92260fa34f42b0a8de918d91 Mon Sep 17 00:00:00 2001
From: Vincenzo Ruotolo 
Date: Thu, 15 Oct 2026 18:41:52 +0000
Subject: [PATCH 3/4] Format spin_system_test.py with yapf

---
 tensorflow_quantum/datasets/spin_system_test.py | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/tensorflow_quantum/datasets/spin_system_test.py b/tensorflow_quantum/datasets/spin_system_test.py
index 5ae3c3553..4b25379c5 100644
--- a/tensorflow_quantum/datasets/spin_system_test.py
+++ b/tensorflow_quantum/datasets/spin_system_test.py
@@ -91,8 +91,9 @@ def test_paulisum(self):
             }
             for n in self.random_subset_tfi_chain:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(
-                    phi, qubit_map, atol=1e-4)
+                e = pauli_sums[n].expectation_from_state_vector(phi,
+                                                                qubit_map,
+                                                                atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=1e-4)
 
     def test_returned_objects(self):
@@ -191,8 +192,9 @@ def test_paulisum(self):
             }
             for n in self.random_subset_xxz_chain:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(
-                    phi, qubit_map, atol=1e-4)
+                e = pauli_sums[n].expectation_from_state_vector(phi,
+                                                                qubit_map,
+                                                                atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=5e-3)
 
     def test_returned_objects(self):
@@ -295,8 +297,9 @@ def test_paulisum(self):
             }
             for n in self.random_subset_tfi_rectangular:
                 phi = cirq.Simulator().simulate(circuits[n]).final_state_vector
-                e = pauli_sums[n].expectation_from_state_vector(
-                    phi, qubit_map, atol=1e-4)
+                e = pauli_sums[n].expectation_from_state_vector(phi,
+                                                                qubit_map,
+                                                                atol=1e-4)
                 self.assertAllClose(e, addinfo[n].gs_energy, rtol=5e-3)
 
     def test_returned_objects(self):

From a688885f3d9018bc2d64686426eb8255ea8483d0 Mon Sep 17 00:00:00 2001
From: Vincenzo Ruotolo 
Date: Thu, 15 Oct 2026 21:50:07 +0000
Subject: [PATCH 4/4] Clean up BUILD_TMPDIR on exit in build_pip_package.sh

---
 release/build_pip_package.sh | 1 +
 1 file changed, 1 insertion(+)

diff --git a/release/build_pip_package.sh b/release/build_pip_package.sh
index be05f1d93..9e175d723 100755
--- a/release/build_pip_package.sh
+++ b/release/build_pip_package.sh
@@ -62,6 +62,7 @@ main() {
   # environment, and overwriting it would cause `python -m build` to create its
   # temporary isolated environment inside the package staging directory.
   BUILD_TMPDIR="$(mktemp -d -t tmp.XXXXXXXXXX)"
+  trap 'rm -rf "${BUILD_TMPDIR}"' EXIT
   echo "$(date) : === Using tmpdir: ${BUILD_TMPDIR}"
   echo "=== Copying TFQ files"