diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b165846..9e42857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,20 +77,6 @@ jobs: python: ['3.14', '3.13', '3.12', '3.11', '3.10'] exclude: - # Broken ctypes find_library - - os: - category: macos - platform: arm64 - python: '3.10' - - os: - category: macos - platform: arm64 - python: '3.11' - - os: - category: macos - platform: arm64 - python: '3.12' - # Fails to call mono methods - os: category: windows diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index 8ee58f9..e3353fc 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -5,6 +5,8 @@ from pathlib import Path from collections.abc import Iterator +from ctypes.util import find_library + from ..types import StrOrPath from .runtime_spec import DotnetCoreRuntimeSpec @@ -107,11 +109,17 @@ def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: return find_runtimes_in_root(dotnet_root) -def find_libmono(*, assembly_dir: StrOrPath | None = None, sgen: bool = True) -> Path: # noqa: F821 +def find_libmono( + *, assembly_dir: StrOrPath | None = None, sgen: bool = True +) -> Path | None: """Find a suitable libmono dynamic library - On Windows and macOS, we check the default installation directories. + On Windows, we check the default installation directory. + On macOS, we check Homebrew (with architecture awareness) and the framework. + On Unix-like systems, we check Homebrew and system locations. + :param assembly_dir: + Optional directory to search for libmono :param sgen: Whether to look for an SGen or Boehm GC instance. This parameter is ignored on Windows, as only ``sgen`` is installed with the default @@ -119,35 +127,67 @@ def find_libmono(*, assembly_dir: StrOrPath | None = None, sgen: bool = True) -> :return: Path to usable ``libmono`` """ - unix_name = f"mono{'sgen' if sgen else ''}-2.0" + if sys.platform == "win32": - if sys.maxsize > 2**32: - prog_files = os.environ.get("ProgramFiles") - else: - prog_files = os.environ.get("ProgramFiles(x86)") + return _find_mono_windows() - if prog_files is None: - raise RuntimeError("Could not determine Program Files location") + else: + return _find_mono_unix(assembly_dir=assembly_dir, sgen=sgen) - # Ignore sgen on Windows, the main installation only contains this DLL - path = Path(prog_files) / "Mono/bin/mono-2.0-sgen.dll" - - elif sys.platform == "darwin": - path = ( - Path("/Library/Frameworks/Mono.framework/Versions/Current/lib") - / f"lib{unix_name}.dylib" - ) +def _find_mono_windows() -> Path | None: + if sys.maxsize > 2**32: + prog_files = os.environ.get("ProgramFiles") else: - if assembly_dir is None: - from ctypes.util import find_library + prog_files = os.environ.get("ProgramFiles(x86)") - path = find_library(unix_name) - else: - libname: str = "lib" + unix_name + ".so" - path = Path(assembly_dir) / "lib" / libname + if prog_files is None: + raise RuntimeError("Could not determine Program Files location") - if path is None: - raise RuntimeError("Could not find libmono") + # Ignore sgen on Windows, the main installation only contains this DLL + return Path(prog_files) / "Mono/bin/mono-2.0-sgen.dll" + + +def _find_mono_unix( + *, assembly_dir: StrOrPath | None = None, sgen: bool = True +) -> Path | None: + macos = sys.platform == "darwin" + + unix_name = f"mono{'sgen' if sgen else ''}-2.0" + ext = ".dylib" if macos else ".so" + + lib_filename = f"lib{unix_name}{ext}" + + if assembly_dir is not None: + candidate = Path(assembly_dir) / "lib" / lib_filename + if candidate.exists(): + return candidate + + if res := find_library(unix_name): + return Path(res) + + if macos: + res = ( + Path("/Library/Frameworks/Mono.framework/Versions/Current/lib") + / lib_filename + ) + if res.exists(): + return res + + # Use HOMEBREW_PREFIX environment variable if available + if homebrew_prefix := os.environ.get("HOMEBREW_PREFIX"): + res = Path(homebrew_prefix) / "opt/mono/lib" / lib_filename + if res.exists(): + return res + + # Check for native Apple Silicon (arm64) + if platform.machine() == "arm64": + res = Path("/opt/homebrew/opt/mono/lib") / lib_filename + if res.exists(): + return res + else: + res = Path("/usr/local/opt/mono/lib") / lib_filename + if res.exists(): + return res - return Path(path) + raise RuntimeError("Could not find libmono")