diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml new file mode 100644 index 0000000..c451f0f --- /dev/null +++ b/.github/workflows/ci-arm.yml @@ -0,0 +1,42 @@ +name: ARM64 Tests + +on: + push: + branches: master + pull_request: + +jobs: + build: + runs-on: [self-hosted, linux, ARM64] + + steps: + - uses: actions/checkout@v2 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 3.1.x + 6.0.x + + - name: Create virtualenv + run: | + virtualenv -p python3 venv + + - name: Install dependencies + run: | + source venv/bin/activate + python -m pip install --upgrade pip + pip install pytest cffi + + # Assumes recent Mono + + - name: Build + run: | + source venv/bin/activate + pip install -e . + + - name: Test with pytest + run: | + source venv/bin/activate + pytest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7627b1e..ae40be9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,35 +1,63 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - name: Python Tests -on: [push, pull_request] +on: + push: + branches: master + pull_request: jobs: build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + - uses: astral-sh/setup-uv@v4 + - name: Build + run: uv build + - name: Upload source distribution + uses: actions/upload-artifact@v3 + with: + name: build-output + path: "dist/*" + if-no-files-found: error + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depths: 0 + - uses: astral-sh/setup-uv@v4 + - name: Install Ruff + run: uv tool install ruff + - name: Check format + run: ruff format --check + - name: Check lints + run: ruff check + + test: runs-on: ${{ matrix.os }} + needs: build strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.9', '3.8', '3.7', '3.6', pypy3] + os: [ubuntu-22.04, windows-latest, macos-13] + python: ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8'] # pypy3 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: astral-sh/setup-uv@v4 with: python-version: ${{ matrix.python }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest cffi - - - name: Build - run: | - pip install -e . - - name: Cache Mono if: runner.os == 'Windows' uses: actions/cache@v2 @@ -42,6 +70,52 @@ jobs: run: | choco install -y mono ${{ matrix.python == 'pypy3' && '--x86' || '' }} + - name: Install dependencies + run: | + uv venv + uv pip install pytest + + - name: Download wheel + uses: actions/download-artifact@v3 + with: + name: build-output + path: dist/ + + - name: Install wheel + shell: bash + run: | + uv pip install dist/*.whl + - name: Test with pytest run: | - pytest + uv run pytest + + deploy: + runs-on: ubuntu-latest + needs: [build, test] + + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: build-output + path: dist/ + + - name: Deploy to Test PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: startsWith(github.ref, 'refs/head/master') + with: + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + files: dist/* + + - name: Deploy to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..bce19d4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: Documentation + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build Sphinx documentation + run: | + pip install -r doc/requirements.txt + sphinx-build doc/ ./doc/_build/html/ + + - name: Upload artifact + # Automatically uploads an artifact from the './_site' directory by default + uses: actions/upload-pages-artifact@v1 + with: + path: doc/_build/html/ + + deploy: + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ac8f31 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Attach", + "type": "mono", + "request": "attach", + "address": "localhost", + "port": 5831 + } + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 33e799f..b8989d3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Benedikt Reinartz +Copyright (c) 2019-2022 Benedikt Reinartz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b8c47d5..e9bc6d5 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ [![CI](https://github.com/pythonnet/clr-loader/workflows/Python%20Tests/badge.svg)](https://github.com/pythonnet/clr-loader/actions) [![Pypi](https://img.shields.io/pypi/v/clr-loader.svg)](https://pypi.org/project/clr-loader/) +[![Conda Version](https://img.shields.io/conda/vn/conda-forge/clr_loader.svg)](https://anaconda.org/conda-forge/clr_loader) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -Implements a generic interface for loading one of the CLR (.NET) runtime implementations and calling simple functions on them. +Implements a generic interface for loading one of the CLR (.NET) runtime +implementations and calling simple functions on them. + +Documentation is available at https://pythonnet.github.io/clr-loader/. diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index aa4c7a9..71eb09d 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -1,52 +1,176 @@ -from typing import Dict, Optional +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Dict, Optional, Sequence -from .wrappers import Runtime -from .util.find import find_libmono, find_dotnet_root +from .types import Assembly, Runtime, RuntimeInfo +from .util import StrOrPath +from .util.find import find_dotnet_root, find_libmono, find_runtimes +from .util.runtime_spec import DotnetCoreRuntimeSpec -__all__ = ["get_mono", "get_netfx", "get_coreclr"] +__all__ = [ + "get_mono", + "get_netfx", + "get_coreclr", + "find_dotnet_root", + "find_libmono", + "find_runtimes", + "Runtime", + "Assembly", + "RuntimeInfo", + "DotnetCoreRuntimeSpec", +] def get_mono( - domain: Optional[str] = None, - config_file: Optional[str] = None, - global_config_file: Optional[str] = None, - libmono: Optional[str] = None, + *, + # domain: Optional[str] = None, + config_file: Optional[StrOrPath] = None, + global_config_file: Optional[StrOrPath] = None, + libmono: Optional[StrOrPath] = None, sgen: bool = True, + debug: bool = False, + jit_options: Optional[Sequence[str]] = None, + assembly_dir: Optional[str] = None, + config_dir: Optional[str] = None, + set_signal_chaining: bool = False, ) -> Runtime: + """Get a Mono runtime instance + + :param config_file: + Path to the domain configuration file + :param global_config_file: + Path to the global configuration file to load (defaults to, e.g., + ``/etc/mono/config``) + :param libmono: + Path to the Mono runtime dll/so/dylib. If this is not specified, we try + to discover a globally installed instance using :py:func:`find_libmono` + :param sgen: + If ``libmono`` is not specified, this is passed to + :py:func:`find_libmono` + :param debug: + Whether to initialise Mono debugging + :param jit_options: + "Command line options" passed to Mono's ``mono_jit_parse_options`` + :param assembly_dir: + The base directory for assemblies, passed to ``mono_set_dirs`` + :param config_dir: + The base directory for configuration files, passed to ``mono_set_dirs`` + :param set_signal_chaining: + Whether to enable signal chaining, passed to ``mono_set_signal_chaining``. + If it is enabled, the runtime saves the original signal handlers before + installing its own, and calls the original ones in the following cases: + - SIGSEGV/SIGABRT while executing native code + - SIGPROF + - SIGFPE + - SIGQUIT + - SIGUSR2 + This currently only works on POSIX platforms + """ from .mono import Mono + libmono = _maybe_path(libmono) if libmono is None: - libmono = find_libmono(sgen) + libmono = find_libmono(sgen=sgen, assembly_dir=assembly_dir) impl = Mono( - domain=domain, - config_file=config_file, - global_config_file=global_config_file, + # domain=domain, + debug=debug, + jit_options=jit_options, + config_file=_maybe_path(config_file), + global_config_file=_maybe_path(global_config_file), libmono=libmono, + assembly_dir=assembly_dir, + config_dir=config_dir, + set_signal_chaining=set_signal_chaining, ) - return Runtime(impl) + return impl def get_coreclr( - runtime_config: str, - dotnet_root: Optional[str] = None, + *, + runtime_config: Optional[StrOrPath] = None, + dotnet_root: Optional[StrOrPath] = None, properties: Optional[Dict[str, str]] = None, + runtime_spec: Optional[DotnetCoreRuntimeSpec] = None, ) -> Runtime: + """Get a CoreCLR (.NET Core) runtime instance + + The returned ``DotnetCoreRuntime`` also acts as a mapping of the config + properties. They can be retrieved using the index operator and can be + written until the runtime is initialized. The runtime is initialized when + the first function object is retrieved. + + :param runtime_config: + Pass to a ``runtimeconfig.json`` as generated by + ``dotnet publish``. If this parameter is not given, a temporary runtime + config will be generated. + :param dotnet_root: + The root directory of the .NET Core installation. If this is not + specified, we try to discover it using :py:func:`find_dotnet_root`. + :param properties: + Additional runtime properties. These can also be passed using the + ``configProperties`` section in the runtime config. + :param runtime_spec: + If the ``runtime_config`` is not specified, the concrete runtime to use + can be controlled by passing this parameter. Possible values can be + retrieved using :py:func:`find_runtimes`.""" from .hostfxr import DotnetCoreRuntime + dotnet_root = _maybe_path(dotnet_root) if dotnet_root is None: dotnet_root = find_dotnet_root() + temp_dir = None + runtime_config = _maybe_path(runtime_config) + if runtime_config is None: + if runtime_spec is None: + candidates = [ + rt for rt in find_runtimes() if rt.name == "Microsoft.NETCore.App" + ] + candidates.sort(key=lambda spec: spec.version, reverse=True) + if not candidates: + raise RuntimeError("Failed to find a suitable runtime") + + runtime_spec = candidates[0] + + temp_dir = TemporaryDirectory() + runtime_config = Path(temp_dir.name) / "runtimeconfig.json" + + with open(runtime_config, "w") as f: + runtime_spec.write_config(f) + impl = DotnetCoreRuntime(runtime_config=runtime_config, dotnet_root=dotnet_root) if properties: for key, value in properties.items(): impl[key] = value - return Runtime(impl) + if temp_dir: + temp_dir.cleanup() + + return impl + +def get_netfx( + *, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None +) -> Runtime: + """Get a .NET Framework runtime instance -def get_netfx(name: Optional[str] = None, config_file: Optional[str] = None) -> Runtime: + :param domain: + Name of the domain to create. If no value is passed, assemblies will be + loaded into the root domain. + :param config_file: + Configuration file to use to initialize the ``AppDomain``. This will + only be used for non-root-domains as we can not control the + configuration of the implicitly loaded root domain. + """ from .netfx import NetFx - impl = NetFx(name=name, config_file=config_file) - return Runtime(impl) + impl = NetFx(domain=domain, config_file=_maybe_path(config_file)) + return impl + + +def _maybe_path(p: Optional[StrOrPath]) -> Optional[Path]: + if p is None: + return None + else: + return Path(p) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 114803c..23debae 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -1,7 +1,6 @@ -import glob -import os import sys -from typing import Optional +from pathlib import Path +from typing import Optional, Tuple import cffi # type: ignore @@ -9,46 +8,66 @@ __all__ = ["ffi", "load_hostfxr", "load_mono", "load_netfx"] -ffi = cffi.FFI() +ffi = cffi.FFI() # type: ignore for cdef in hostfxr.cdef + mono.cdef + netfx.cdef: ffi.cdef(cdef) -def load_hostfxr(dotnet_root: str): +def load_hostfxr(dotnet_root: Path): hostfxr_name = _get_dll_name("hostfxr") - hostfxr_path = os.path.join(dotnet_root, "host", "fxr", "?.*", hostfxr_name) + dotnet_root = dotnet_root.absolute() - for hostfxr_path in reversed(sorted(glob.glob(hostfxr_path))): + # This will fail as soon as .NET hits version 10, but hopefully by then + # we'll have a more robust way of finding the libhostfxr + hostfxr_path = dotnet_root / "host" / "fxr" + hostfxr_paths = hostfxr_path.glob(f"?.*/{hostfxr_name}") + + for hostfxr_path in reversed(sorted(hostfxr_paths, key=_path_to_version)): try: - return ffi.dlopen(hostfxr_path) + return ffi.dlopen(str(hostfxr_path)) except Exception: pass + try: + return ffi.dlopen(str(dotnet_root / hostfxr_name)) + except Exception: + pass + raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}") -def load_mono(path: Optional[str] = None): +def load_mono(path: Optional[Path] = None): # Preload C++ standard library, Mono needs that and doesn't properly link against it - if sys.platform.startswith("linux"): + if sys.platform == "linux": ffi.dlopen("stdc++", ffi.RTLD_GLOBAL) - return ffi.dlopen(path, ffi.RTLD_GLOBAL) + path_str = str(path) if path else None + return ffi.dlopen(path_str, ffi.RTLD_GLOBAL) def load_netfx(): if sys.platform != "win32": raise RuntimeError(".NET Framework is only supported on Windows") - dirname = os.path.join(os.path.dirname(__file__), "dlls") - if sys.maxsize > 2 ** 32: + dirname = Path(__file__).parent / "dlls" + if sys.maxsize > 2**32: arch = "amd64" else: arch = "x86" - path = os.path.join(dirname, arch, "ClrLoader.dll") + path = dirname / arch / "ClrLoader.dll" + + return ffi.dlopen(str(path)) + - return ffi.dlopen(path) +def _path_to_version(path: Path) -> Tuple[int, int, int]: + name = path.parent.name + try: + res = list(map(int, name.split("."))) + return tuple(res + [0, 0, 0])[:3] + except Exception: + return (0, 0, 0) def _get_dll_name(name: str) -> str: diff --git a/clr_loader/ffi/hostfxr.py b/clr_loader/ffi/hostfxr.py index 9dc47ee..f5d74bc 100644 --- a/clr_loader/ffi/hostfxr.py +++ b/clr_loader/ffi/hostfxr.py @@ -2,6 +2,7 @@ import sys + cdef = [] if sys.platform == "win32": diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index 427360c..c194393 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -11,8 +11,21 @@ typedef struct _MonoMethod MonoMethod; typedef struct _MonoObject MonoObject; +typedef enum { + MONO_DEBUG_FORMAT_NONE, + MONO_DEBUG_FORMAT_MONO, + /* Deprecated, the mdb debugger is not longer supported. */ + MONO_DEBUG_FORMAT_DEBUGGER +} MonoDebugFormat; + +char* mono_get_runtime_build_info (void); + MonoDomain* mono_jit_init(const char *root_domain_name); void mono_jit_cleanup(MonoDomain *domain); +void mono_jit_parse_options(int argc, char * argv[]); + +void mono_debug_init (MonoDebugFormat format); + MonoAssembly* mono_domain_assembly_open(MonoDomain *domain, const char *name); MonoImage* mono_assembly_get_image(MonoAssembly *assembly); @@ -26,5 +39,10 @@ MonoObject* mono_runtime_invoke(MonoMethod *method, void *obj, void **params, MonoObject **exc); void* mono_object_unbox(MonoObject *object); + +void mono_set_dirs(const char *assembly_dir, const char* config_dir); + +void mono_set_signal_chaining(bool chain_signals); + """ ) diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index fed2f77..225b4c7 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -1,29 +1,49 @@ -import os import sys +from pathlib import Path +from typing import Generator, Tuple from .ffi import ffi, load_hostfxr -from .util import check_result, find_dotnet_root +from .types import Runtime, RuntimeInfo, StrOrPath +from .util import check_result __all__ = ["DotnetCoreRuntime"] +_IS_SHUTDOWN = False -class DotnetCoreRuntime: - def __init__(self, runtime_config: str, dotnet_root: str): - self._dotnet_root = dotnet_root or find_dotnet_root() + +class DotnetCoreRuntime(Runtime): + def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): + self._handle = None + + if _IS_SHUTDOWN: + raise RuntimeError("Runtime can not be reinitialized") + + self._dotnet_root = Path(dotnet_root) self._dll = load_hostfxr(self._dotnet_root) - self._is_finalized = False self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config) - self._load_func = _get_load_func(self._dll, self._handle) + self._load_func = None + + for key, value in params.items(): + self[key] = value + + # TODO: Get version + self._version = "" @property - def dotnet_root(self) -> str: + def dotnet_root(self) -> Path: return self._dotnet_root @property - def is_finalized(self) -> bool: - return self._is_finalized + def is_initialized(self) -> bool: + return self._load_func is not None + + @property + def is_shutdown(self) -> bool: + return _IS_SHUTDOWN def __getitem__(self, key: str) -> str: + if self.is_shutdown: + raise RuntimeError("Runtime is shut down") buf = ffi.new("char_t**") res = self._dll.hostfxr_get_runtime_property_value( self._handle, encode(key), buf @@ -34,15 +54,17 @@ def __getitem__(self, key: str) -> str: return decode(buf[0]) def __setitem__(self, key: str, value: str) -> None: - if self.is_finalized: - raise RuntimeError("Already finalized") + if self.is_initialized: + raise RuntimeError("Already initialized") res = self._dll.hostfxr_set_runtime_property_value( self._handle, encode(key), encode(value) ) check_result(res) - def __iter__(self): + def __iter__(self) -> Generator[Tuple[str, str], None, None]: + if self.is_shutdown: + raise RuntimeError("Runtime is shut down") max_size = 100 size_ptr = ffi.new("size_t*") size_ptr[0] = max_size @@ -51,25 +73,31 @@ def __iter__(self): values_ptr = ffi.new("char_t*[]", max_size) res = self._dll.hostfxr_get_runtime_properties( - self._dll._handle, size_ptr, keys_ptr, values_ptr + self._handle, size_ptr, keys_ptr, values_ptr ) check_result(res) for i in range(size_ptr[0]): yield (decode(keys_ptr[i]), decode(values_ptr[i])) - def get_callable(self, assembly_path: str, typename: str, function: str): + def _get_load_func(self): + if self._load_func is None: + self._load_func = _get_load_func(self._dll, self._handle) + + return self._load_func + + def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): # TODO: Maybe use coreclr_get_delegate as well, supported with newer API # versions of hostfxr - self._is_finalized = True # Append assembly name to typename - assembly_name, _ = os.path.splitext(os.path.basename(assembly_path)) + assembly_path = Path(assembly_path) + assembly_name = assembly_path.stem typename = f"{typename}, {assembly_name}" delegate_ptr = ffi.new("void**") - res = self._load_func( - encode(assembly_path), + res = self._get_load_func()( + encode(str(assembly_path)), encode(typename), encode(function), ffi.NULL, @@ -84,22 +112,28 @@ def shutdown(self) -> None: self._dll.hostfxr_close(self._handle) self._handle = None - def __del__(self): - self.shutdown() + def info(self): + return RuntimeInfo( + kind="CoreCLR", + version=self._version, + initialized=self._handle is not None, + shutdown=self._handle is None, + properties=dict(self) if not _IS_SHUTDOWN else {}, + ) -def _get_handle(dll, dotnet_root: str, runtime_config: str): +def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): params = ffi.new("hostfxr_initialize_parameters*") params.size = ffi.sizeof("hostfxr_initialize_parameters") # params.host_path = ffi.new("char_t[]", encode(sys.executable)) params.host_path = ffi.NULL - dotnet_root_p = ffi.new("char_t[]", encode(dotnet_root)) + dotnet_root_p = ffi.new("char_t[]", encode(str(Path(dotnet_root)))) params.dotnet_root = dotnet_root_p handle_ptr = ffi.new("hostfxr_handle*") res = dll.hostfxr_initialize_for_runtime_config( - encode(runtime_config), params, handle_ptr + encode(str(Path(runtime_config))), params, handle_ptr ) check_result(res) @@ -119,17 +153,16 @@ def _get_load_func(dll, handle): if sys.platform == "win32": - def encode(string): + def encode(string: str): return string - def decode(char_ptr): + def decode(char_ptr) -> str: return ffi.string(char_ptr) - else: - def encode(string): + def encode(string: str): return string.encode("utf8") - def decode(char_ptr): + def decode(char_ptr) -> str: return ffi.string(char_ptr).decode("utf8") diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 10e422f..1899ea3 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -1,31 +1,44 @@ import atexit -from typing import Optional - -from .ffi import load_mono, ffi +import re +from pathlib import Path +from typing import Any, Dict, Optional, Sequence +from .ffi import ffi, load_mono +from .types import Runtime, RuntimeInfo +from .util import optional_path_as_string, path_as_string __all__ = ["Mono"] -_MONO = None -_ROOT_DOMAIN = None +_MONO: Any = None +_ROOT_DOMAIN: Any = None -class Mono: +class Mono(Runtime): def __init__( self, - libmono, + libmono: Optional[Path], *, - domain=None, - config_file: Optional[str] = None, - global_config_file: Optional[str] = None, + domain: Optional[str] = None, + debug: bool = False, + jit_options: Optional[Sequence[str]] = None, + config_file: Optional[Path] = None, + global_config_file: Optional[Path] = None, + assembly_dir: Optional[str] = None, + config_dir: Optional[str] = None, + set_signal_chaining: bool = False, ): - self._assemblies = {} + self._assemblies: Dict[Path, Any] = {} - initialize( - config_file=config_file, - global_config_file=global_config_file, + self._version = initialize( + config_file=optional_path_as_string(config_file), + debug=debug, + jit_options=jit_options, + global_config_file=optional_path_as_string(global_config_file), libmono=libmono, + assembly_dir=assembly_dir, + config_dir=config_dir, + set_signal_chaining=set_signal_chaining, ) if domain is None: @@ -33,11 +46,12 @@ def __init__( else: raise NotImplementedError - def get_callable(self, assembly_path, typename, function): + def _get_callable(self, assembly_path, typename, function): + assembly_path = Path(assembly_path) assembly = self._assemblies.get(assembly_path) if not assembly: assembly = _MONO.mono_domain_assembly_open( - self._domain, assembly_path.encode("utf8") + self._domain, path_as_string(assembly_path).encode("utf8") ) _check_result(assembly, f"Unable to load assembly {assembly_path}") self._assemblies[assembly_path] = assembly @@ -53,12 +67,27 @@ def get_callable(self, assembly_path, typename, function): return MonoMethod(method) + def info(self) -> RuntimeInfo: + return RuntimeInfo( + kind="Mono", + version=self._version, + initialized=True, + shutdown=_MONO is None, + properties={}, + ) + + def shutdown(self) -> None: + # We don't implement non-root-domains, yet. When we do, it needs to be + # released here. + pass + class MethodDesc: def __init__(self, typename, function): self._desc = f"{typename}:{function}" self._ptr = _MONO.mono_method_desc_new( - self._desc.encode("utf8"), 1 # include_namespace + self._desc.encode("utf8"), + 1, # include_namespace ) def search(self, image): @@ -94,14 +123,22 @@ def __call__(self, ptr, size): def initialize( - libmono: str, + libmono: Optional[Path], + debug: bool = False, + jit_options: Optional[Sequence[str]] = None, config_file: Optional[str] = None, global_config_file: Optional[str] = None, -) -> None: + assembly_dir: Optional[str] = None, + config_dir: Optional[str] = None, + set_signal_chaining: bool = False, +) -> str: global _MONO, _ROOT_DOMAIN if _MONO is None: _MONO = load_mono(libmono) + if assembly_dir is not None and config_dir is not None: + _MONO.mono_set_dirs(assembly_dir.encode("utf8"), config_dir.encode("utf8")) + # Load in global config (i.e /etc/mono/config) global_encoded = global_config_file or ffi.NULL _MONO.mono_config_parse(global_encoded) @@ -113,13 +150,45 @@ def initialize( config_encoded = config_file.encode("utf8") + if jit_options: + options = [ffi.new("char[]", o.encode("utf8")) for o in jit_options] + _MONO.mono_jit_parse_options(len(options), options) + else: + options = [] + + if set_signal_chaining: + _MONO.mono_set_signal_chaining(True) + + if debug: + _MONO.mono_debug_init(_MONO.MONO_DEBUG_FORMAT_MONO) + _ROOT_DOMAIN = _MONO.mono_jit_init(b"clr_loader") _MONO.mono_domain_set_config(_ROOT_DOMAIN, b".", config_encoded) _check_result(_ROOT_DOMAIN, "Failed to initialize Mono") - atexit.register(_release) + + build = _MONO.mono_get_runtime_build_info() + _check_result(build, "Failed to get Mono version") + ver_str = ffi.string(build).decode("utf8") # e.g. '6.12.0.122 (tarball)' + + ver = re.match(r"^(?P\d+)\.(?P\d+)\.[\d.]+", ver_str) + if ver is not None: + major = int(ver.group("major")) + minor = int(ver.group("minor")) + + if major < 6 or (major == 6 and minor < 12): + import warnings + + warnings.warn( + "Hosting Mono versions before v6.12 is known to be problematic. " + "If the process crashes shortly after you see this message, try " + "updating Mono to at least v6.12." + ) + + atexit.register(_release) + return ver_str -def _release(): +def _release() -> None: global _MONO, _ROOT_DOMAIN if _ROOT_DOMAIN is not None and _MONO is not None: _MONO.mono_jit_cleanup(_ROOT_DOMAIN) @@ -127,6 +196,6 @@ def _release(): _ROOT_DOMAIN = None -def _check_result(res, msg): +def _check_result(res: Any, msg: str) -> None: if res == ffi.NULL or not res: raise RuntimeError(msg) diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 6b6c003..4d46b37 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -1,28 +1,58 @@ import atexit -from typing import Optional, Any +from pathlib import Path +from typing import Any, Optional + from .ffi import ffi, load_netfx +from .types import Runtime, RuntimeInfo, StrOrPath + +_FW: Any = None -_FW: Optional[Any] = None +class NetFx(Runtime): + def __init__( + self, domain: Optional[str] = None, config_file: Optional[Path] = None + ): + self._domain = None -class NetFx: - def __init__(self, name: Optional[str] = None, config_file: Optional[str] = None): initialize() - self._domain = _FW.pyclr_create_appdomain( - name or ffi.NULL, config_file or ffi.NULL + if config_file is not None: + config_file_s = str(config_file).encode("utf8") + else: + config_file_s = ffi.NULL + + domain_s = domain.encode("utf8") if domain else ffi.NULL + + self._domain_name = domain + self._config_file = config_file + self._domain = _FW.pyclr_create_appdomain(domain_s, config_file_s) + + def info(self) -> RuntimeInfo: + return RuntimeInfo( + kind=".NET Framework", + version="", + initialized=True, + shutdown=_FW is None, + properties=dict( + domain=self._domain_name or "", config_file=str(self._config_file) + ), ) - def get_callable(self, assembly_path: str, typename: str, function: str): + def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): func = _FW.pyclr_get_function( self._domain, - assembly_path.encode("utf8"), + str(Path(assembly_path)).encode("utf8"), typename.encode("utf8"), function.encode("utf8"), ) + if func == ffi.NULL: + raise RuntimeError( + f"Failed to resolve {typename}.{function} from {assembly_path}" + ) + return func - def __del__(self): + def shutdown(self): if self._domain and _FW: _FW.pyclr_close_appdomain(self._domain) diff --git a/clr_loader/types.py b/clr_loader/types.py new file mode 100644 index 0000000..15c1e30 --- /dev/null +++ b/clr_loader/types.py @@ -0,0 +1,146 @@ +from abc import ABCMeta, abstractmethod +from dataclasses import dataclass, field +from os import PathLike +from typing import Any, Callable, Dict, Optional, Union + +__all__ = ["StrOrPath"] + +StrOrPath = Union[str, PathLike] + + +@dataclass +class RuntimeInfo: + """Information on a Runtime instance + + An informative text can be retrieved from this by converting it to a + ``str``, in particular the following results in readable debug information: + + >>> ri = RuntimeInfo() + >>> print(ri) + 6.12.0.122 (tarball) + Runtime: Mono + ============= + Version: 6.12.0.122 (tarball) + Initialized: True + Shut down: False + Properties: + """ + + kind: str + version: str + initialized: bool + shutdown: bool + properties: Dict[str, str] = field(repr=False) + + def __str__(self) -> str: + return ( + f"Runtime: {self.kind}\n" + "=============\n" + f" Version: {self.version}\n" + f" Initialized: {self.initialized}\n" + f" Shut down: {self.shutdown}\n" + f" Properties:\n" + + "\n".join( + f" {key} = {_truncate(value, 65 - len(key))}" + for key, value in self.properties.items() + ) + ) + + +class ClrFunction: + def __init__( + self, runtime: "Runtime", assembly: StrOrPath, typename: str, func_name: str + ): + self._assembly = assembly + self._class = typename + self._name = func_name + + self._callable = runtime._get_callable(assembly, typename, func_name) + + def __call__(self, buffer: bytes) -> int: + from .ffi import ffi + + buf_arr = ffi.from_buffer("char[]", buffer) + return self._callable(ffi.cast("void*", buf_arr), len(buf_arr)) + + def __repr__(self) -> str: + return f"" + + +class Assembly: + def __init__(self, runtime: "Runtime", path: StrOrPath): + self._runtime = runtime + self._path = path + + def get_function(self, name: str, func: Optional[str] = None) -> ClrFunction: + """Get a wrapped .NET function instance + + The function must be ``static``, and it must have the signature + ``int Func(IntPtr ptr, int size)``. The returned wrapped instance will + take a ``binary`` and call the .NET function with a pointer to that + buffer and the buffer length. The buffer is reflected using CFFI's + `from_buffer`. + + :param name: If ``func`` is not given, this is the fully qualified name + of the function. If ``func`` is given, this is the fully + qualified name of the containing class + :param func: Name of the function + :return: A function object that takes a single ``binary`` parameter + and returns an ``int`` + """ + if func is None: + name, func = name.rsplit(".", 1) + + return ClrFunction(self._runtime, self._path, name, func) + + def __repr__(self) -> str: + return f"" + + +class Runtime(metaclass=ABCMeta): + """CLR Runtime + + Encapsulates the lifetime of a CLR (.NET) runtime. If the instance is + deleted, the runtime will be shut down. + """ + + @abstractmethod + def info(self) -> RuntimeInfo: + """Get configuration and version information""" + pass + + def get_assembly(self, assembly_path: StrOrPath) -> Assembly: + """Get an assembly wrapper + + This function does not guarantee that the respective assembly is or can + be loaded. Due to the design of the different hosting APIs, loading only + happens when the first function is referenced, and only then potential + errors will be raised.""" + return Assembly(self, assembly_path) + + @abstractmethod + def _get_callable( + self, assembly_path: StrOrPath, typename: str, function: str + ) -> Callable[[Any, int], Any]: + """Private function to retrieve a low-level callable object""" + pass + + @abstractmethod + def shutdown(self) -> None: + """Shut down the runtime as much as possible + + Implementations should still be able to "reinitialize", thus the final + cleanup will usually happen in an ``atexit`` handler.""" + pass + + def __del__(self) -> None: + self.shutdown() + + +def _truncate(string: str, length: int) -> str: + if length <= 1: + raise TypeError("length must be > 1") + if len(string) > length - 1: + return f"{string[:length-1]}…" + else: + return string diff --git a/clr_loader/util/__init__.py b/clr_loader/util/__init__.py index 1498039..b1a3f16 100644 --- a/clr_loader/util/__init__.py +++ b/clr_loader/util/__init__.py @@ -1,9 +1,28 @@ +from pathlib import Path +from typing import Optional + +from ..types import StrOrPath from .clr_error import ClrError from .coreclr_errors import get_coreclr_error from .find import find_dotnet_root from .hostfxr_errors import get_hostfxr_error -__all__ = ["check_result", "find_dotnet_root"] +__all__ = [ + "check_result", + "find_dotnet_root", + "path_as_string", + "optional_path_as_string", +] + + +def optional_path_as_string(path: Optional[StrOrPath]) -> Optional[str]: + if path is None: + return None + return path_as_string(path) + + +def path_as_string(path: StrOrPath) -> str: + return str(Path(path)) def check_result(err_code: int) -> None: @@ -15,12 +34,9 @@ def check_result(err_code: int) -> None: if err_code < 0: hresult = err_code & 0xFFFF_FFFF - error = get_coreclr_error(hresult) if not error: error = get_hostfxr_error(hresult) - if not error: error = ClrError(hresult) - raise error diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index f33b0c2..65bc7ac 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -1,73 +1,151 @@ import os -import os.path +import platform import shutil import sys +from pathlib import Path +from typing import Iterator, Optional +from .runtime_spec import DotnetCoreRuntimeSpec + + +def find_dotnet_cli() -> Optional[Path]: + dotnet_path = shutil.which("dotnet") + if not dotnet_path: + return None + else: + return Path(dotnet_path) + + +def find_dotnet_root() -> Path: + """Try to discover the .NET Core root directory + + If the environment variable ``DOTNET_ROOT`` is defined, we will use that. + Otherwise, we probe the default installation paths on Windows and macOS. + + If none of these lead to a result, we try to discover the ``dotnet`` CLI + tool and use its (real) parent directory. + + Otherwise, this function raises an exception. + + :return: Path to the .NET Core root + """ -def find_dotnet_root() -> str: dotnet_root = os.environ.get("DOTNET_ROOT", None) if dotnet_root is not None: - return dotnet_root + return Path(dotnet_root) if sys.platform == "win32": # On Windows, the host library is stored separately from dotnet.exe for x86 - if sys.maxsize > 2 ** 32: - prog_files = os.environ.get("ProgramFiles") + prog_files = os.environ.get("ProgramFiles") + if not prog_files: + raise RuntimeError("Could not find ProgramFiles") + prog_files = Path(prog_files) + dotnet_root = prog_files / "dotnet" + elif sys.platform == "darwin": + if "ARM64" in os.uname().version and platform.machine() == "x86_64": + # Apple Silicon in Rosetta 2 mode + dotnet_root = Path("/usr/local/share/dotnet/x64") else: - prog_files = os.environ.get("ProgramFiles(x86)") + dotnet_root = Path("/usr/local/share/dotnet") - dotnet_root = os.path.join(prog_files, "dotnet") - if os.path.isdir(dotnet_root): - return dotnet_root + if dotnet_root is not None and dotnet_root.is_dir(): + return dotnet_root # Try to discover dotnet from PATH otherwise - dotnet_path = shutil.which("dotnet") - if not dotnet_path: + dotnet_cli = find_dotnet_cli() + if not dotnet_cli: raise RuntimeError("Can not determine dotnet root") - try: - # Pypy does not provide os.readlink right now - if hasattr(os, "readlink"): - dotnet_tmp_path = os.readlink(dotnet_path) - else: - dotnet_tmp_path = dotnet_path + return dotnet_cli.resolve().parent + + +def find_runtimes_using_cli(dotnet_cli: Path) -> Iterator[DotnetCoreRuntimeSpec]: + import re + from subprocess import check_output + + out = check_output([str(dotnet_cli), "--list-runtimes"], encoding="UTF8") + runtime_re = re.compile(r"(?P\S+) (?P\S+) \[(?P[^\]]+)\]") + + for line in out.splitlines(): + m = re.match(runtime_re, line) + if m: + path = Path(m.group("path")) + version = m.group("version") + if path.is_dir(): + yield DotnetCoreRuntimeSpec(m.group("name"), version, path / version) + + +def find_runtimes_in_root(dotnet_root: Path) -> Iterator[DotnetCoreRuntimeSpec]: + shared = dotnet_root / "shared" + for runtime in shared.iterdir(): + if runtime.is_dir(): + name = runtime.name + for version_path in runtime.iterdir(): + if version_path.is_dir(): + yield DotnetCoreRuntimeSpec(name, version_path.name, version_path) - if os.path.isabs(dotnet_tmp_path): - dotnet_path = dotnet_tmp_path - else: - dotnet_path = os.path.abspath( - os.path.join(os.path.dirname(dotnet_path), dotnet_tmp_path) - ) - except OSError: - pass - return os.path.dirname(dotnet_path) +def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: + """Find installed .NET Core runtimes + If the ``dotnet`` CLI can be found, we will call it as ``dotnet + --list-runtimes`` and parse the result. -def find_libmono(sgen: bool = True) -> str: + If it is not available, we try to discover the dotnet root directory using + :py:func:`find_dotnet_root` and enumerate the runtimes installed in the + ``shared`` subdirectory. + + :return: Iterable of :py:class:`DotnetCoreRuntimeSpec` objects + """ + dotnet_cli = find_dotnet_cli() + if dotnet_cli is not None: + return find_runtimes_using_cli(dotnet_cli) + else: + dotnet_root = find_dotnet_root() + return find_runtimes_in_root(dotnet_root) + + +def find_libmono(*, assembly_dir: str = None, sgen: bool = True) -> Path: + """Find a suitable libmono dynamic library + + On Windows and macOS, we check the default installation directories. + + :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 + installer + :return: + Path to usable ``libmono`` + """ unix_name = f"mono{'sgen' if sgen else ''}-2.0" if sys.platform == "win32": - if sys.maxsize > 2 ** 32: + if sys.maxsize > 2**32: prog_files = os.environ.get("ProgramFiles") else: prog_files = os.environ.get("ProgramFiles(x86)") + if prog_files is None: + raise RuntimeError("Could not determine Program Files location") + # Ignore sgen on Windows, the main installation only contains this DLL - path = fr"{prog_files}\Mono\bin\mono-2.0-sgen.dll" + path = Path(prog_files) / "Mono/bin/mono-2.0-sgen.dll" elif sys.platform == "darwin": path = ( - "/Library/Frameworks/Mono.framework/Versions/" - "Current" - f"/lib/lib{unix_name}.dylib" + Path("/Library/Frameworks/Mono.framework/Versions/Current/lib") + / f"lib{unix_name}.dylib" ) else: - from ctypes.util import find_library + if assembly_dir is None: + from ctypes.util import find_library - path = find_library(unix_name) + path = find_library(unix_name) + else: + libname = "lib" + unix_name + ".so" + path = Path(assembly_dir) / "lib" / libname if path is None: raise RuntimeError("Could not find libmono") - return path + return Path(path) diff --git a/clr_loader/util/runtime_spec.py b/clr_loader/util/runtime_spec.py new file mode 100644 index 0000000..2eaeb68 --- /dev/null +++ b/clr_loader/util/runtime_spec.py @@ -0,0 +1,33 @@ +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, TextIO + + +@dataclass +class DotnetCoreRuntimeSpec: + """Specification of an installed .NET Core runtime""" + + name: str + version: str + path: Path + + @property + def tfm(self) -> str: + return f"net{self.version[:3]}" + + @property + def floor_version(self) -> str: + return f"{self.version[:3]}.0" + + @property + def runtime_config(self) -> Dict[str, Any]: + return { + "runtimeOptions": { + "tfm": self.tfm, + "framework": {"name": self.name, "version": self.floor_version}, + } + } + + def write_config(self, f: TextIO) -> None: + json.dump(self.runtime_config, f) diff --git a/clr_loader/wrappers.py b/clr_loader/wrappers.py deleted file mode 100644 index e8f2a89..0000000 --- a/clr_loader/wrappers.py +++ /dev/null @@ -1,52 +0,0 @@ -from os.path import basename -from typing import Any, Optional -from .ffi import ffi - -RuntimeImpl = Any - - -class ClrFunction: - def __init__( - self, runtime: RuntimeImpl, assembly: str, typename: str, func_name: str - ): - self._assembly = assembly - self._class = typename - self._name = func_name - - self._callable = runtime.get_callable(assembly, typename, func_name) - - def __call__(self, buffer: bytes) -> int: - buf_arr = ffi.from_buffer("char[]", buffer) - return self._callable(ffi.cast("void*", buf_arr), len(buf_arr)) - - def __repr__(self) -> str: - return f"" - - -class Assembly: - def __init__(self, runtime: RuntimeImpl, path: str): - self._runtime = runtime - self._path = path - - def get_function(self, name: str, func: Optional[str] = None) -> ClrFunction: - if func is None: - name, func = name.rsplit(".", 1) - - return ClrFunction(self._runtime, self._path, name, func) - - def __getitem__(self, name: str) -> ClrFunction: - return self.get_function(name) - - def __repr__(self) -> str: - return f"" - - -class Runtime: - def __init__(self, impl: RuntimeImpl): - self._impl = impl - - def get_assembly(self, path: str) -> Assembly: - return Assembly(self._impl, path) - - def __getitem__(self, path: str) -> Assembly: - return self.get_assembly(path) diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..69fa449 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +_build/ diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..385629c --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,23 @@ +import sys +from pathlib import Path + +project = "clr-loader" +copyright = "2022, Benedikt Reinartz" +author = "Benedikt Reinartz" + +extensions = ["sphinx.ext.autodoc"] + +# Add parent to path for autodoc +sys.path.append(str(Path("..").absolute())) + +# autodoc_typehints = "both" + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +html_theme = "furo" +html_static_path = [] diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..f7331e3 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,36 @@ +.. clr-loader documentation master file, created by + sphinx-quickstart on Fri Sep 16 17:57:02 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to clr-loader's documentation! +====================================== + +`clr_loader` provides a unified way to load one of the CLR (.NET) runtime +implementations (.NET Framework, .NET (Core) or Mono), load assemblies, and call +very simple functions. + +The only supported signature is + +.. code-block:: csharp + + public static int Function(IntPtr buffer, int size) + +A function like this can be called from Python with a single ``bytes`` +parameter. If more functionality is required, please consider using `Python.NET +`_ instead. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + usage + reference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/doc/reference.rst b/doc/reference.rst new file mode 100644 index 0000000..d1922a6 --- /dev/null +++ b/doc/reference.rst @@ -0,0 +1,35 @@ +.. _reference: + +Reference +========= + +Factory functions +----------------- + +.. py:module:: clr_loader + +.. autofunction:: get_mono +.. autofunction:: get_coreclr +.. autofunction:: get_netfx + +Wrapper types +------------- + +.. autoclass:: Runtime + :members: + +.. autoclass:: Assembly + :members: + +Utilities +--------- + +.. autoclass:: RuntimeInfo + :members: + +.. autoclass:: DotnetCoreRuntimeSpec + :members: + +.. autofunction:: find_dotnet_root +.. autofunction:: find_libmono +.. autofunction:: find_runtimes diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..97147a4 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,5 @@ +sphinx + +# Theme, force pygments update +furo>=2022.9.15 +pygments>=2.7 diff --git a/doc/usage.rst b/doc/usage.rst new file mode 100644 index 0000000..23b61e5 --- /dev/null +++ b/doc/usage.rst @@ -0,0 +1,101 @@ +Usage +===== + +.. py:currentmodule:: clr_loader + +Getting a runtime +----------------- + +To get a :py:class:`Runtime` instance, one of the ``get_*`` functions has to be +called. There are currently the factory functions :py:func:`get_mono`, +:py:func:`get_coreclr` and :py:func:`get_netfx`. All of these provide various +configuration options that are documented in the :ref:`Reference `. +They also provide reasonable defaults and can be called without parameters if +the respective runtime is installed globally: + +.. code-block:: python + + from clr_loader import get_coreclr + runtime = get_coreclr() + +After this, the runtime will usually already be initialized. The initialization +is delayed for .NET Core to allow adjusting the runtime properties beforehand. + +Information on the runtime, its version and parameters can be retrieved using +``runtime.info()`` (see :py:func:`Runtime.info`). + +Getting a callable function +--------------------------- + +A wrapped assembly can be retrieved from the runtime by calling +:py:func:`Runtime.get_assembly` with the path. + +The following example class is provided in the repository: + +.. code-block:: csharp + + using System.Text; + using System.Runtime.InteropServices; + using System; + + namespace Example + { + public class TestClass + { + public static int Test(IntPtr arg, int size) { + var buf = new byte[size]; + Marshal.Copy(arg, buf, 0, size); + var bufAsString = Encoding.UTF8.GetString(buf); + var result = bufAsString.Length; + Console.WriteLine($"Called {nameof(Test)} in {nameof(TestClass)} with {bufAsString}, returning {result}"); + Console.WriteLine($"Binary data: {Convert.ToBase64String(buf)}"); + + return result; + } + } + } + +Assuming it has been compiled to ``out/example.dll``, it can now be loaded using +:py:func:`Runtime.get_assembly`: + +.. code-block:: python + + assembly = runtime.get_assembly("path/to/assembly.dll") + +.. note:: + This does *not* guarantee that the DLL is already loaded and will not + necessarily trigger an error if that is not possible. Actually resolving the + DLL only happens (for all implementations but Mono) when retrieving the + concrete function. + +The ``assembly`` instance can now be used to get a wrapper instance of the +``Test`` function in Python. The given parameters are the fully qualified class +name and the function name. Alternatively, a single parameter can be provided, +and we assume that the last "component" is the function name. These are +equivalent: + +.. code-block:: python + + function = assembly.get_function("Example.TestClass", "Test") + function = assembly.get_function("Example.TestClass.Test") + +This function can now be called with a Python ``binary`` like this: + +.. code-block:: python + + result = function(b"testy mctestface") + +The ``IntPtr`` parameter in C# will now point directly at the ``binary`` buffer, +the ``int`` parameter will contain the size. The given call will thus result in +the output: + +.. code-block:: output + + Called Test in TestClass with testy mctestface, returning 16 + Binary data: dGVzdHkgbWN0ZXN0ZmFjZQ== + +``result`` will now be ``16``. + +.. warning:: + While the buffer can theoretically also be changed in the .NET function, this + is not tested. diff --git a/example/example.csproj b/example/example.csproj index f30cfb7..fd6d566 100644 --- a/example/example.csproj +++ b/example/example.csproj @@ -1,6 +1,6 @@  - netcoreapp31;netstandard20 + net60;netstandard20 true diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index e8b2767..32b4c01 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -32,8 +32,10 @@ public static IntPtr CreateAppDomain( { var setup = new AppDomainSetup { + ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, ConfigurationFile = configFile }; + Print($"Base: {AppDomain.CurrentDomain.BaseDirectory}"); var domain = AppDomain.CreateDomain(name, null, setup); Print($"Located domain {domain}"); @@ -51,9 +53,9 @@ public static IntPtr CreateAppDomain( [DllExport("pyclr_get_function", CallingConvention.Cdecl)] public static IntPtr GetFunction( IntPtr domain, - [MarshalAs(UnmanagedType.LPStr)] string assemblyPath, - [MarshalAs(UnmanagedType.LPStr)] string typeName, - [MarshalAs(UnmanagedType.LPStr)] string function + [MarshalAs(UnmanagedType.LPUTF8Str)] string assemblyPath, + [MarshalAs(UnmanagedType.LPUTF8Str)] string typeName, + [MarshalAs(UnmanagedType.LPUTF8Str)] string function ) { try diff --git a/pyproject.toml b/pyproject.toml index 9787c3b..2c643e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,63 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools>=75", "setuptools_scm[toml]"] build-backend = "setuptools.build_meta" + +[project] +name = "clr_loader" +description = "Generic pure Python loader for .NET runtimes" +license = {text = "MIT"} +requires-python = ">=3.7" + +readme = "README.md" + +dependencies = [ + "cffi >= 1.13; python_version <= '3.8'", + "cffi >= 1.17; python_version >= '3.8'", +] + +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", +] + +dynamic = ["version"] + +[[project.authors]] +name = "Benedikt Reinartz" +email = "filmor@gmail.com" + +[project.urls] +Sources = "https://github.com/pythonnet/clr-loader" +Documentation = "https://pythonnet.github.io/clr-loader/" + +[dependency-groups] +dev = [ + "pytest" +] + +[tool.setuptools] +zip-safe = false +package-data = {"clr_loader.ffi" = ["dlls/x86/*.dll", "dlls/amd64/*.dll"]} +license-files = [] + +[tool.setuptools.packages.find] +include = ["clr_loader*"] + +[tool.setuptools_scm] + +[tool.pytest.ini_options] +xfail_strict = true +testpaths = [ + "tests" +] + +[tool.mypy] +allow-redefinition = true + +[tool.pyright] +pythonPlatform = "All" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 93a1104..0000000 --- a/setup.cfg +++ /dev/null @@ -1,10 +0,0 @@ -[metadata] -license_file = LICENSE - -[flake8] -# Recommend matching the black line length (default 88), -# rather than using the flake8 default of 79: -max-line-length = 88 -extend-ignore = - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, \ No newline at end of file diff --git a/setup.py b/setup.py index f56465c..8d613b4 100755 --- a/setup.py +++ b/setup.py @@ -1,33 +1,64 @@ #!/usr/bin/env python -from setuptools import setup, find_packages, Command, Extension -from wheel.bdist_wheel import bdist_wheel +import distutils +from distutils.command.build import build as _build +from setuptools.command.develop import develop as _develop +from wheel.bdist_wheel import bdist_wheel as _bdist_wheel +from setuptools import Distribution +from setuptools import setup, Command +import os -class DotnetLib(Extension): +# Disable SourceLink during the build until it can read repo-format v1, #1613 +os.environ["EnableSourceControlManagerQueries"] = "false" + + +class DotnetLib: def __init__(self, name, path, **kwargs): + self.name = name self.path = path self.args = kwargs - super().__init__(name, sources=[]) -class BuildDotnet(Command): +class build_dotnet(Command): """Build command for dotnet-cli based builds""" description = "Build DLLs with dotnet-cli" - user_options = [("dotnet-config", None, "dotnet build configuration")] + user_options = [ + ("dotnet-config=", None, "dotnet build configuration"), + ( + "inplace", + "i", + "ignore build-lib and put compiled extensions into the source " + + "directory alongside your pure Python modules", + ), + ] def initialize_options(self): - self.dotnet_config = "release" + self.dotnet_config = None + self.build_lib = None + self.inplace = False def finalize_options(self): - pass + if self.dotnet_config is None: + self.dotnet_config = "release" - def get_source_files(self): - return [] + build = self.distribution.get_command_obj("build") + build.ensure_finalized() + if self.inplace: + self.build_lib = "." + else: + self.build_lib = build.build_lib def run(self): - for lib in self.distribution.ext_modules: + dotnet_modules = self.distribution.dotnet_libs + + for lib in dotnet_modules: + output = os.path.join( + os.path.abspath(self.build_lib), lib.args.pop("output") + ) + rename = lib.args.pop("rename", {}) + opts = sum( [ ["--" + name.replace("_", "-"), value] @@ -36,13 +67,46 @@ def run(self): [], ) - opts.append("--configuration") - opts.append(self.dotnet_config) + opts.extend(["--configuration", self.dotnet_config]) + opts.extend(["--output", output]) + self.announce("Running dotnet build...", level=distutils.log.INFO) self.spawn(["dotnet", "build", lib.path] + opts) + for k, v in rename.items(): + source = os.path.join(output, k) + dest = os.path.join(output, v) + + if os.path.isfile(source): + try: + os.remove(dest) + except OSError: + pass + + self.move_file(src=source, dst=dest, level=distutils.log.INFO) + else: + self.warn( + "Can't find file to rename: {}, current dir: {}".format( + source, os.getcwd() + ) + ) -class bdist_wheel_patched(bdist_wheel): + +# Add build_dotnet to the build tasks: +class build(_build): + sub_commands = _build.sub_commands + [("build_dotnet", None)] + + +class develop(_develop): + def install_for_development(self): + # Build extensions in-place + self.reinitialize_command("build_dotnet", inplace=1) + self.run_command("build_dotnet") + + return super().install_for_development() + + +class bdist_wheel(_bdist_wheel): def finalize_options(self): # Monkey patch bdist_wheel to think the package is pure even though we # include DLLs @@ -50,44 +114,32 @@ def finalize_options(self): self.root_is_pure = True -with open("README.md", "r") as f: - long_description = f.read() +# Monkey-patch Distribution s.t. it supports the dotnet_libs attribute +Distribution.dotnet_libs = None + +cmdclass = { + "build": build, + "build_dotnet": build_dotnet, + "develop": develop, + "bdist_wheel": bdist_wheel, +} + +dotnet_libs = [ + DotnetLib( + "netfx-loader-x86", + "netfx_loader/ClrLoader.csproj", + runtime="win-x86", + output="clr_loader/ffi/dlls/x86", + ), + DotnetLib( + "netfx-loader-amd64", + "netfx_loader/ClrLoader.csproj", + runtime="win-x64", + output="clr_loader/ffi/dlls/amd64", + ), +] setup( - name="clr_loader", - version="0.1.6", - description="Generic pure Python loader for .NET runtimes", - author="Benedikt Reinartz", - author_email="filmor@gmail.com", - long_description=long_description, - long_description_content_type="text/markdown", - license="MIT", - python_requires=">=3.6", - install_requires=["cffi>=1.13"], - classifiers=[ - "Development Status :: 2 - Pre-Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS :: MacOS X", - ], - package_data={"clr_loader.ffi": ["dlls/x86/*.dll", "dlls/amd64/*.dll"]}, - packages=find_packages(), - cmdclass={"build_ext": BuildDotnet, "bdist_wheel": bdist_wheel_patched}, - ext_modules={ - DotnetLib( - "netfx-loader-x86", - "netfx_loader/ClrLoader.csproj", - runtime="win-x86", - output="clr_loader/ffi/dlls/x86", - ), - DotnetLib( - "netfx-loader-amd64", - "netfx_loader/ClrLoader.csproj", - runtime="win-x64", - output="clr_loader/ffi/dlls/amd64", - ), - }, + cmdclass=cmdclass, + dotnet_libs=dotnet_libs, ) diff --git a/tests/test_common.py b/tests/test_common.py index 0a6e177..139f192 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,7 +1,8 @@ +import shutil import pytest from subprocess import check_call -import os import sys +from pathlib import Path @pytest.fixture(scope="session") @@ -11,32 +12,87 @@ def example_netstandard(tmpdir_factory): @pytest.fixture(scope="session") def example_netcore(tmpdir_factory): - return build_example(tmpdir_factory, "netcoreapp31") + return build_example(tmpdir_factory, "net60") def build_example(tmpdir_factory, framework): - out = str(tmpdir_factory.mktemp(f"example-{framework}")) - proj_path = os.path.join(os.path.dirname(__file__), "../example") + out = Path(tmpdir_factory.mktemp(f"example-{framework}")) + proj_path = Path(__file__).parent.parent / "example" / "example.csproj" - check_call(["dotnet", "build", proj_path, "-o", out, "-f", framework]) + check_call(["dotnet", "build", str(proj_path), "-o", str(out), "-f", framework]) return out -def test_mono(example_netstandard): +def test_mono(example_netstandard: Path): from clr_loader import get_mono mono = get_mono() - asm = mono.get_assembly(os.path.join(example_netstandard, "example.dll")) + asm = mono.get_assembly(example_netstandard / "example.dll") run_tests(asm) -def test_coreclr(example_netcore): +def test_mono_debug(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono( + debug=True, + jit_options=[ + "--debugger-agent=address=0.0.0.0:5831,transport=dt_socket,server=y" + ], + ) + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +def test_mono_signal_chaining(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(set_signal_chaining=True) + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +def test_mono_set_dir(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(assembly_dir="/usr/lib", config_dir="/etc") + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +def test_coreclr(example_netcore: Path): + from clr_loader import get_coreclr + + coreclr = get_coreclr(runtime_config=example_netcore / "example.runtimeconfig.json") + asm = coreclr.get_assembly(example_netcore / "example.dll") + + run_tests(asm) + + +def test_coreclr_properties(example_netcore: Path): + run_in_subprocess( + _do_test_coreclr_autogenerated_runtimeconfig, + example_netstandard, + properties=dict(APP_CONTEXT_BASE_DIRECTORY=str(example_netcore)), + ) + + +def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): + run_in_subprocess(_do_test_coreclr_autogenerated_runtimeconfig, example_netstandard) + + +def _do_test_coreclr_autogenerated_runtimeconfig( + example_netstandard: Path, **properties +): from clr_loader import get_coreclr - coreclr = get_coreclr(os.path.join(example_netcore, "example.runtimeconfig.json")) - asm = coreclr.get_assembly(os.path.join(example_netcore, "example.dll")) + coreclr = get_coreclr(properties=properties) + asm = coreclr.get_assembly(example_netstandard / "example.dll") run_tests(asm) @@ -44,11 +100,32 @@ def test_coreclr(example_netcore): @pytest.mark.skipif( sys.platform != "win32", reason=".NET Framework only exists on Windows" ) -def test_netfx(example_netstandard): +def test_netfx(example_netstandard: Path): + run_in_subprocess(_do_test_netfx, example_netstandard) + + +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_chinese_path(example_netstandard: Path, tmpdir_factory): + tmp_path = Path(tmpdir_factory.mktemp("example-中国")) + shutil.copytree(example_netstandard, tmp_path, dirs_exist_ok=True) + + run_in_subprocess(_do_test_netfx, tmp_path) + + +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_separate_domain(example_netstandard): + run_in_subprocess(_do_test_netfx, example_netstandard, domain="some domain") + + +def _do_test_netfx(example_netstandard, **kwargs): from clr_loader import get_netfx - netfx = get_netfx() - asm = netfx.get_assembly(os.path.join(example_netstandard, "example.dll")) + netfx = get_netfx(**kwargs) + asm = netfx.get_assembly(example_netstandard / "example.dll") run_tests(asm) @@ -58,3 +135,12 @@ def run_tests(asm): test_data = b"testy mctestface" res = func(test_data) assert res == len(test_data) + + +def run_in_subprocess(func, *args, **kwargs): + from multiprocessing import get_context + + p = get_context("spawn").Process(target=func, args=args, kwargs=kwargs) + p.start() + p.join() + p.close() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..60809e6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,174 @@ +version = 1 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version < '3.8'", + "python_full_version == '3.8.*'", + "python_full_version >= '3.9'", +] + +[[package]] +name = "cffi" +version = "1.15.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/a8/050ab4f0c3d4c1b8aaa805f70e26e84d0e27004907c5b8ecc1d31815f92a/cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", size = 508501 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/ff/c4b7a358526f231efa46a375c959506c87622fb4a2c5726e827c55e6adf2/cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21", size = 179233 }, + { url = "https://files.pythonhosted.org/packages/ea/be/c4ad40ad441ac847b67c7a37284ae3c58f39f3e638c6b0f85fb662233825/cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185", size = 174280 }, + { url = "https://files.pythonhosted.org/packages/ed/a3/c5f01988ddb70a187c3e6112152e01696188c9f8a4fa4c68aa330adbb179/cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd", size = 421712 }, + { url = "https://files.pythonhosted.org/packages/ef/41/19da352d341963d29a33bdb28433ba94c05672fb16155f794fad3fd907b0/cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc", size = 449886 }, + { url = "https://files.pythonhosted.org/packages/af/da/9441d56d7dd19d07dcc40a2a5031a1f51c82a27cee3705edf53dadcac398/cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f", size = 450520 }, + { url = "https://files.pythonhosted.org/packages/aa/02/ab15b3aa572759df752491d5fa0f74128cd14e002e8e3257c1ab1587810b/cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e", size = 446015 }, + { url = "https://files.pythonhosted.org/packages/88/89/c34caf63029fb7628ec2ebd5c88ae0c9bd17db98c812e4065a4d020ca41f/cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4", size = 441830 }, + { url = "https://files.pythonhosted.org/packages/32/bd/d0809593f7976828f06a492716fbcbbfb62798bbf60ea1f65200b8d49901/cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01", size = 434743 }, + { url = "https://files.pythonhosted.org/packages/0e/65/0d7b5dad821ced4dcd43f96a362905a68ce71e6b5f5cfd2fada867840582/cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e", size = 464113 }, + { url = "https://files.pythonhosted.org/packages/9f/52/1e2b43cfdd7d9a39f48bc89fcaee8d8685b1295e205a4f1044909ac14d89/cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2", size = 170412 }, + { url = "https://files.pythonhosted.org/packages/0e/e2/a23af3d81838c577571da4ff01b799b0c2bbde24bd924d97e228febae810/cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d", size = 179060 }, + { url = "https://files.pythonhosted.org/packages/23/8b/2e8c2469eaf89f7273ac685164949a7e644cdfe5daf1c036564208c3d26b/cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", size = 179198 }, + { url = "https://files.pythonhosted.org/packages/f9/96/fc9e118c47b7adc45a0676f413b4a47554e5f3b6c99b8607ec9726466ef1/cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", size = 174221 }, + { url = "https://files.pythonhosted.org/packages/10/72/617ee266192223a38b67149c830bd9376b69cf3551e1477abc72ff23ef8e/cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", size = 441694 }, + { url = "https://files.pythonhosted.org/packages/91/bc/b7723c2fe7a22eee71d7edf2102cd43423d5f95ff3932ebaa2f82c7ec8d0/cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", size = 470613 }, + { url = "https://files.pythonhosted.org/packages/5d/4e/4e0bb5579b01fdbfd4388bd1eb9394a989e1336203a4b7f700d887b233c1/cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", size = 472199 }, + { url = "https://files.pythonhosted.org/packages/37/5a/c37631a86be838bdd84cc0259130942bf7e6e32f70f4cab95f479847fb91/cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", size = 462588 }, + { url = "https://files.pythonhosted.org/packages/71/d7/0fe0d91b0bbf610fb7254bb164fa8931596e660d62e90fb6289b7ee27b09/cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", size = 450543 }, + { url = "https://files.pythonhosted.org/packages/d3/56/3e94aa719ae96eeda8b68b3ec6e347e0a23168c6841dc276ccdcdadc9f32/cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", size = 474253 }, + { url = "https://files.pythonhosted.org/packages/87/ee/ddc23981fc0f5e7b5356e98884226bcb899f95ebaefc3e8e8b8742dd7e22/cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", size = 170313 }, + { url = "https://files.pythonhosted.org/packages/43/a0/cc7370ef72b6ee586369bacd3961089ab3d94ae712febf07a244f1448ffd/cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", size = 179001 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/df6c088ef30e78a78b0c9cca6b904d5abb698afb5bc8f5191d529d83d667/cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375", size = 178906 }, + { url = "https://files.pythonhosted.org/packages/c2/0b/3b09a755ddb977c167e6d209a7536f6ade43bb0654bad42e08df1406b8e4/cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e", size = 405992 }, + { url = "https://files.pythonhosted.org/packages/5b/1a/e1ee5bed11d8b6540c05a8e3c32448832d775364d4461dd6497374533401/cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82", size = 435560 }, + { url = "https://files.pythonhosted.org/packages/d3/e1/e55ca2e0dd446caa2cc8f73c2b98879c04a1f4064ac529e1836683ca58b8/cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b", size = 435478 }, + { url = "https://files.pythonhosted.org/packages/2e/7a/68c35c151e5b7a12650ecc12fdfb85211aa1da43e9924598451c4a0a3839/cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c", size = 430395 }, + { url = "https://files.pythonhosted.org/packages/93/d0/2e2b27ea2f69b0ec9e481647822f8f77f5fc23faca2dd00d1ff009940eb7/cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426", size = 427911 }, + { url = "https://files.pythonhosted.org/packages/50/34/4cc590ad600869502c9838b4824982c122179089ed6791a8b1c95f0ff55e/cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9", size = 169721 }, + { url = "https://files.pythonhosted.org/packages/32/2a/63cb8c07d151de92ff9d897b2eb27ba6a0e78dda8e4c5f70d7b8c16cd6a2/cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045", size = 179301 }, + { url = "https://files.pythonhosted.org/packages/87/4b/64e8bd9d15d6b22b6cb11997094fbe61edf453ea0a97c8675cb7d1c3f06f/cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3", size = 178940 }, + { url = "https://files.pythonhosted.org/packages/22/c6/df826563f55f7e9dd9a1d3617866282afa969fe0d57decffa1911f416ed8/cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a", size = 421947 }, + { url = "https://files.pythonhosted.org/packages/c1/25/16a082701378170559bb1d0e9ef2d293cece8dc62913d79351beb34c5ddf/cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5", size = 449906 }, + { url = "https://files.pythonhosted.org/packages/df/02/aef53d4aa43154b829e9707c8c60bab413cd21819c4a36b0d7aaa83e2a61/cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca", size = 451028 }, + { url = "https://files.pythonhosted.org/packages/79/4b/33494eb0adbcd884656c48f6db0c98ad8a5c678fb8fb5ed41ab546b04d8c/cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02", size = 446520 }, + { url = "https://files.pythonhosted.org/packages/b7/8b/06f30caa03b5b3ac006de4f93478dbd0239e2a16566d81a106c322dc4f79/cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192", size = 442655 }, + { url = "https://files.pythonhosted.org/packages/47/97/137f0e3d2304df2060abb872a5830af809d7559a5a4b6a295afb02728e65/cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314", size = 170236 }, + { url = "https://files.pythonhosted.org/packages/c9/e3/0a52838832408cfbbf3a59cb19bcd17e64eb33795c9710ca7d29ae10b5b7/cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5", size = 178835 }, + { url = "https://files.pythonhosted.org/packages/18/8f/5ff70c7458d61fa8a9752e5ee9c9984c601b0060aae0c619316a1e1f1ee5/cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585", size = 179249 }, + { url = "https://files.pythonhosted.org/packages/3a/75/a162315adeaf47e94a3b7f886a8e31d77b9e525a387eef2d6f0efc96a7c8/cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0", size = 174297 }, + { url = "https://files.pythonhosted.org/packages/85/1f/a3c533f8d377da5ca7edb4f580cc3edc1edbebc45fac8bb3ae60f1176629/cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415", size = 420641 }, + { url = "https://files.pythonhosted.org/packages/77/b7/d3618d612be01e184033eab90006f8ca5b5edafd17bf247439ea4e167d8a/cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d", size = 448814 }, + { url = "https://files.pythonhosted.org/packages/a9/ba/e082df21ebaa9cb29f2c4e1d7e49a29b90fcd667d43632c6674a16d65382/cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984", size = 449647 }, + { url = "https://files.pythonhosted.org/packages/af/cb/53b7bba75a18372d57113ba934b27d0734206c283c1dfcc172347fbd9f76/cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35", size = 445191 }, + { url = "https://files.pythonhosted.org/packages/2d/86/3ca57cddfa0419f6a95d1c8478f8f622ba597e3581fd501bbb915b20eb75/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27", size = 441236 }, + { url = "https://files.pythonhosted.org/packages/ad/26/7b3a73ab7d82a64664c7c4ea470e4ec4a3c73bb4f02575c543a41e272de5/cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76", size = 433865 }, + { url = "https://files.pythonhosted.org/packages/da/ff/ab939e2c7b3f40d851c0f7192c876f1910f3442080c9c846532993ec3cef/cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3", size = 463090 }, + { url = "https://files.pythonhosted.org/packages/c6/3d/dd085bb831b22ce4d0b7ba8550e6d78960f02f770bbd1314fea3580727f8/cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee", size = 170382 }, + { url = "https://files.pythonhosted.org/packages/a8/16/06b84a7063a4c0a2b081030fdd976022086da9c14e80a9ed4ba0183a98a9/cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c", size = 179079 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version >= '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/48/08/15bf6b43ae9bd06f6b00ad8a91f5a8fe1069d4c9fab550a866755402724e/cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", size = 182457 }, + { url = "https://files.pythonhosted.org/packages/c2/5b/f1523dd545f92f7df468e5f653ffa4df30ac222f3c884e51e139878f1cb5/cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", size = 425932 }, + { url = "https://files.pythonhosted.org/packages/53/93/7e547ab4105969cc8c93b38a667b82a835dd2cc78f3a7dad6130cfd41e1d/cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", size = 448585 }, + { url = "https://files.pythonhosted.org/packages/56/c4/a308f2c332006206bb511de219efeff090e9d63529ba0a77aae72e82248b/cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", size = 456268 }, + { url = "https://files.pythonhosted.org/packages/ca/5b/b63681518265f2f4060d2b60755c1c77ec89e5e045fc3773b72735ddaad5/cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", size = 436592 }, + { url = "https://files.pythonhosted.org/packages/bb/19/b51af9f4a4faa4a8ac5a0e5d5c2522dcd9703d07fac69da34a36c4d960d3/cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/e2/63/2bed8323890cb613bbecda807688a31ed11a7fe7afe31f8faaae0206a9a3/cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", size = 171576 }, + { url = "https://files.pythonhosted.org/packages/2f/70/80c33b044ebc79527447fd4fbc5455d514c3bb840dede4455de97da39b4d/cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", size = 181229 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "clr-loader" +version = "0.2.7.dev2+g3e2dff0.d20241212" +source = { editable = "." } +dependencies = [ + { name = "cffi", version = "1.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8'" }, +] + +[package.metadata] +requires-dist = [ + { name = "cffi", marker = "python_full_version >= '3.8'", specifier = ">=1.17" }, + { name = "cffi", marker = "python_full_version < '3.9'", specifier = ">=1.13" }, +] + +[[package]] +name = "pycparser" +version = "2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206", size = 170877 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", size = 118697 }, +]