diff --git a/.gitignore b/.gitignore index dabdbdf..0606e17 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ target/ *.so *.dll *.dll.lib +*.dll.a venv/ .venv/ diff --git a/hatch_build.py b/hatch_build.py index 6dc2343..b30d94e 100644 --- a/hatch_build.py +++ b/hatch_build.py @@ -1,6 +1,7 @@ import subprocess import shutil import sys +import os from packaging.tags import sys_tags from hatchling.builders.hooks.plugin.interface import BuildHookInterface from pathlib import Path @@ -60,7 +61,7 @@ def build_all(self): self.hook.app.display_success("Cargo build completed successfully") def extract_libs(self): - release_dir = Path(self.hook.root) / "target/release" + release_dir = self.hook.get_cargo_release_dir() assert release_dir.exists() for package in self.metadata["packages"]: for target in package["targets"]: @@ -71,7 +72,7 @@ def extract_libs(self): lib_filenames = { "darwin": [f"lib{lib_name}.dylib"], "linux": [f"lib{lib_name}.so"], - "win32": [f"{lib_name}.dll", f"{lib_name}.dll.lib"], + "win32": [f"{lib_name}.dll", f"lib{lib_name}.dll.a"], }[sys.platform] assert all((release_dir / file).exists() for file in lib_filenames), ( f"Compiled library for {lib_name} not found in {release_dir}. " @@ -103,9 +104,15 @@ def extract_libs(self): f"Copying {lib_path} to {destination}" ) shutil.copy(lib_path, destination) - - class BundleBuildHook(BuildHookInterface): + def get_cargo_release_dir(self) -> Path: + target = os.environ.get("CARGO_BUILD_TARGET") + if target: + candidate = Path(self.root) / "target" / target / "release" + if candidate.exists(): + return candidate + return Path(self.root) / "target/release" + def initialize(self, version: str, build_data: dict) -> None: cargo_runner = CargoWorkspaceBuild(self) cargo_runner.run() @@ -188,7 +195,7 @@ def initialize(self, version: str, build_data: dict) -> None: build_data["tag"] = f"py3-none-{target_platform}" def find_release_files(self, cdylib_name): - release_dir = Path(self.root) / "target/release" + release_dir = self.get_cargo_release_dir() if sys.platform == "darwin": return [release_dir / f"lib{cdylib_name}.dylib"] elif sys.platform == "linux": @@ -196,7 +203,7 @@ def find_release_files(self, cdylib_name): elif sys.platform == "win32": return [ release_dir / f"{cdylib_name}.dll", - release_dir / f"{cdylib_name}.dll.lib", + release_dir / f"lib{cdylib_name}.dll.a", ] def build_cargo_workspace(self): @@ -299,16 +306,24 @@ def build_helios_qis(self): dist_dir = helios_qis_dir / "python/selene_helios_qis_plugin/_dist" dist_dir.mkdir(parents=True, exist_ok=True) selene_sim_dist_dir = Path(self.root) / "selene-sim/python/selene_sim/_dist" + cmake_configure_cmd = [ + "cmake", + f"-DCMAKE_INSTALL_PREFIX={dist_dir}", + "-DCMAKE_BUILD_TYPE=Release", + f"-DCMAKE_PREFIX_PATH={selene_sim_dist_dir}", + "..", + ] + if os.environ.get("CARGO_BUILD_TARGET", "").endswith("windows-gnu"): + cmake_configure_cmd = [ + cmake_configure_cmd[0], + "-G", + "MinGW Makefiles", + *cmake_configure_cmd[1:], + ] try: subprocess.run( - [ - "cmake", - f"-DCMAKE_INSTALL_PREFIX={dist_dir}", - "-DCMAKE_BUILD_TYPE=Release", - f"-DCMAKE_PREFIX_PATH={selene_sim_dist_dir}", - "..", - ], + cmake_configure_cmd, cwd=cmake_build_dir, check=True, capture_output=True, @@ -322,12 +337,7 @@ def build_helios_qis(self): shutil.rmtree(cmake_build_dir) cmake_build_dir.mkdir() subprocess.run( - [ - "cmake", - f"-DCMAKE_INSTALL_PREFIX={dist_dir}", - f"-DCMAKE_PREFIX_PATH={selene_sim_dist_dir}", - "..", - ], + cmake_configure_cmd, cwd=cmake_build_dir, check=True, capture_output=True, @@ -344,6 +354,8 @@ def build_helios_qis(self): ".", "--target", "install", + "--config", + "Release", ], check=True, cwd=cmake_build_dir, diff --git a/pyproject.toml b/pyproject.toml index 6677bd4..443e95e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -222,5 +222,6 @@ environment = { PATH = "$PATH:$HOME/.cargo/bin", MACOSX_DEPLOYMENT_TARGET = "11. before-all = ['curl -sSf https://sh.rustup.rs | sh -s -- -y'] before-test = ['uv pip install ./selene-core'] [tool.cibuildwheel.windows] -before-all = ['curl -sSf https://sh.rustup.rs | sh -s -- -y'] +environment = { PATH = "$PATH:$HOME/.cargo/bin", CARGO_BUILD_TARGET = "x86_64-pc-windows-gnu" } +before-all = ['curl -sSf https://sh.rustup.rs | sh -s -- -y', 'rustup target add x86_64-pc-windows-gnu'] before-test = ['uv pip install ./selene-core'] diff --git a/selene-core/pyproject.toml b/selene-core/pyproject.toml index 1142763..11e77bc 100644 --- a/selene-core/pyproject.toml +++ b/selene-core/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "networkx>=2.6,<4", "pydot>=4.0.0", "pyyaml~=6.0", - "qir-qis>=0.1.1", + "qir-qis>=0.1.2,<0.1.4", "typing_extensions>=4", "ziglang~=0.13", ] diff --git a/selene-core/python/selene_core/build_utils/builtins/helios.py b/selene-core/python/selene_core/build_utils/builtins/helios.py index 61af5e1..8ea59c5 100644 --- a/selene-core/python/selene_core/build_utils/builtins/helios.py +++ b/selene-core/python/selene_core/build_utils/builtins/helios.py @@ -315,7 +315,7 @@ def apply(cls, build_ctx: BuildCtx, input_artifact: Artifact) -> Artifact: ) selene_lib_dir = selene_dist / "lib" - selene_lib = selene_lib_dir / "selene.dll.lib" + selene_lib = selene_lib_dir / "libselene.dll.a" link_flags = ["-lc"] libraries = [selene_lib] library_search_dirs = [selene_lib_dir] diff --git a/selene-core/python/selene_core/build_utils/utils.py b/selene-core/python/selene_core/build_utils/utils.py index 3e0d4e0..b645f45 100644 --- a/selene-core/python/selene_core/build_utils/utils.py +++ b/selene-core/python/selene_core/build_utils/utils.py @@ -11,8 +11,7 @@ def get_target_triple(arch: str | None = None, system: str | None = None) -> str as the default is the current platform which selene components might be incompatible with. - Windows needs pointing to MSVC, as the default is MinGW, - and selene components are shipped with MSVC bindings. + Windows uses MinGW to align with selene wheel artifacts. Linux doesn't need further specification, as the default behaviour is correct. Using e.g. "x86_64-linux-gnu" would fail on nixos, for @@ -33,7 +32,7 @@ def get_target_triple(arch: str | None = None, system: str | None = None) -> str case "darwin" | "macos": target_system = "macos.11.0-none" case "windows": - target_system = "windows-msvc" + target_system = "windows-gnu" case _: raise RuntimeError(f"Unsupported OS: {system}") diff --git a/selene-ext/interfaces/helios_qis/python/selene_helios_qis_plugin/plugin.py b/selene-ext/interfaces/helios_qis/python/selene_helios_qis_plugin/plugin.py index 1be46b5..5b81a1e 100644 --- a/selene-ext/interfaces/helios_qis/python/selene_helios_qis_plugin/plugin.py +++ b/selene-ext/interfaces/helios_qis/python/selene_helios_qis_plugin/plugin.py @@ -43,6 +43,9 @@ def library_file(self): case "Darwin": return lib_dir / f"lib{lib_name}.a" case "Windows": + mingw_static = lib_dir / f"lib{lib_name}.a" + if mingw_static.exists(): + return mingw_static return lib_dir / f"{lib_name}.lib" case _: raise RuntimeError(f"Unsupported platform: {platform.system()}") diff --git a/selene-ext/simulators/quest/rust/lib.rs b/selene-ext/simulators/quest/rust/lib.rs index e8c1b79..ff963a7 100644 --- a/selene-ext/simulators/quest/rust/lib.rs +++ b/selene-ext/simulators/quest/rust/lib.rs @@ -17,12 +17,40 @@ use selene_core::utils::MetricValue; use std::io::Write; use quest_sys::Qureg; +#[cfg(all(target_os = "windows", target_env = "gnu"))] +use std::ffi::{CStr, c_char}; use std::mem::size_of; use std::os::raw::{c_int, c_ulong}; #[cfg(test)] mod tests; +#[cfg(all(target_os = "windows", target_env = "gnu"))] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn invalidQuESTInputError(err_msg: *const c_char, err_func: *const c_char) { + let err_msg = if err_msg.is_null() { + "Unknown QuEST error" + } else { + // SAFETY: `err_msg` is expected to be a valid null-terminated C string from QuEST. + CStr::from_ptr(err_msg) + .to_str() + .unwrap_or("Invalid UTF-8 in QuEST error message") + }; + let err_func = if err_func.is_null() { + "unknown" + } else { + // SAFETY: `err_func` is expected to be a valid null-terminated C string from QuEST. + CStr::from_ptr(err_func) + .to_str() + .unwrap_or("Invalid UTF-8 in QuEST function name") + }; + eprintln!("!!!"); + eprintln!("QuEST Error in function {err_func}: {err_msg}"); + eprintln!("!!!"); + eprintln!("Exiting..."); + std::process::exit(1); +} + pub struct QuestSimulator { environment: quest_sys::QuESTEnv, qureg: Qureg, diff --git a/selene-ext/simulators/stim/build.rs b/selene-ext/simulators/stim/build.rs index 28d7824..48949a3 100644 --- a/selene-ext/simulators/stim/build.rs +++ b/selene-ext/simulators/stim/build.rs @@ -16,6 +16,11 @@ fn main() { let target_triple = std::env::var("TARGET").unwrap(); if target_triple.contains("linux-gnu") { println!("cargo:rustc-link-lib=stdc++"); + } else if target_triple.contains("windows-gnu") { + println!("cargo:rustc-link-lib=static=stdc++"); + println!("cargo:rustc-link-lib=static=winpthread"); + println!("cargo:rustc-link-arg=-static-libstdc++"); + println!("cargo:rustc-link-arg=-static-libgcc"); } else if target_triple.contains("apple-darwin") { println!("cargo:rustc-link-lib=c++"); } diff --git a/selene-ext/simulators/stim/python/selene_stim_plugin/plugin.py b/selene-ext/simulators/stim/python/selene_stim_plugin/plugin.py index becdf4f..eb13d51 100644 --- a/selene-ext/simulators/stim/python/selene_stim_plugin/plugin.py +++ b/selene-ext/simulators/stim/python/selene_stim_plugin/plugin.py @@ -49,6 +49,12 @@ def library_file(self): case _: raise RuntimeError(f"Unsupported platform: {platform.system()}") + @property + def library_search_dirs(self) -> list[Path]: + if platform.system() == "Windows": + return [Path(__file__).parent / "_dist/lib/"] + return [] + @staticmethod def extract_states_dict( results: Iterable[TaggedResult], diff --git a/selene-sim/python/tests/test_build_validation.py b/selene-sim/python/tests/test_build_validation.py index d4277e5..ba35903 100644 --- a/selene-sim/python/tests/test_build_validation.py +++ b/selene-sim/python/tests/test_build_validation.py @@ -9,7 +9,7 @@ from selene_sim import Quest -@pytest.mark.xfail( +@pytest.mark.skipif( platform.system() == "Windows", reason=( "As Lief doesn't support COFF formats yet, we can't " @@ -83,7 +83,7 @@ def main() -> None: build(contents, strict=True) -@pytest.mark.xfail( +@pytest.mark.skipif( platform.system() == "Windows", reason=( "As Lief doesn't support COFF formats yet, we can't " diff --git a/selene-sim/python/tests/test_qis.py b/selene-sim/python/tests/test_qis.py index 6373af6..61a9442 100644 --- a/selene-sim/python/tests/test_qis.py +++ b/selene-sim/python/tests/test_qis.py @@ -1,17 +1,43 @@ import pytest from pathlib import Path import platform +from unittest.mock import patch import yaml from selene_sim.event_hooks import CircuitExtractor, MetricStore, MultiEventHook from selene_sim import Quest from selene_sim.build import build from selene_helios_qis_plugin import HeliosInterface +from selene_stim_plugin import StimPlugin RESOURCE_DIR = Path(__file__).parent / "resources" QIS_RESOURCE_DIR = RESOURCE_DIR / "qis" +def test_helios_interface_windows_prefers_mingw_static_lib(): + with ( + patch("platform.system", return_value="Windows"), + patch("pathlib.Path.exists", return_value=True), + ): + library_file = HeliosInterface().library_file + assert library_file.name == "libhelios_selene_interface.a" + + +def test_helios_interface_windows_falls_back_to_msvc_lib(): + with ( + patch("platform.system", return_value="Windows"), + patch("pathlib.Path.exists", return_value=False), + ): + library_file = HeliosInterface().library_file + assert library_file.name == "helios_selene_interface.lib" + + +def test_stim_plugin_windows_library_search_dirs(): + with patch("platform.system", return_value="Windows"): + search_dirs = StimPlugin().library_search_dirs + assert search_dirs and search_dirs[0].name == "lib" + + def get_platform_suffix(): arch = platform.machine() system = platform.system() @@ -31,12 +57,31 @@ def get_platform_suffix(): case "darwin" | "macos": target_system = "apple-darwin" case "windows": - target_system = "windows-msvc" + target_system = "windows-gnu" case _: raise RuntimeError(f"Unsupported OS: {system}") return f"{target_arch}-{target_system}" +def get_helios_resource(program_name: str) -> Path: + """Return the platform-specific Helios IR fixture for a program. + + On Windows we prefer the GNU-target fixture name, but fall back to the + existing MSVC fixture when a GNU fixture file is not present. + """ + filename = f"{program_name}-{get_platform_suffix()}.ll" + helios_file = QIS_RESOURCE_DIR / "helios" / filename + if helios_file.exists(): + return helios_file + if platform.system().lower() == "windows": + fallback = ( + QIS_RESOURCE_DIR / "helios" / f"{program_name}-x86_64-windows-msvc.ll" + ) + if fallback.exists(): + return fallback + return helios_file + + @pytest.mark.parametrize( "program_name", [ @@ -44,8 +89,7 @@ def get_platform_suffix(): ], ) def test_qis(snapshot, program_name: str): - filename = f"{program_name}-{get_platform_suffix()}.ll" - helios_file = QIS_RESOURCE_DIR / "helios" / filename + helios_file = get_helios_resource(program_name) assert helios_file.exists() helios_build = build(helios_file, interface=HeliosInterface()) @@ -68,8 +112,7 @@ def test_qis(snapshot, program_name: str): ], ) def test_qis_circuit_log(snapshot, program_name: str): - filename = f"{program_name}-{get_platform_suffix()}.ll" - helios_file = QIS_RESOURCE_DIR / "helios" / filename + helios_file = get_helios_resource(program_name) assert helios_file.exists() helios_build = build(helios_file, interface=HeliosInterface()) diff --git a/uv.lock b/uv.lock index 63b5d03..5e296c5 100644 --- a/uv.lock +++ b/uv.lock @@ -1354,7 +1354,7 @@ requires-dist = [ { name = "networkx", specifier = ">=2.6,<4" }, { name = "pydot", specifier = ">=4.0.0" }, { name = "pyyaml", specifier = "~=6.0" }, - { name = "qir-qis", specifier = ">=0.1.1" }, + { name = "qir-qis", specifier = ">=0.1.2,<0.1.4" }, { name = "typing-extensions", specifier = ">=4" }, { name = "ziglang", specifier = "~=0.13" }, ]