diff --git a/Dockerfile b/Dockerfile index 87155d2f6..1ff333b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,7 @@ COPY ./pybind_interface/ /qsim/lib/ COPY ./qsimcirq_tests/ /qsim/qsimcirq_tests/ COPY ./requirements.txt /qsim/requirements.txt COPY ./pyproject.toml /qsim/pyproject.toml +COPY ./setup.py /qsim/setup.py WORKDIR /qsim/ diff --git a/qsimcirq_tests/setuppy_test.py b/qsimcirq_tests/setuppy_test.py new file mode 100644 index 000000000..3a49e42c3 --- /dev/null +++ b/qsimcirq_tests/setuppy_test.py @@ -0,0 +1,78 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test setup.py code.""" + +import os +import subprocess +import sys + + +def run_setup(cmake_args, root_dir): + env = os.environ.copy() + env["CMAKE_ARGS"] = cmake_args + + try: + # Using sys.executable to run setup.py with the same Python executable. + return subprocess.run( + [sys.executable, "setup.py", "build_ext"], + env=env, + capture_output=True, + text=True, + cwd=str(root_dir), + ) + except FileNotFoundError as e: + # This Dummy class emulates what subprocess.run() returns. + class Dummy: + stderr = f"Command or file not found: {e}" + returncode = 1 + + return Dummy() + except PermissionError as e: + + class Dummy: + stderr = f"Permission denied: {e}" + returncode = 1 + + return Dummy() + except OSError as e: + + class Dummy: + stderr = f"OS error occurred: {e}" + returncode = 1 + + return Dummy() + except Exception as e: + # Fallback for unexpected errors in subprocess.run itself + class Dummy: + stderr = f"Unexpected error: {str(e)}" + returncode = 1 + + return Dummy() + + +def test_valid_cmake_args(pytestconfig): + res = run_setup("-DCMAKE_CXX_STANDARD=17", pytestconfig.rootpath) + # If it fails, it shouldn't be because of our validation. + assert "arguments must begin with a dash" not in res.stderr + + +def test_invalid_cmake_args_no_dash(pytestconfig): + res = run_setup("NOT_A_FLAG", pytestconfig.rootpath) + assert "arguments must begin with a dash" in res.stderr + + +def test_invalid_cmake_args_malicious(pytestconfig): + res = run_setup("-DVAR=VAL ; rm -rf /", pytestconfig.rootpath) + assert "arguments must begin with a dash" in res.stderr diff --git a/setup.py b/setup.py index b9a9b7b36..34e51fc23 100644 --- a/setup.py +++ b/setup.py @@ -85,8 +85,13 @@ def build_extension(self, ext): # Append additional CMake arguments from the environment variable. # This is e.g. used by cibuildwheel to force a certain C++ standard. additional_cmake_args = os.environ.get("CMAKE_ARGS", "") - if additional_cmake_args: - cmake_args += additional_cmake_args.split() + for arg in additional_cmake_args.split(): + if not arg.startswith("-D"): + raise RuntimeError( + f"The value '{arg}' in the environment variable CMAKE_ARGS " + "is invalid; only definition arguments starting with '-D' are allowed." + ) + cmake_args.append(arg) cfg = "Debug" if self.debug else "Release" build_args = ["--config", cfg]