From c8ca462bdbcff85d32420fcaadfe20cacefdde17 Mon Sep 17 00:00:00 2001 From: Vince <87706793+DareDevilDenis@users.noreply.github.com> Date: Wed, 10 Nov 2021 19:17:54 +0000 Subject: [PATCH 01/56] Fix issue 14 for 32 bit Windows (#15) Co-authored-by: LUFF,VINCE (K-UnitedKingdom,ex1) --- clr_loader/util/find.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index f33b0c2..a38600e 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -11,11 +11,7 @@ def find_dotnet_root() -> str: 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") - else: - prog_files = os.environ.get("ProgramFiles(x86)") - + prog_files = os.environ.get("ProgramFiles") dotnet_root = os.path.join(prog_files, "dotnet") if os.path.isdir(dotnet_root): return dotnet_root From 699c6d640e41404bb1cb008f811809aa18b3fb6c Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 16 Nov 2021 13:58:01 -0800 Subject: [PATCH 02/56] Allow mono debugging and jit flags (#16) --- .vscode/launch.json | 12 ++++++++++++ clr_loader/__init__.py | 6 +++++- clr_loader/ffi/mono.py | 11 +++++++++++ clr_loader/mono.py | 15 ++++++++++++++- tests/test_common.py | 14 ++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 .vscode/launch.json 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/clr_loader/__init__.py b/clr_loader/__init__.py index aa4c7a9..779b514 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, Optional, Sequence from .wrappers import Runtime from .util.find import find_libmono, find_dotnet_root @@ -12,6 +12,8 @@ def get_mono( global_config_file: Optional[str] = None, libmono: Optional[str] = None, sgen: bool = True, + debug: bool = False, + jit_options: Optional[Sequence[str]] = None, ) -> Runtime: from .mono import Mono @@ -20,6 +22,8 @@ def get_mono( impl = Mono( domain=domain, + debug=debug, + jit_options=jit_options, config_file=config_file, global_config_file=global_config_file, libmono=libmono, diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index 427360c..25a855a 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -11,8 +11,19 @@ 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; + 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); diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 10e422f..e4fcb44 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -1,5 +1,5 @@ import atexit -from typing import Optional +from typing import Optional, Sequence from .ffi import load_mono, ffi @@ -17,6 +17,8 @@ def __init__( libmono, *, domain=None, + debug=False, + jit_options: Optional[Sequence[str]] = None, config_file: Optional[str] = None, global_config_file: Optional[str] = None, ): @@ -24,6 +26,8 @@ def __init__( initialize( config_file=config_file, + debug=debug, + jit_options=jit_options, global_config_file=global_config_file, libmono=libmono, ) @@ -95,6 +99,8 @@ def __call__(self, ptr, size): def initialize( libmono: str, + debug: bool = False, + jit_options: Optional[Sequence[str]] = None, config_file: Optional[str] = None, global_config_file: Optional[str] = None, ) -> None: @@ -113,6 +119,13 @@ 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) + + 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") diff --git a/tests/test_common.py b/tests/test_common.py index 0a6e177..04fec64 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -32,6 +32,20 @@ def test_mono(example_netstandard): run_tests(asm) +def test_mono_debug(example_netstandard): + 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(os.path.join(example_netstandard, "example.dll")) + + run_tests(asm) + + def test_coreclr(example_netcore): from clr_loader import get_coreclr From c282eadea8c118d7853231d5c75f972f4aabc62f Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 19 Nov 2021 04:15:58 -0800 Subject: [PATCH 03/56] Bump version to 0.1.7 (#17) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f56465c..efd98bc 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def finalize_options(self): setup( name="clr_loader", - version="0.1.6", + version="0.1.7", description="Generic pure Python loader for .NET runtimes", author="Benedikt Reinartz", author_email="filmor@gmail.com", From f6761de7bebced7dbf09a45d48523caa7e3dd001 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 13 Dec 2021 18:29:39 +0100 Subject: [PATCH 04/56] Actually make dotnet-config configurable --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index efd98bc..4723ddf 100755 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ class BuildDotnet(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")] def initialize_options(self): self.dotnet_config = "release" From 8293122c8c06a4fa41806509d199ccdec1404ee9 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 22 Dec 2021 10:12:48 +0100 Subject: [PATCH 05/56] Drop pypy3 from CI for now The setup-python step seems to be failing, need to investigate this --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7627b1e..d7ddff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.9', '3.8', '3.7', '3.6', pypy3] + python: ['3.9', '3.8', '3.7', '3.6'] # pypy3 steps: - uses: actions/checkout@v2 From 5b18691ce05e256e7231998d94b07088f4e987f4 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 8 Jan 2022 23:04:28 -0800 Subject: [PATCH 06/56] warn if Mono version is <= 6.12 (#21) --- clr_loader/ffi/mono.py | 2 ++ clr_loader/mono.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index 25a855a..ed48cff 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -18,6 +18,8 @@ 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[]); diff --git a/clr_loader/mono.py b/clr_loader/mono.py index e4fcb44..26c6696 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -1,4 +1,5 @@ import atexit +import re from typing import Optional, Sequence from .ffi import load_mono, ffi @@ -122,6 +123,8 @@ def initialize( 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 debug: _MONO.mono_debug_init(_MONO.MONO_DEBUG_FORMAT_MONO) @@ -129,6 +132,20 @@ def initialize( _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") + + 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) From bead884aae472716b35e7d1b8688501291a10fdc Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 12 Jan 2022 07:13:18 -0800 Subject: [PATCH 07/56] ARM64 CI (#22) --- .github/workflows/ci-arm.yml | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/ci-arm.yml diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml new file mode 100644 index 0000000..0c308bd --- /dev/null +++ b/.github/workflows/ci-arm.yml @@ -0,0 +1,42 @@ +# 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: ARM64 Tests + +on: [push, 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 From d36d5483a1e2f8545b7ba9a3b7d51cfc5950e854 Mon Sep 17 00:00:00 2001 From: Marco Rossi Date: Wed, 12 Jan 2022 16:13:46 +0100 Subject: [PATCH 08/56] Add conda-forge badge (#23) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b8c47d5..575b7cb 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![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. From 6d6f7a1047f447aafca2b2569ac8087839c9ba5f Mon Sep 17 00:00:00 2001 From: Vince <87706793+DareDevilDenis@users.noreply.github.com> Date: Tue, 1 Mar 2022 11:17:18 +0000 Subject: [PATCH 09/56] Fix issue 24: macOS: dotnet from fixed path when DOTNET_ROOT not set (#25) * Fix issue 24: Find dotnet on macos from fixed location when DOTNET_ROOT is not defined Co-authored-by: LUFF,VINCE (K-UnitedKingdom,ex1) --- clr_loader/util/find.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index a38600e..83b1bb1 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -13,8 +13,11 @@ def find_dotnet_root() -> str: # On Windows, the host library is stored separately from dotnet.exe for x86 prog_files = os.environ.get("ProgramFiles") dotnet_root = os.path.join(prog_files, "dotnet") - if os.path.isdir(dotnet_root): - return dotnet_root + elif sys.platform == "darwin": + dotnet_root = "/usr/local/share/dotnet" + + if dotnet_root is not None and os.path.isdir(dotnet_root): + return dotnet_root # Try to discover dotnet from PATH otherwise dotnet_path = shutil.which("dotnet") From 1efebe4dc328f399ca3eb28f05f15171d8fef9ba Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 8 Jul 2022 01:28:02 +0200 Subject: [PATCH 10/56] Move setuptools config to pyproject.toml - Drop flake8 config - Reformat with black - Use .NET 6.0 for the example/test project --- LICENSE | 2 +- clr_loader/ffi/__init__.py | 2 +- clr_loader/hostfxr.py | 1 - clr_loader/mono.py | 19 ++++++++++------- clr_loader/netfx.py | 2 +- clr_loader/util/find.py | 4 ++-- example/example.csproj | 2 +- pyproject.toml | 42 +++++++++++++++++++++++++++++++++++++- setup.cfg | 10 --------- setup.py | 26 +---------------------- tests/test_common.py | 2 +- 11 files changed, 61 insertions(+), 51 deletions(-) delete mode 100644 setup.cfg 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/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 114803c..285fe6d 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -41,7 +41,7 @@ def load_netfx(): raise RuntimeError(".NET Framework is only supported on Windows") dirname = os.path.join(os.path.dirname(__file__), "dlls") - if sys.maxsize > 2 ** 32: + if sys.maxsize > 2**32: arch = "amd64" else: arch = "x86" diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index fed2f77..51ca26a 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -125,7 +125,6 @@ def encode(string): def decode(char_ptr): return ffi.string(char_ptr) - else: def encode(string): diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 26c6696..d9279af 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -1,6 +1,6 @@ import atexit import re -from typing import Optional, Sequence +from typing import Optional, Sequence, Dict, Any from .ffi import load_mono, ffi @@ -23,7 +23,7 @@ def __init__( config_file: Optional[str] = None, global_config_file: Optional[str] = None, ): - self._assemblies = {} + self._assemblies: Dict[str, Any] = {} initialize( config_file=config_file, @@ -135,16 +135,21 @@ def initialize( 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_str = ffi.string(build).decode("utf8") # e.g. '6.12.0.122 (tarball)' - ver = re.match(r'^(?P\d+)\.(?P\d+)\.[\d.]+', ver_str) + 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')) + 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.') + + 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) diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 6b6c003..00323f5 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -2,7 +2,7 @@ from typing import Optional, Any from .ffi import ffi, load_netfx -_FW: Optional[Any] = None +_FW: Any = None class NetFx: diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index 83b1bb1..b090fa4 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -46,13 +46,13 @@ def find_dotnet_root() -> str: def find_libmono(sgen: bool = True) -> str: 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)") # Ignore sgen on Windows, the main installation only contains this DLL - path = fr"{prog_files}\Mono\bin\mono-2.0-sgen.dll" + path = rf"{prog_files}\Mono\bin\mono-2.0-sgen.dll" elif sys.platform == "darwin": path = ( 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/pyproject.toml b/pyproject.toml index 9787c3b..5a66918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,43 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools>=61", "wheel"] build-backend = "setuptools.build_meta" + +[project] +name = "clr_loader" +description = "Generic pure Python loader for .NET runtimes" +license = {text = "MIT"} +version = "0.1.7" + +readme = "README.md" + +dependencies = ["cffi>=1.13"] + +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", +] + +[[project.authors]] +name = "Benedikt Reinartz" +email = "filmor@gmail.com" + +[project.urls] +Sources = "https://github.com/pythonnet/clr-loader" + +[tool.setuptools] +zip-safe = false +package-data = {"clr_loader.ffi" = ["dlls/x86/*.dll", "dlls/amd64/*.dll"]} + +[tool.setuptools.packages.find] +include = ["clr_loader*"] + +[tool.pytest.ini_options] +xfail_strict = true +testpaths = [ + "tests" +] 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 4723ddf..44212b5 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from setuptools import setup, find_packages, Command, Extension +from setuptools import setup, Command, Extension from wheel.bdist_wheel import bdist_wheel @@ -50,31 +50,7 @@ def finalize_options(self): self.root_is_pure = True -with open("README.md", "r") as f: - long_description = f.read() - setup( - name="clr_loader", - version="0.1.7", - 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( diff --git a/tests/test_common.py b/tests/test_common.py index 04fec64..1822b6d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -11,7 +11,7 @@ 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): From 7ac4360a991be6f9e3d3f2944a1822861350aee8 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 12 Jul 2022 19:42:49 +0200 Subject: [PATCH 11/56] Drop support for Python 3.6 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7ddff0..e32ab9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.9', '3.8', '3.7', '3.6'] # pypy3 + python: ['3.10', '3.9', '3.8', '3.7'] # pypy3 steps: - uses: actions/checkout@v2 From f6cc833ca22eb26ec21c6d5b6b63675ef070c156 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 12 Jul 2022 20:21:20 +0200 Subject: [PATCH 12/56] Use .NET 6 for building --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e32ab9c..55f5d65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,11 @@ jobs: steps: - uses: actions/checkout@v2 + - 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 with: From 46143d400b4471e2833d889d94e69b19a90488cd Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 20:30:49 +0200 Subject: [PATCH 13/56] Refactoring to improve API and allow configless .NET Core loading (#31) - Drop `Runtime` wrapper in favour of an abstract base class - Use `pathlib` throughout and allow it as parameter - Expose a `shutdown` method (that is a noop in most cases, but at least it exists) - Add functions to find all installed .NET Core runtimes - If no runtime configuration is given, generate a simple one in a temporary directory --- clr_loader/__init__.py | 80 ++++++++++++++++++++++----- clr_loader/ffi/__init__.py | 30 +++++----- clr_loader/ffi/hostfxr.py | 1 + clr_loader/hostfxr.py | 86 ++++++++++++++++++++--------- clr_loader/mono.py | 89 ++++++++++++++++++------------ clr_loader/netfx.py | 33 ++++++++--- clr_loader/types.py | 97 +++++++++++++++++++++++++++++++++ clr_loader/util/__init__.py | 24 ++++++-- clr_loader/util/find.py | 92 +++++++++++++++++++++---------- clr_loader/util/runtime_spec.py | 31 +++++++++++ clr_loader/wrappers.py | 52 ------------------ pyproject.toml | 6 ++ tests/test_common.py | 24 +++++--- 13 files changed, 457 insertions(+), 188 deletions(-) create mode 100644 clr_loader/types.py create mode 100644 clr_loader/util/runtime_spec.py delete mode 100644 clr_loader/wrappers.py diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 779b514..446c170 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -1,22 +1,38 @@ +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", +] def get_mono( + *, domain: Optional[str] = None, - config_file: Optional[str] = None, - global_config_file: Optional[str] = None, - libmono: 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, ) -> Runtime: from .mono import Mono + libmono = _maybe_path(libmono) if libmono is None: libmono = find_libmono(sgen) @@ -24,33 +40,67 @@ def get_mono( domain=domain, debug=debug, jit_options=jit_options, - config_file=config_file, - global_config_file=global_config_file, + config_file=_maybe_path(config_file), + global_config_file=_maybe_path(global_config_file), libmono=libmono, ) - 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: 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(name: Optional[str] = None, config_file: Optional[str] = None) -> Runtime: + +def get_netfx( + *, name: Optional[str] = None, config_file: Optional[StrOrPath] = None +) -> Runtime: from .netfx import NetFx - impl = NetFx(name=name, config_file=config_file) - return Runtime(impl) + impl = NetFx(name=name, 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 285fe6d..208824f 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -1,6 +1,5 @@ -import glob -import os import sys +from pathlib import Path from typing import Optional import cffi # type: ignore @@ -9,46 +8,51 @@ __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) - 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)): try: - return ffi.dlopen(hostfxr_path) + return ffi.dlopen(str(hostfxr_path)) 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") + 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(path) + return ffi.dlopen(str(path)) 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/hostfxr.py b/clr_loader/hostfxr.py index 51ca26a..9171def 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -1,29 +1,48 @@ -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): + 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._is_initialized = False self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config) self._load_func = _get_load_func(self._dll, self._handle) + 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._is_initialized + + @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 +53,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 +72,26 @@ 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_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 + self._is_initialized = 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), + encode(str(assembly_path)), encode(typename), encode(function), ffi.NULL, @@ -79,27 +101,39 @@ def get_callable(self, assembly_path: str, typename: str, function: str): check_result(res) return ffi.cast("component_entry_point_fn", delegate_ptr[0]) + def _check_initialized(self) -> None: + if self._handle is None: + raise RuntimeError("Runtime is shut down") + elif not self._is_initialized: + raise RuntimeError("Runtime is not initialized") + def shutdown(self) -> None: if self._handle is not 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,16 +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 d9279af..c9123ca 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -1,37 +1,40 @@ import atexit import re -from typing import Optional, Sequence, Dict, Any - -from .ffi import load_mono, ffi +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, - debug=False, + domain: Optional[str] = None, + debug: bool = False, jit_options: Optional[Sequence[str]] = None, - config_file: Optional[str] = None, - global_config_file: Optional[str] = None, + config_file: Optional[Path] = None, + global_config_file: Optional[Path] = None, ): - self._assemblies: Dict[str, Any] = {} + self._assemblies: Dict[Path, Any] = {} - initialize( - config_file=config_file, + self._version = initialize( + config_file=optional_path_as_string(config_file), debug=debug, jit_options=jit_options, - global_config_file=global_config_file, + global_config_file=optional_path_as_string(global_config_file), libmono=libmono, ) + print(self._version) if domain is None: self._domain = _ROOT_DOMAIN @@ -39,10 +42,11 @@ def __init__( raise NotImplementedError 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 @@ -58,6 +62,20 @@ 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): @@ -99,12 +117,12 @@ 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: +) -> str: global _MONO, _ROOT_DOMAIN if _MONO is None: _MONO = load_mono(libmono) @@ -133,28 +151,29 @@ def initialize( _MONO.mono_domain_set_config(_ROOT_DOMAIN, b".", config_encoded) _check_result(_ROOT_DOMAIN, "Failed to initialize Mono") - 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)' + 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")) + 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 + 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." - ) + 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) + 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) @@ -162,6 +181,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 00323f5..9f18a16 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -1,28 +1,45 @@ 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 -class NetFx: - def __init__(self, name: Optional[str] = None, config_file: Optional[str] = None): +class NetFx(Runtime): + def __init__(self, name: Optional[str] = None, config_file: Optional[Path] = 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) + else: + config_file_s = ffi.NULL + + self._name = name + self._config_file = config_file + self._domain = _FW.pyclr_create_appdomain(name or ffi.NULL, config_file_s) + + def info(self) -> RuntimeInfo: + return RuntimeInfo( + kind=".NET Framework", + version="", + initialized=True, + shutdown=_FW is None, + properties={}, ) - 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"), ) 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..85c234d --- /dev/null +++ b/clr_loader/types.py @@ -0,0 +1,97 @@ +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: + 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: + 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): + @abstractmethod + def info(self) -> RuntimeInfo: + pass + + def get_assembly(self, assembly_path: StrOrPath) -> Assembly: + return Assembly(self, assembly_path) + + @abstractmethod + def get_callable( + self, assembly_path: StrOrPath, typename: str, function: str + ) -> Callable[[Any, int], Any]: + pass + + @abstractmethod + def shutdown(self) -> None: + 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 b090fa4..8114df8 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -2,48 +2,82 @@ import os.path import shutil import sys +from pathlib import Path +from typing import Iterator, Optional +from .runtime_spec import DotnetCoreRuntimeSpec -def find_dotnet_root() -> str: + +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: 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 prog_files = os.environ.get("ProgramFiles") - dotnet_root = os.path.join(prog_files, "dotnet") + if not prog_files: + raise RuntimeError("Could not find ProgramFiles") + prog_files = Path(prog_files) + dotnet_root = prog_files / "dotnet" elif sys.platform == "darwin": - dotnet_root = "/usr/local/share/dotnet" + dotnet_root = Path("/usr/local/share/dotnet") - if dotnet_root is not None and os.path.isdir(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) - 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_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) -def find_libmono(sgen: bool = True) -> str: +def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: + 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(sgen: bool = True) -> Path: unix_name = f"mono{'sgen' if sgen else ''}-2.0" if sys.platform == "win32": if sys.maxsize > 2**32: @@ -51,14 +85,16 @@ def find_libmono(sgen: bool = True) -> str: 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 = rf"{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: @@ -69,4 +105,4 @@ def find_libmono(sgen: bool = True) -> str: 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..e874d1e --- /dev/null +++ b/clr_loader/util/runtime_spec.py @@ -0,0 +1,31 @@ +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, TextIO + + +@dataclass +class DotnetCoreRuntimeSpec: + 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/pyproject.toml b/pyproject.toml index 5a66918..002ef1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,3 +41,9 @@ xfail_strict = true testpaths = [ "tests" ] + +[tool.mypy] +allow-redefinition = true + +[tool.pyright] +pythonPlatform = "All" diff --git a/tests/test_common.py b/tests/test_common.py index 1822b6d..1191102 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -2,6 +2,7 @@ from subprocess import check_call import os import sys +from pathlib import Path @pytest.fixture(scope="session") @@ -15,10 +16,10 @@ def example_netcore(tmpdir_factory): 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" - check_call(["dotnet", "build", proj_path, "-o", out, "-f", framework]) + check_call(["dotnet", "build", str(proj_path), "-o", str(out), "-f", framework]) return out @@ -27,7 +28,7 @@ def test_mono(example_netstandard): 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) @@ -41,7 +42,7 @@ def test_mono_debug(example_netstandard): "--debugger-agent=address=0.0.0.0:5831,transport=dt_socket,server=y" ], ) - asm = mono.get_assembly(os.path.join(example_netstandard, "example.dll")) + asm = mono.get_assembly(example_netstandard / "example.dll") run_tests(asm) @@ -49,8 +50,17 @@ def test_mono_debug(example_netstandard): def test_coreclr(example_netcore): 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(runtime_config=example_netcore / "example.runtimeconfig.json") + asm = coreclr.get_assembly(example_netcore / "example.dll") + + run_tests(asm) + + +def test_coreclr_autogenerated_runtimeconfig(example_netstandard): + from clr_loader import get_coreclr + + coreclr = get_coreclr() + asm = coreclr.get_assembly(example_netstandard / "example.dll") run_tests(asm) From b14e7f70df482784cde9a50802a240afe019fe06 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 20:34:39 +0200 Subject: [PATCH 14/56] Add documentation --- clr_loader/__init__.py | 60 ++++++++++++++++++-- clr_loader/hostfxr.py | 2 +- clr_loader/mono.py | 2 +- clr_loader/netfx.py | 14 +++-- clr_loader/types.py | 53 +++++++++++++++++- clr_loader/util/find.py | 37 +++++++++++- clr_loader/util/runtime_spec.py | 2 + doc/.gitignore | 1 + doc/Makefile | 20 +++++++ doc/conf.py | 53 ++++++++++++++++++ doc/index.rst | 36 ++++++++++++ doc/make.bat | 35 ++++++++++++ doc/reference.rst | 35 ++++++++++++ doc/usage.rst | 99 +++++++++++++++++++++++++++++++++ 14 files changed, 434 insertions(+), 15 deletions(-) create mode 100644 doc/.gitignore create mode 100644 doc/Makefile create mode 100644 doc/conf.py create mode 100644 doc/index.rst create mode 100644 doc/make.bat create mode 100644 doc/reference.rst create mode 100644 doc/usage.rst diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 446c170..d0d8ff2 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -17,12 +17,13 @@ "Runtime", "Assembly", "RuntimeInfo", + "DotnetCoreRuntimeSpec", ] def get_mono( *, - domain: Optional[str] = None, + # domain: Optional[str] = None, config_file: Optional[StrOrPath] = None, global_config_file: Optional[StrOrPath] = None, libmono: Optional[StrOrPath] = None, @@ -30,14 +31,32 @@ def get_mono( debug: bool = False, jit_options: Optional[Sequence[str]] = None, ) -> 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`` + """ from .mono import Mono libmono = _maybe_path(libmono) if libmono is None: - libmono = find_libmono(sgen) + libmono = find_libmono(sgen=sgen) impl = Mono( - domain=domain, + # domain=domain, debug=debug, jit_options=jit_options, config_file=_maybe_path(config_file), @@ -54,6 +73,27 @@ def get_coreclr( 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) @@ -91,11 +131,21 @@ def get_coreclr( def get_netfx( - *, name: Optional[str] = None, config_file: Optional[StrOrPath] = None + *, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None ) -> Runtime: + """Get a .NET Framework runtime instance + + :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=_maybe_path(config_file)) + impl = NetFx(domain=domain, config_file=_maybe_path(config_file)) return impl diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index 9171def..dce5c82 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -79,7 +79,7 @@ def __iter__(self) -> Generator[Tuple[str, str], None, None]: for i in range(size_ptr[0]): yield (decode(keys_ptr[i]), decode(values_ptr[i])) - def get_callable(self, assembly_path: StrOrPath, typename: str, function: str): + 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_initialized = True diff --git a/clr_loader/mono.py b/clr_loader/mono.py index c9123ca..3558822 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -41,7 +41,7 @@ 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: diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 9f18a16..f98cd74 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -9,16 +9,18 @@ class NetFx(Runtime): - def __init__(self, name: Optional[str] = None, config_file: Optional[Path] = None): + def __init__( + self, domain: Optional[str] = None, config_file: Optional[Path] = None + ): initialize() if config_file is not None: config_file_s = str(config_file) else: config_file_s = ffi.NULL - self._name = name + self._domain = domain self._config_file = config_file - self._domain = _FW.pyclr_create_appdomain(name or ffi.NULL, config_file_s) + self._domain = _FW.pyclr_create_appdomain(domain or ffi.NULL, config_file_s) def info(self) -> RuntimeInfo: return RuntimeInfo( @@ -26,10 +28,12 @@ def info(self) -> RuntimeInfo: version="", initialized=True, shutdown=_FW is None, - properties={}, + properties=dict( + domain=self._domain or "", config_file=str(self._config_file) + ), ) - def get_callable(self, assembly_path: StrOrPath, typename: str, function: str): + def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): func = _FW.pyclr_get_function( self._domain, str(Path(assembly_path)).encode("utf8"), diff --git a/clr_loader/types.py b/clr_loader/types.py index 85c234d..15c1e30 100644 --- a/clr_loader/types.py +++ b/clr_loader/types.py @@ -10,6 +10,22 @@ @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 @@ -39,7 +55,7 @@ def __init__( self._class = typename self._name = func_name - self._callable = runtime.get_callable(assembly, typename, func_name) + self._callable = runtime._get_callable(assembly, typename, func_name) def __call__(self, buffer: bytes) -> int: from .ffi import ffi @@ -57,6 +73,21 @@ def __init__(self, runtime: "Runtime", path: StrOrPath): 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) @@ -67,21 +98,39 @@ def __repr__(self) -> str: 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( + 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: diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index 8114df8..daad769 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -17,6 +17,19 @@ def find_dotnet_cli() -> Optional[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 + """ + dotnet_root = os.environ.get("DOTNET_ROOT", None) if dotnet_root is not None: return Path(dotnet_root) @@ -69,6 +82,17 @@ def find_runtimes_in_root(dotnet_root: Path) -> Iterator[DotnetCoreRuntimeSpec]: 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. + + 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) @@ -77,7 +101,18 @@ def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: return find_runtimes_in_root(dotnet_root) -def find_libmono(sgen: bool = True) -> Path: +def find_libmono(*, 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: diff --git a/clr_loader/util/runtime_spec.py b/clr_loader/util/runtime_spec.py index e874d1e..2eaeb68 100644 --- a/clr_loader/util/runtime_spec.py +++ b/clr_loader/util/runtime_spec.py @@ -6,6 +6,8 @@ @dataclass class DotnetCoreRuntimeSpec: + """Specification of an installed .NET Core runtime""" + name: str version: str path: 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..5f97ad9 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,53 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = "clr-loader" +copyright = "2022, Benedikt Reinartz" +author = "Benedikt Reinartz" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc"] + +# 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"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] 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/usage.rst b/doc/usage.rst new file mode 100644 index 0000000..2c10f62 --- /dev/null +++ b/doc/usage.rst @@ -0,0 +1,99 @@ +Usage +===== + +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. From 4cbd29f91ec87d5a574734824780959b92c7fed0 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 20:54:53 +0200 Subject: [PATCH 15/56] Build documentation in CI --- .github/workflows/docs.yml | 29 +++++++++++++++++++++++++++++ doc/requirements.txt | 1 + 2 files changed, 30 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 doc/requirements.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..edac6e1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,29 @@ +name: Documentation + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: ammaraskar/sphinx-action@master + with: + docs-folder: doc/ + - 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 + 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/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 0000000..483a4e9 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1 @@ +sphinx_rtd_theme From 65d5bf9148700be11f3f8b356429ec63825a0aec Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 21:05:28 +0200 Subject: [PATCH 16/56] Add write permissions to documentation deploy job --- .github/workflows/docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index edac6e1..ed89bc2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,6 +19,10 @@ jobs: 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 }} From 569e5e3f4eef6c8dd65f3c16d3dfc65a00fc9576 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 21:13:52 +0200 Subject: [PATCH 17/56] Bump version to v0.2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 002ef1c..2b93b3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "clr_loader" description = "Generic pure Python loader for .NET runtimes" license = {text = "MIT"} -version = "0.1.7" +version = "0.2.0" readme = "README.md" From eca7d8b8174611447973ff0de2c91becfd2afc6c Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 23:11:33 +0200 Subject: [PATCH 18/56] Drop accidentally left debug print --- clr_loader/mono.py | 1 - 1 file changed, 1 deletion(-) diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 3558822..fb8ab1b 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -34,7 +34,6 @@ def __init__( global_config_file=optional_path_as_string(global_config_file), libmono=libmono, ) - print(self._version) if domain is None: self._domain = _ROOT_DOMAIN From 8360ed6c70e969ab3930e21f78ac081e22d2758c Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 23:11:53 +0200 Subject: [PATCH 19/56] Update README and bump version --- README.md | 5 ++++- pyproject.toml | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 575b7cb..e9bc6d5 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,7 @@ [![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/pyproject.toml b/pyproject.toml index 2b93b3f..0bf95f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "clr_loader" description = "Generic pure Python loader for .NET runtimes" license = {text = "MIT"} -version = "0.2.0" +version = "0.2.1" readme = "README.md" @@ -28,6 +28,7 @@ email = "filmor@gmail.com" [project.urls] Sources = "https://github.com/pythonnet/clr-loader" +Documentation = "https://pythonnet.github.io/clr-loader/" [tool.setuptools] zip-safe = false From 82c671b95046dd625ae113793f3c4af19e2521e6 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 23:45:26 +0200 Subject: [PATCH 20/56] Fix accidental overwrite of domain field in NetFx --- clr_loader/netfx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index f98cd74..24460ca 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -18,7 +18,7 @@ def __init__( else: config_file_s = ffi.NULL - self._domain = domain + self._domain_name = domain self._config_file = config_file self._domain = _FW.pyclr_create_appdomain(domain or ffi.NULL, config_file_s) @@ -29,7 +29,7 @@ def info(self) -> RuntimeInfo: initialized=True, shutdown=_FW is None, properties=dict( - domain=self._domain or "", config_file=str(self._config_file) + domain=self._domain_name or "", config_file=str(self._config_file) ), ) From c51bf95efe37bcf2aeaed84a40cfd4a2ee68eb5b Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Sep 2022 23:46:10 +0200 Subject: [PATCH 21/56] Bump version to v0.2.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0bf95f0..9e33815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "clr_loader" description = "Generic pure Python loader for .NET runtimes" license = {text = "MIT"} -version = "0.2.1" +version = "0.2.2" readme = "README.md" From 6532799bc04f55e7078a85762d885a66bf7d2c29 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 17 Sep 2022 00:28:50 +0200 Subject: [PATCH 22/56] Use same setup.py as pythonnet --- setup.py | 134 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 29 deletions(-) diff --git a/setup.py b/setup.py index 44212b5..8d613b4 100755 --- a/setup.py +++ b/setup.py @@ -1,33 +1,64 @@ #!/usr/bin/env python -from setuptools import setup, 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,20 +114,32 @@ def finalize_options(self): self.root_is_pure = True +# 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( - 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, ) From 979099a03ef6d4eaaf1d5b127bb1214a268af996 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 17 Sep 2022 00:29:21 +0200 Subject: [PATCH 23/56] Bump version to v0.2.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9e33815..d344968 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "clr_loader" description = "Generic pure Python loader for .NET runtimes" license = {text = "MIT"} -version = "0.2.2" +version = "0.2.3" readme = "README.md" From 435b6b3b7b6d32148fc5c4ce430e715bfdae6ac9 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 17 Sep 2022 09:06:03 +0200 Subject: [PATCH 24/56] Fix links to reference in documentation --- doc/usage.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/usage.rst b/doc/usage.rst index 2c10f62..23b61e5 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -1,6 +1,8 @@ Usage ===== +.. py:currentmodule:: clr_loader + Getting a runtime ----------------- From c1e337592f5bb7c1fced8e27dc65c408a0a8eb78 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 17 Sep 2022 17:01:02 +0200 Subject: [PATCH 25/56] Switch to Furo theme and use custom step (#33) The "published" step uses an ages old version of Sphinx (2.2.4). This should also fix issues with links to referenced functions. --- .github/workflows/docs.yml | 8 +++++--- doc/conf.py | 2 +- doc/requirements.txt | 6 +++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ed89bc2..bce19d4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -7,9 +7,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - uses: ammaraskar/sphinx-action@master - with: - docs-folder: doc/ + - 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 diff --git a/doc/conf.py b/doc/conf.py index 5f97ad9..dac323a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -45,7 +45,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "furo" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/doc/requirements.txt b/doc/requirements.txt index 483a4e9..97147a4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1 +1,5 @@ -sphinx_rtd_theme +sphinx + +# Theme, force pygments update +furo>=2022.9.15 +pygments>=2.7 From 1df0f23d7b5fe557c6082b8006247466db1a7ebb Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 23 Sep 2022 07:34:31 +0200 Subject: [PATCH 26/56] Fix autodoc not loading clr_loader from the repository --- doc/conf.py | 42 +++++------------------------------------- 1 file changed, 5 insertions(+), 37 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index dac323a..3e0f45d 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,34 +1,13 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - project = "clr-loader" copyright = "2022, Benedikt Reinartz" author = "Benedikt Reinartz" - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = ["sphinx.ext.autodoc"] +# Add parent to path for autodoc +import sys, os +sys.path.append(os.path.abspath('..')) + # autodoc_typehints = "both" # Add any paths that contain templates here, relative to this directory. @@ -38,16 +17,5 @@ # 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"] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# html_theme = "furo" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +html_static_path = [] From cba6ebfdcd33eda54a1ae9e9bc81be92a74904bf Mon Sep 17 00:00:00 2001 From: DongGeon Lee Date: Mon, 10 Oct 2022 19:06:10 +0900 Subject: [PATCH 27/56] Fix to find dotnet root on 64-bits darwin system (#37) --- clr_loader/util/find.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index daad769..c163240 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -42,7 +42,10 @@ def find_dotnet_root() -> Path: prog_files = Path(prog_files) dotnet_root = prog_files / "dotnet" elif sys.platform == "darwin": - dotnet_root = Path("/usr/local/share/dotnet") + if sys.maxsize > 2**32: # is_64bits + dotnet_root = Path("/usr/local/share/dotnet/x64") + else: + dotnet_root = Path("/usr/local/share/dotnet") if dotnet_root is not None and dotnet_root.is_dir(): return dotnet_root From 9c24c89ceabc608ed100bf9f884f98d2f7fe36f1 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 12 Oct 2022 20:07:43 +0200 Subject: [PATCH 28/56] Define self._handle up-front to prevent warning in __del__ Since initialization failures happen (usually) in `get_handle`, the instance would not have a proper `self._handle` defined and fail on `__del__` which checks for this field's value. --- clr_loader/hostfxr.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index dce5c82..522fec5 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -13,6 +13,8 @@ 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") From 15ede715c5248164d1a6a6e4f5c921b83ab329b9 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 12 Oct 2022 20:08:30 +0200 Subject: [PATCH 29/56] Allow loading libhostfxr directly from the dotnet_root This is a prerequisite to load self-contained components (which is still not supported upstream, though). --- clr_loader/ffi/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 208824f..035541e 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -16,6 +16,7 @@ def load_hostfxr(dotnet_root: Path): hostfxr_name = _get_dll_name("hostfxr") + dotnet_root = dotnet_root.absolute() # 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 @@ -28,6 +29,11 @@ def load_hostfxr(dotnet_root: 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}") From ba0a3535e7518fc36d530e52aae38b46b5bbf82f Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 14 Oct 2022 15:48:00 +0200 Subject: [PATCH 30/56] Format --- clr_loader/util/find.py | 2 +- doc/conf.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index c163240..22ba843 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -42,7 +42,7 @@ def find_dotnet_root() -> Path: prog_files = Path(prog_files) dotnet_root = prog_files / "dotnet" elif sys.platform == "darwin": - if sys.maxsize > 2**32: # is_64bits + if sys.maxsize > 2**32: # is_64bits dotnet_root = Path("/usr/local/share/dotnet/x64") else: dotnet_root = Path("/usr/local/share/dotnet") diff --git a/doc/conf.py b/doc/conf.py index 3e0f45d..1ad2427 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -6,7 +6,8 @@ # Add parent to path for autodoc import sys, os -sys.path.append(os.path.abspath('..')) + +sys.path.append(os.path.abspath("..")) # autodoc_typehints = "both" From ba68d160173f508b89fc973fc1d5cc53e8dc052d Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 14 Oct 2022 15:49:19 +0200 Subject: [PATCH 31/56] Parse the FXR version to properly select the newest one --- clr_loader/ffi/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 035541e..23debae 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -1,6 +1,6 @@ import sys from pathlib import Path -from typing import Optional +from typing import Optional, Tuple import cffi # type: ignore @@ -23,7 +23,7 @@ def load_hostfxr(dotnet_root: Path): hostfxr_path = dotnet_root / "host" / "fxr" hostfxr_paths = hostfxr_path.glob(f"?.*/{hostfxr_name}") - for hostfxr_path in reversed(sorted(hostfxr_paths)): + for hostfxr_path in reversed(sorted(hostfxr_paths, key=_path_to_version)): try: return ffi.dlopen(str(hostfxr_path)) except Exception: @@ -61,6 +61,15 @@ def load_netfx(): return ffi.dlopen(str(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: if sys.platform == "win32": return f"{name}.dll" From 0e7b2f16a76773aaad20ddee56cdab13a6e8cb0a Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 14 Oct 2022 22:52:29 +0200 Subject: [PATCH 32/56] Split package build from testing (#39) --- .github/workflows/ci.yml | 57 ++++++++++++++++++++++++++++++++-------- pyproject.toml | 8 ++++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55f5d65..b4e25f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,42 @@ on: [push, pull_request] jobs: build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions/setup-dotnet@v1 + - uses: actions/setup-python@v4 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - name: Build + run: python -m build + - name: Upload source distribution + uses: actions/upload-artifact@v3 + with: + name: sdist + path: "dist/*.tar.gz" + if-no-files-found: error + - name: Upload wheel + uses: actions/upload-artifact@v3 + with: + name: wheel + path: "dist/*.whl" + if-no-files-found: error + + test: runs-on: ${{ matrix.os }} + needs: build strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python: ['3.10', '3.9', '3.8', '3.7'] # pypy3 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v1 @@ -22,19 +50,10 @@ jobs: dotnet-version: '6.0.x' - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@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 @@ -47,6 +66,22 @@ jobs: run: | choco install -y mono ${{ matrix.python == 'pypy3' && '--x86' || '' }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Download wheel + uses: actions/download-artifact@v3 + with: + name: wheel + path: dist/ + + - name: Install wheel + shell: bash + run: | + pip install dist/*.whl + - name: Test with pytest run: | pytest diff --git a/pyproject.toml b/pyproject.toml index d344968..5491c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,12 @@ [build-system] -requires = ["setuptools>=61", "wheel"] +requires = ["setuptools>=61", "setuptools_scm[toml]", "wheel"] build-backend = "setuptools.build_meta" [project] name = "clr_loader" description = "Generic pure Python loader for .NET runtimes" license = {text = "MIT"} -version = "0.2.3" +requires-python = ">=3.7" readme = "README.md" @@ -22,6 +22,8 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", ] +dynamic = ["version"] + [[project.authors]] name = "Benedikt Reinartz" email = "filmor@gmail.com" @@ -37,6 +39,8 @@ package-data = {"clr_loader.ffi" = ["dlls/x86/*.dll", "dlls/amd64/*.dll"]} [tool.setuptools.packages.find] include = ["clr_loader*"] +[tool.setuptools_scm] + [tool.pytest.ini_options] xfail_strict = true testpaths = [ From 690a756757a41ab6c1e951655079ad148c89b415 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Sat, 15 Oct 2022 09:39:16 +0200 Subject: [PATCH 33/56] Add deployment to CI --- .github/workflows/ci.yml | 42 +++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4e25f9..45d1f06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,14 +23,8 @@ jobs: - name: Upload source distribution uses: actions/upload-artifact@v3 with: - name: sdist - path: "dist/*.tar.gz" - if-no-files-found: error - - name: Upload wheel - uses: actions/upload-artifact@v3 - with: - name: wheel - path: "dist/*.whl" + name: build-output + path: "dist/*" if-no-files-found: error test: @@ -74,7 +68,7 @@ jobs: - name: Download wheel uses: actions/download-artifact@v3 with: - name: wheel + name: build-output path: dist/ - name: Install wheel @@ -85,3 +79,33 @@ jobs: - name: Test with pytest 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 }} From c5d1a92892e871a9feb79630b981836bae795ac4 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 3 Jan 2023 12:57:47 +0100 Subject: [PATCH 34/56] Run test for autogenerated config in subprocess (#47) Only a single version of a dotnet-core runtime can be loaded at a given time. This off-loads one of our tests to a subprocess (the same pattern would work if we did this for the other test). Could be cleaner, but this will do for now. --- tests/test_common.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_common.py b/tests/test_common.py index 1191102..5e5338d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -57,6 +57,17 @@ def test_coreclr(example_netcore): def test_coreclr_autogenerated_runtimeconfig(example_netstandard): + from multiprocessing import get_context + + p = get_context("spawn").Process( + target=_do_test_coreclr_autogenerated_runtimeconfig, args=(example_netstandard,) + ) + p.start() + p.join() + p.close() + + +def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard): from clr_loader import get_coreclr coreclr = get_coreclr() From c6b464942fab7a2b2325864c51c75ab897d5f9f8 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 3 Jan 2023 12:58:41 +0100 Subject: [PATCH 35/56] Adjust CI to only run once on pull requests --- .github/workflows/ci-arm.yml | 8 ++++---- .github/workflows/ci.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml index 0c308bd..c451f0f 100644 --- a/.github/workflows/ci-arm.yml +++ b/.github/workflows/ci-arm.yml @@ -1,9 +1,9 @@ -# 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: ARM64 Tests -on: [push, pull_request] +on: + push: + branches: master + pull_request: jobs: build: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45d1f06..f6f9e75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ -# 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: From 717e797616c6306edff0dd32f8d78679e66d5ca7 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Tue, 3 Jan 2023 06:08:04 -0600 Subject: [PATCH 36/56] Add support for mono_set_dirs (#43) * add support for mono_set_dirs * add set_signal_chaining flag * add tests --- clr_loader/__init__.py | 20 ++++++++++++++++++++ clr_loader/ffi/mono.py | 5 +++++ clr_loader/mono.py | 15 +++++++++++++++ tests/test_common.py | 15 +++++++++++++++ 4 files changed, 55 insertions(+) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index d0d8ff2..4b08148 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -30,6 +30,9 @@ def get_mono( 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 @@ -48,6 +51,20 @@ def get_mono( 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 @@ -62,6 +79,9 @@ def get_mono( 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 impl diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index ed48cff..c194393 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -39,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/mono.py b/clr_loader/mono.py index fb8ab1b..158ddb7 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -24,6 +24,9 @@ def __init__( 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: Dict[Path, Any] = {} @@ -33,6 +36,9 @@ def __init__( 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: @@ -121,11 +127,17 @@ def initialize( jit_options: Optional[Sequence[str]] = None, config_file: Optional[str] = None, global_config_file: Optional[str] = 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, config_dir) + # Load in global config (i.e /etc/mono/config) global_encoded = global_config_file or ffi.NULL _MONO.mono_config_parse(global_encoded) @@ -143,6 +155,9 @@ def initialize( else: options = [] + if set_signal_chaining: + _MONO.mono_set_signal_chaining(True) + if debug: _MONO.mono_debug_init(_MONO.MONO_DEBUG_FORMAT_MONO) diff --git a/tests/test_common.py b/tests/test_common.py index 5e5338d..f42020a 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -46,6 +46,21 @@ def test_mono_debug(example_netstandard): run_tests(asm) +def test_mono_signal_chaining(example_netstandard): + 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): + 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): from clr_loader import get_coreclr From a56d77cd98c145e65fb58bd510b108ac1614533c Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 3 Jan 2023 13:08:24 +0100 Subject: [PATCH 37/56] Fix macOS dotnet root discovery (#45) --- clr_loader/util/find.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index 22ba843..d5d7b89 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -1,5 +1,5 @@ import os -import os.path +import platform import shutil import sys from pathlib import Path @@ -42,7 +42,8 @@ def find_dotnet_root() -> Path: prog_files = Path(prog_files) dotnet_root = prog_files / "dotnet" elif sys.platform == "darwin": - if sys.maxsize > 2**32: # is_64bits + 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: dotnet_root = Path("/usr/local/share/dotnet") From 79d6c6792ebc09e43d514a3ce554b32a89a2eb19 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Fri, 13 Jan 2023 04:52:54 -0600 Subject: [PATCH 38/56] fix mono set dirs (#48) * fix mono set dirs * fix path --- clr_loader/__init__.py | 2 +- clr_loader/mono.py | 2 +- clr_loader/util/find.py | 11 +++++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 4b08148..aa604ad 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -70,7 +70,7 @@ def get_mono( libmono = _maybe_path(libmono) if libmono is None: - libmono = find_libmono(sgen=sgen) + libmono = find_libmono(sgen=sgen, assembly_dir=assembly_dir) impl = Mono( # domain=domain, diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 158ddb7..7c3f20d 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -136,7 +136,7 @@ def initialize( _MONO = load_mono(libmono) if assembly_dir is not None and config_dir is not None: - _MONO.mono_set_dirs(assembly_dir, config_dir) + _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 diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index d5d7b89..6ef7bc3 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -105,7 +105,7 @@ def find_runtimes() -> Iterator[DotnetCoreRuntimeSpec]: return find_runtimes_in_root(dotnet_root) -def find_libmono(*, sgen: bool = True) -> Path: +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. @@ -137,9 +137,12 @@ def find_libmono(*, sgen: bool = True) -> Path: ) else: - from ctypes.util import find_library - - path = find_library(unix_name) + if assembly_dir == None: + from ctypes.util import find_library + 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") From 7dd9b463099425087f4de51c7a68b365a4612344 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 7 Aug 2023 14:10:46 +0200 Subject: [PATCH 39/56] Add Python 3.11 to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6f9e75..0c6749e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.10', '3.9', '3.8', '3.7'] # pypy3 + python: ['3.11', '3.10', '3.9', '3.8', '3.7'] # pypy3 steps: - uses: actions/checkout@v3 From d8f51e9e28f41f2a495327e7cbecb50edd6af243 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 7 Aug 2023 14:33:45 +0200 Subject: [PATCH 40/56] Drop CI for EOLd Python 3.7 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c6749e..41f4e0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.11', '3.10', '3.9', '3.8', '3.7'] # pypy3 + python: ['3.11', '3.10', '3.9', '3.8'] # pypy3 steps: - uses: actions/checkout@v3 From 63d7f698d3be00bd1f609545e12f87cc3c549c42 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 7 Aug 2023 14:39:44 +0200 Subject: [PATCH 41/56] Fix handling of non-ASCII assembly paths on .NET Framework (#62) --- netfx_loader/ClrLoader.cs | 6 +++--- tests/test_common.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index e8b2767..af89cee 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -51,9 +51,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/tests/test_common.py b/tests/test_common.py index f42020a..250c290 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,3 +1,4 @@ +import shutil import pytest from subprocess import check_call import os @@ -24,7 +25,7 @@ def build_example(tmpdir_factory, framework): return out -def test_mono(example_netstandard): +def test_mono(example_netstandard: Path): from clr_loader import get_mono mono = get_mono() @@ -33,7 +34,7 @@ def test_mono(example_netstandard): run_tests(asm) -def test_mono_debug(example_netstandard): +def test_mono_debug(example_netstandard: Path): from clr_loader import get_mono mono = get_mono( @@ -46,7 +47,8 @@ def test_mono_debug(example_netstandard): run_tests(asm) -def test_mono_signal_chaining(example_netstandard): + +def test_mono_signal_chaining(example_netstandard: Path): from clr_loader import get_mono mono = get_mono(set_signal_chaining=True) @@ -54,7 +56,8 @@ def test_mono_signal_chaining(example_netstandard): run_tests(asm) -def test_mono_set_dir(example_netstandard): + +def test_mono_set_dir(example_netstandard: Path): from clr_loader import get_mono mono = get_mono(assembly_dir="/usr/lib", config_dir="/etc") @@ -62,7 +65,8 @@ def test_mono_set_dir(example_netstandard): run_tests(asm) -def test_coreclr(example_netcore): + +def test_coreclr(example_netcore: Path): from clr_loader import get_coreclr coreclr = get_coreclr(runtime_config=example_netcore / "example.runtimeconfig.json") @@ -71,7 +75,7 @@ def test_coreclr(example_netcore): run_tests(asm) -def test_coreclr_autogenerated_runtimeconfig(example_netstandard): +def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): from multiprocessing import get_context p = get_context("spawn").Process( @@ -82,7 +86,7 @@ def test_coreclr_autogenerated_runtimeconfig(example_netstandard): p.close() -def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard): +def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): from clr_loader import get_coreclr coreclr = get_coreclr() @@ -94,9 +98,24 @@ def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard): @pytest.mark.skipif( sys.platform != "win32", reason=".NET Framework only exists on Windows" ) -def test_netfx(example_netstandard): +def test_netfx(example_netstandard: Path): + from clr_loader import get_netfx + + netfx = get_netfx() + asm = netfx.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_chinese_path(example_netstandard: Path, tmpdir_factory): from clr_loader import get_netfx + tmp_path = Path(tmpdir_factory.mktemp("example-中国")) + shutil.copytree(example_netstandard, tmp_path, dirs_exist_ok=True) + netfx = get_netfx() asm = netfx.get_assembly(os.path.join(example_netstandard, "example.dll")) From 000bc780b33b5dbb3f0c1891776fda0971cc2df2 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 7 Aug 2023 15:21:26 +0200 Subject: [PATCH 42/56] Delay actually loading the runtime to fix property setting (#63) * Delay actually loading the runtime to fix property setting * Launch test for properties in separate process --- clr_loader/hostfxr.py | 20 +++++++++----------- tests/test_common.py | 21 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index 522fec5..225b4c7 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -20,9 +20,8 @@ def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): self._dotnet_root = Path(dotnet_root) self._dll = load_hostfxr(self._dotnet_root) - self._is_initialized = 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 @@ -36,7 +35,7 @@ def dotnet_root(self) -> Path: @property def is_initialized(self) -> bool: - return self._is_initialized + return self._load_func is not None @property def is_shutdown(self) -> bool: @@ -81,10 +80,15 @@ def __iter__(self) -> Generator[Tuple[str, str], None, None]: for i in range(size_ptr[0]): yield (decode(keys_ptr[i]), decode(values_ptr[i])) + 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_initialized = True # Append assembly name to typename assembly_path = Path(assembly_path) @@ -92,7 +96,7 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): typename = f"{typename}, {assembly_name}" delegate_ptr = ffi.new("void**") - res = self._load_func( + res = self._get_load_func()( encode(str(assembly_path)), encode(typename), encode(function), @@ -103,12 +107,6 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): check_result(res) return ffi.cast("component_entry_point_fn", delegate_ptr[0]) - def _check_initialized(self) -> None: - if self._handle is None: - raise RuntimeError("Runtime is shut down") - elif not self._is_initialized: - raise RuntimeError("Runtime is not initialized") - def shutdown(self) -> None: if self._handle is not None: self._dll.hostfxr_close(self._handle) diff --git a/tests/test_common.py b/tests/test_common.py index 250c290..a33dec0 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -18,7 +18,7 @@ def example_netcore(tmpdir_factory): def build_example(tmpdir_factory, framework): out = Path(tmpdir_factory.mktemp(f"example-{framework}")) - proj_path = Path(__file__).parent.parent / "example" + proj_path = Path(__file__).parent.parent / "example" / "example.csproj" check_call(["dotnet", "build", str(proj_path), "-o", str(out), "-f", framework]) @@ -75,6 +75,19 @@ def test_coreclr(example_netcore: Path): run_tests(asm) +def test_coreclr_properties(example_netcore: Path): + from multiprocessing import get_context + + p = get_context("spawn").Process( + target=_do_test_coreclr_autogenerated_runtimeconfig, + args=(example_netstandard,), + kwargs=dict(properties=dict(APP_CONTEXT_BASE_DIRECTORY=str(example_netcore))), + ) + p.start() + p.join() + p.close() + + def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): from multiprocessing import get_context @@ -86,10 +99,12 @@ def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): p.close() -def _do_test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): +def _do_test_coreclr_autogenerated_runtimeconfig( + example_netstandard: Path, **properties +): from clr_loader import get_coreclr - coreclr = get_coreclr() + coreclr = get_coreclr(properties=properties) asm = coreclr.get_assembly(example_netstandard / "example.dll") run_tests(asm) From 70e2c72da9db0c769df1df195891da5c74a55dd5 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Dec 2022 16:33:59 +0100 Subject: [PATCH 43/56] Fix parameter passing for .NET Framework domains --- clr_loader/netfx.py | 11 +++++++++-- netfx_loader/ClrLoader.cs | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 24460ca..4efe841 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -14,13 +14,15 @@ def __init__( ): initialize() if config_file is not None: - config_file_s = str(config_file) + 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 or ffi.NULL, config_file_s) + self._domain = _FW.pyclr_create_appdomain(domain_s, config_file_s) def info(self) -> RuntimeInfo: return RuntimeInfo( @@ -41,6 +43,11 @@ def _get_callable(self, assembly_path: StrOrPath, typename: str, function: str): function.encode("utf8"), ) + if func == ffi.NULL: + raise RuntimeError( + f"Failed to resolve {typename}.{function} from {assembly_path}" + ) + return func def shutdown(self): diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index af89cee..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}"); From 05235b979a6fa169898318af7d752428a1bbd850 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Fri, 16 Dec 2022 16:40:08 +0100 Subject: [PATCH 44/56] Add unit test for using a separate domain --- tests/test_common.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_common.py b/tests/test_common.py index a33dec0..3cba3cd 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -137,6 +137,18 @@ def test_netfx_chinese_path(example_netstandard: Path, tmpdir_factory): run_tests(asm) +@pytest.mark.skipif( + sys.platform != "win32", reason=".NET Framework only exists on Windows" +) +def test_netfx_separate_domain(example_netstandard): + from clr_loader import get_netfx + + netfx = get_netfx(domain="some_domain") + asm = netfx.get_assembly(os.path.join(example_netstandard, "example.dll")) + + run_tests(asm) + + def run_tests(asm): func = asm.get_function("Example.TestClass", "Test") test_data = b"testy mctestface" From 59f47f9088da9cf999439bb15ff7ac131633fec7 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Mon, 7 Aug 2023 16:22:10 +0200 Subject: [PATCH 45/56] Move netfx tests to individual subprocesses --- tests/test_common.py | 53 ++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 3cba3cd..8a9e36d 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -76,27 +76,15 @@ def test_coreclr(example_netcore: Path): def test_coreclr_properties(example_netcore: Path): - from multiprocessing import get_context - - p = get_context("spawn").Process( - target=_do_test_coreclr_autogenerated_runtimeconfig, - args=(example_netstandard,), - kwargs=dict(properties=dict(APP_CONTEXT_BASE_DIRECTORY=str(example_netcore))), + run_in_subprocess( + _do_test_coreclr_autogenerated_runtimeconfig, + example_netstandard, + properties=dict(APP_CONTEXT_BASE_DIRECTORY=str(example_netcore)), ) - p.start() - p.join() - p.close() def test_coreclr_autogenerated_runtimeconfig(example_netstandard: Path): - from multiprocessing import get_context - - p = get_context("spawn").Process( - target=_do_test_coreclr_autogenerated_runtimeconfig, args=(example_netstandard,) - ) - p.start() - p.join() - p.close() + run_in_subprocess(_do_test_coreclr_autogenerated_runtimeconfig, example_netstandard) def _do_test_coreclr_autogenerated_runtimeconfig( @@ -114,37 +102,31 @@ def _do_test_coreclr_autogenerated_runtimeconfig( sys.platform != "win32", reason=".NET Framework only exists on Windows" ) def test_netfx(example_netstandard: Path): - from clr_loader import get_netfx - - netfx = get_netfx() - asm = netfx.get_assembly(example_netstandard / "example.dll") - - run_tests(asm) + 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): - from clr_loader import get_netfx - tmp_path = Path(tmpdir_factory.mktemp("example-中国")) shutil.copytree(example_netstandard, tmp_path, dirs_exist_ok=True) - netfx = get_netfx() - asm = netfx.get_assembly(os.path.join(example_netstandard, "example.dll")) - - run_tests(asm) + 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(domain="some_domain") - 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) @@ -154,3 +136,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() From f2058b49ee08817c373474d28c637f36feec4fa1 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Tue, 21 Nov 2023 12:22:31 +0100 Subject: [PATCH 46/56] Ensure that an uninitialised NetFx object does not fail in shutdown --- clr_loader/netfx.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clr_loader/netfx.py b/clr_loader/netfx.py index 4efe841..4d46b37 100644 --- a/clr_loader/netfx.py +++ b/clr_loader/netfx.py @@ -12,6 +12,8 @@ class NetFx(Runtime): def __init__( self, domain: Optional[str] = None, config_file: Optional[Path] = None ): + self._domain = None + initialize() if config_file is not None: config_file_s = str(config_file).encode("utf8") From 3e2dff02bc08a840c74c0910e0303c92c8a7c306 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Wed, 10 Jul 2024 09:23:39 +0200 Subject: [PATCH 47/56] Add ruff formatting and linting (#65) --- .github/workflows/ci.yml | 14 ++++++++++++++ clr_loader/__init__.py | 2 +- clr_loader/mono.py | 3 ++- clr_loader/util/find.py | 3 ++- doc/conf.py | 7 ++++--- tests/test_common.py | 1 - 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f4e0c..3495443 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,6 +27,20 @@ jobs: path: "dist/*" if-no-files-found: error + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depths: 0 + - uses: actions/setup-python@v4 + - name: Install Ruff + run: pip install ruff + - name: Check format + run: ruff format --check + - name: Check lints + run: ruff check + test: runs-on: ${{ matrix.os }} needs: build diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index aa604ad..71eb09d 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -32,7 +32,7 @@ def get_mono( jit_options: Optional[Sequence[str]] = None, assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, - set_signal_chaining: bool = False + set_signal_chaining: bool = False, ) -> Runtime: """Get a Mono runtime instance diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 7c3f20d..1899ea3 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -86,7 +86,8 @@ 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): diff --git a/clr_loader/util/find.py b/clr_loader/util/find.py index 6ef7bc3..65bc7ac 100644 --- a/clr_loader/util/find.py +++ b/clr_loader/util/find.py @@ -137,8 +137,9 @@ def find_libmono(*, assembly_dir: str = None, sgen: bool = True) -> Path: ) else: - if assembly_dir == None: + if assembly_dir is None: from ctypes.util import find_library + path = find_library(unix_name) else: libname = "lib" + unix_name + ".so" diff --git a/doc/conf.py b/doc/conf.py index 1ad2427..385629c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,3 +1,6 @@ +import sys +from pathlib import Path + project = "clr-loader" copyright = "2022, Benedikt Reinartz" author = "Benedikt Reinartz" @@ -5,9 +8,7 @@ extensions = ["sphinx.ext.autodoc"] # Add parent to path for autodoc -import sys, os - -sys.path.append(os.path.abspath("..")) +sys.path.append(str(Path("..").absolute())) # autodoc_typehints = "both" diff --git a/tests/test_common.py b/tests/test_common.py index 8a9e36d..139f192 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,7 +1,6 @@ import shutil import pytest from subprocess import check_call -import os import sys from pathlib import Path From 8e6dcd3cccc9bc98c877b9464f7f2000a108a49b Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 20:53:06 +0100 Subject: [PATCH 48/56] Use uv and test 3.13 and 3.12 (#72) --- .github/workflows/ci.yml | 26 +++--- pyproject.toml | 10 ++- uv.lock | 174 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 uv.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3495443..ae40be9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,13 +13,9 @@ jobs: with: fetch-depth: 0 - uses: actions/setup-dotnet@v1 - - uses: actions/setup-python@v4 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build + - uses: astral-sh/setup-uv@v4 - name: Build - run: python -m build + run: uv build - name: Upload source distribution uses: actions/upload-artifact@v3 with: @@ -33,9 +29,9 @@ jobs: - uses: actions/checkout@v3 with: fetch-depths: 0 - - uses: actions/setup-python@v4 + - uses: astral-sh/setup-uv@v4 - name: Install Ruff - run: pip install ruff + run: uv tool install ruff - name: Check format run: ruff format --check - name: Check lints @@ -46,8 +42,8 @@ jobs: needs: build strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python: ['3.11', '3.10', '3.9', '3.8'] # 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@v3 @@ -58,7 +54,7 @@ jobs: dotnet-version: '6.0.x' - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v4 + uses: astral-sh/setup-uv@v4 with: python-version: ${{ matrix.python }} @@ -76,8 +72,8 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install pytest + uv venv + uv pip install pytest - name: Download wheel uses: actions/download-artifact@v3 @@ -88,11 +84,11 @@ jobs: - name: Install wheel shell: bash run: | - pip install dist/*.whl + uv pip install dist/*.whl - name: Test with pytest run: | - pytest + uv run pytest deploy: runs-on: ubuntu-latest diff --git a/pyproject.toml b/pyproject.toml index 5491c06..94857e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,10 @@ requires-python = ">=3.7" readme = "README.md" -dependencies = ["cffi>=1.13"] +dependencies = [ + "cffi >= 1.13; python_version <= '3.8'", + "cffi >= 1.17; python_version >= '3.8'", +] classifiers = [ "Development Status :: 4 - Beta", @@ -32,6 +35,11 @@ email = "filmor@gmail.com" Sources = "https://github.com/pythonnet/clr-loader" Documentation = "https://pythonnet.github.io/clr-loader/" +[optional-dependencies] +dev = [ + "pytest" +] + [tool.setuptools] zip-safe = false package-data = {"clr_loader.ffi" = ["dlls/x86/*.dll", "dlls/amd64/*.dll"]} 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 }, +] From 9b5914e3cfcd57bf4f5d4afc8d40c4733aedc4aa Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:14:08 +0100 Subject: [PATCH 49/56] Workaround for setuptools bug #4759 --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 94857e4..c3ed5df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=61", "setuptools_scm[toml]", "wheel"] +requires = ["setuptools>=75", "setuptools_scm[toml]"] build-backend = "setuptools.build_meta" [project] @@ -43,6 +43,7 @@ dev = [ [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*"] From cbe765c35ecf8c1885be4f2eef4e9aea7d8ce406 Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 12 Dec 2024 21:33:46 +0100 Subject: [PATCH 50/56] Use dependency group instead of optional deps --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c3ed5df..2c643e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ email = "filmor@gmail.com" Sources = "https://github.com/pythonnet/clr-loader" Documentation = "https://pythonnet.github.io/clr-loader/" -[optional-dependencies] +[dependency-groups] dev = [ "pytest" ] From 80e2385f5e1ba3ed2292948422514878b951793f Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 24 Jul 2025 19:55:34 +0200 Subject: [PATCH 51/56] Update CI actions (#79) * Update CI actions * Fix format --- .github/workflows/ci-arm.yml | 5 ++--- .github/workflows/ci.yml | 22 +++++++++++----------- .github/workflows/docs.yml | 2 +- clr_loader/types.py | 2 +- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml index c451f0f..8f46757 100644 --- a/.github/workflows/ci-arm.yml +++ b/.github/workflows/ci-arm.yml @@ -2,7 +2,7 @@ name: ARM64 Tests on: push: - branches: master + branches: [master] pull_request: jobs: @@ -10,13 +10,12 @@ jobs: runs-on: [self-hosted, linux, ARM64] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x 6.0.x - name: Create virtualenv diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae40be9..b83433d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,22 +2,22 @@ name: Python Tests on: push: - branches: master + branches: [master] pull_request: jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-dotnet@v1 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v6 - name: Build run: uv build - name: Upload source distribution - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-output path: "dist/*" @@ -26,10 +26,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depths: 0 - - uses: astral-sh/setup-uv@v4 + - uses: astral-sh/setup-uv@v6 - name: Install Ruff run: uv tool install ruff - name: Check format @@ -46,7 +46,7 @@ jobs: python: ['3.13', '3.12', '3.11', '3.10', '3.9', '3.8'] # pypy3 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v1 @@ -54,13 +54,13 @@ jobs: dotnet-version: '6.0.x' - name: Set up Python ${{ matrix.python }} - uses: astral-sh/setup-uv@v4 + uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python }} - name: Cache Mono if: runner.os == 'Windows' - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ env.TEMP }}\chocolatey key: ${{ runner.os }}-chocolatey-${{ matrix.python == 'pypy3' && '32' || '64' }} @@ -76,7 +76,7 @@ jobs: uv pip install pytest - name: Download wheel - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-output path: dist/ @@ -96,7 +96,7 @@ jobs: steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build-output path: dist/ diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bce19d4..1fe8fee 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,7 @@ jobs: - name: Upload artifact # Automatically uploads an artifact from the './_site' directory by default - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: doc/_build/html/ diff --git a/clr_loader/types.py b/clr_loader/types.py index 15c1e30..6b54030 100644 --- a/clr_loader/types.py +++ b/clr_loader/types.py @@ -141,6 +141,6 @@ 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]}…" + return f"{string[: length - 1]}…" else: return string From f9e0544ff02c6fced35e47a3593d477dd17a4cb2 Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Mon, 28 Jul 2025 16:20:14 -0500 Subject: [PATCH 52/56] add trace options (#67) Co-authored-by: Mohamed Koubaa --- clr_loader/__init__.py | 2 ++ clr_loader/ffi/mono.py | 3 +++ clr_loader/mono.py | 12 ++++++++++++ tests/test_common.py | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 71eb09d..52ce603 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -33,6 +33,8 @@ def get_mono( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ) -> Runtime: """Get a Mono runtime instance diff --git a/clr_loader/ffi/mono.py b/clr_loader/ffi/mono.py index c194393..a3249a1 100644 --- a/clr_loader/ffi/mono.py +++ b/clr_loader/ffi/mono.py @@ -44,5 +44,8 @@ void mono_set_signal_chaining(bool chain_signals); +void mono_trace_set_level_string(const char* value); +void mono_trace_set_mask_string(const char* value); + """ ) diff --git a/clr_loader/mono.py b/clr_loader/mono.py index 1899ea3..f7f90e5 100644 --- a/clr_loader/mono.py +++ b/clr_loader/mono.py @@ -27,6 +27,8 @@ def __init__( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ): self._assemblies: Dict[Path, Any] = {} @@ -39,6 +41,8 @@ def __init__( assembly_dir=assembly_dir, config_dir=config_dir, set_signal_chaining=set_signal_chaining, + trace_mask=trace_mask, + trace_level=trace_level, ) if domain is None: @@ -131,11 +135,19 @@ def initialize( assembly_dir: Optional[str] = None, config_dir: Optional[str] = None, set_signal_chaining: bool = False, + trace_mask: Optional[str] = None, + trace_level: Optional[str] = None, ) -> str: global _MONO, _ROOT_DOMAIN if _MONO is None: _MONO = load_mono(libmono) + if trace_mask is not None: + _MONO.mono_trace_set_mask_string(trace_mask.encode("utf8")) + + if trace_level is not None: + _MONO.mono_trace_set_level_string(trace_level.encode("utf8")) + if assembly_dir is not None and config_dir is not None: _MONO.mono_set_dirs(assembly_dir.encode("utf8"), config_dir.encode("utf8")) diff --git a/tests/test_common.py b/tests/test_common.py index 139f192..3eaf215 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -56,6 +56,24 @@ def test_mono_signal_chaining(example_netstandard: Path): run_tests(asm) +def test_mono_trace_mask(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(trace_mask="all") + asm = mono.get_assembly(example_netstandard / "example.dll") + + run_tests(asm) + + +def test_mono_trace_level(example_netstandard: Path): + from clr_loader import get_mono + + mono = get_mono(trace_level="message") + 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 From 5dadf4cedbe8e68075d50e626f26a8fc556591af Mon Sep 17 00:00:00 2001 From: Mohamed Koubaa Date: Thu, 14 Aug 2025 16:44:40 -0500 Subject: [PATCH 53/56] Support pythonnet for AppDomain (#78) * load the assembly and get function address inside the domain When using domain.Load for an assembly, the assembly resolution rules are awkward Even if the full path is given to the AssemblyName, when the domain tries to load the assembly, it does not use that context and tries to resolve the assembly using normal domain resolution rules, which would require an assembly resolver to be installed. However, the assembly resolver that is actually used at runtime is the one installed to the main appdomain. This prevents a library like Python.Runtime.dll (used by pythonnet) which is not installed to the application base directory to be loaded by clr_loader. To fix this issue, the assembly resolver of the main appdomain is lazily extending to include paths needed for libraries passed into GetFunction, and GetFunction internally uses AppDomain.DoCallBack() to marshal the function pointer inside the target app domain, using global domain data to access the function pointer and return it to the user of clr_loader. * Add comment * PR review feedback --------- Co-authored-by: Mohamed Koubaa --- netfx_loader/ClrLoader.cs | 25 ++++++++-- netfx_loader/DomainData.cs | 99 ++++++++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 24 deletions(-) diff --git a/netfx_loader/ClrLoader.cs b/netfx_loader/ClrLoader.cs index 32b4c01..2a065bb 100644 --- a/netfx_loader/ClrLoader.cs +++ b/netfx_loader/ClrLoader.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Runtime.InteropServices; using NXPorts.Attributes; @@ -21,6 +22,19 @@ public static void Initialize() } } + private static string AssemblyDirectory + { + get + { + // This is needed in case the DLL was shadow-copied + // (Otherwise .Location would work) + string codeBase = Assembly.GetExecutingAssembly().CodeBase; + UriBuilder uri = new UriBuilder(codeBase); + string path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + [DllExport("pyclr_create_appdomain", CallingConvention.Cdecl)] public static IntPtr CreateAppDomain( [MarshalAs(UnmanagedType.LPUTF8Str)] string name, @@ -28,16 +42,17 @@ public static IntPtr CreateAppDomain( ) { Print($"Creating AppDomain {name} with {configFile}"); + + var clrLoaderDir = AssemblyDirectory; if (!string.IsNullOrEmpty(name)) { var setup = new AppDomainSetup { - ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, + ApplicationBase = clrLoaderDir, ConfigurationFile = configFile }; - Print($"Base: {AppDomain.CurrentDomain.BaseDirectory}"); + Print($"Base: {clrLoaderDir}"); var domain = AppDomain.CreateDomain(name, null, setup); - Print($"Located domain {domain}"); var domainData = new DomainData(domain); @@ -61,8 +76,8 @@ public static IntPtr GetFunction( try { var domainData = _domains[(int)domain]; - var deleg = domainData.GetEntryPoint(assemblyPath, typeName, function); - return Marshal.GetFunctionPointerForDelegate(deleg); + Print($"Getting functor for function {function} of type {typeName} in assembly {assemblyPath}"); + return domainData.GetFunctor(assemblyPath, typeName, function); } catch (Exception exc) { diff --git a/netfx_loader/DomainData.cs b/netfx_loader/DomainData.cs index 3a17d7a..35c3363 100644 --- a/netfx_loader/DomainData.cs +++ b/netfx_loader/DomainData.cs @@ -1,54 +1,115 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Runtime.InteropServices; namespace ClrLoader { using static ClrLoader; - class DomainData : IDisposable + public static class DomainSetup { public delegate int EntryPoint(IntPtr buffer, int size); + public static void StoreFunctorFromDomainData() + { + var domain = AppDomain.CurrentDomain; + var assemblyPath = (string)domain.GetData("_assemblyPath"); + var typeName = (string)domain.GetData("_typeName"); + var function = (string)domain.GetData("_function"); + var deleg = GetDelegate(domain, assemblyPath, typeName, function); + var functor = Marshal.GetFunctionPointerForDelegate(deleg); + domain.SetData("_thisDelegate", deleg); + domain.SetData("_thisFunctor", functor); + } + + private static Delegate GetDelegate(AppDomain domain, string assemblyPath, string typeName, string function) + { + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath); + var assembly = domain.Load(assemblyName); + var type = assembly.GetType(typeName, throwOnError: true); + var deleg = Delegate.CreateDelegate(typeof(EntryPoint), type, function); + return deleg; + } + } + + class DomainData : IDisposable + { bool _disposed = false; public AppDomain Domain { get; } - public Dictionary<(string, string, string), EntryPoint> _delegates; + public Dictionary<(string, string, string), IntPtr> _functors; + public HashSet _resolvedAssemblies; public DomainData(AppDomain domain) { Domain = domain; - _delegates = new Dictionary<(string, string, string), EntryPoint>(); + _functors = new Dictionary<(string, string, string), IntPtr>(); + _resolvedAssemblies = new HashSet(); } - public EntryPoint GetEntryPoint(string assemblyPath, string typeName, string function) + private void installResolver(string assemblyPath) { - if (_disposed) - throw new InvalidOperationException("Domain is already disposed"); - - var key = (assemblyPath, typeName, function); - - EntryPoint result; + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + if (_resolvedAssemblies.Contains(assemblyName)) + return; + _resolvedAssemblies.Add(assemblyName); - if (!_delegates.TryGetValue(key, out result)) + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => { - var assembly = Domain.Load(AssemblyName.GetAssemblyName(assemblyPath)); - var type = assembly.GetType(typeName, throwOnError: true); + if (args.Name.Contains(assemblyName)) + return Assembly.LoadFrom(assemblyPath); + return null; + }; + } - Print($"Loaded type {type}"); - result = (EntryPoint)Delegate.CreateDelegate(typeof(EntryPoint), type, function); + private static readonly object _lockObj = new object(); - _delegates[key] = result; - } + public IntPtr GetFunctor(string assemblyPath, string typeName, string function) + { + if (_disposed) + throw new InvalidOperationException("Domain is already disposed"); - return result; + // neither the domain data nor the _functors dictionary is threadsafe + lock (_lockObj) + { + installResolver(assemblyPath); + var assemblyName = AssemblyName.GetAssemblyName(assemblyPath).Name; + + var key = (assemblyName, typeName, function); + + IntPtr result; + if (!_functors.TryGetValue(key, out result)) + { + Domain.SetData("_assemblyPath", assemblyPath); + Domain.SetData("_typeName", typeName); + Domain.SetData("_function", function); + + Domain.DoCallBack(new CrossAppDomainDelegate(DomainSetup.StoreFunctorFromDomainData)); + result = (IntPtr)Domain.GetData("_thisFunctor"); + if (result == IntPtr.Zero) + throw new Exception($"Unable to get functor for {assemblyName}, {typeName}, {function}"); + + // set inputs to StoreFunctorFromDomainData to null. + // (There's no method to explicitly clear domain data) + Domain.SetData("_assemblyPath", null); + Domain.SetData("_typeName", null); + Domain.SetData("_function", null); + + // the result has to remain in the domain data because we don't know when the + // client of pyclr_get_function will actually invoke the functor, and if we + // remove it from the domain data after returning it may get collected too early. + _functors[key] = result; + } + return result; + } } public void Dispose() { if (!_disposed) { - _delegates.Clear(); + _functors.Clear(); if (Domain != AppDomain.CurrentDomain) AppDomain.Unload(Domain); From c3445ff0f5f7212154224717b29f64c48a81b88e Mon Sep 17 00:00:00 2001 From: Etienne Gaudrain Date: Thu, 14 Aug 2025 23:51:10 +0200 Subject: [PATCH 54/56] Added extra info about error when hostfxr not found or loaded properly. (#76) https://github.com/pythonnet/clr-loader/issues/75 --- clr_loader/ffi/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 23debae..289999d 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -23,18 +23,20 @@ def load_hostfxr(dotnet_root: Path): hostfxr_path = dotnet_root / "host" / "fxr" hostfxr_paths = hostfxr_path.glob(f"?.*/{hostfxr_name}") + error_report = list() + for hostfxr_path in reversed(sorted(hostfxr_paths, key=_path_to_version)): try: return ffi.dlopen(str(hostfxr_path)) - except Exception: - pass + except Exception as err: + error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") try: return ffi.dlopen(str(dotnet_root / hostfxr_name)) - except Exception: - pass + except Exception as err: + error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") - raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}") + raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n"+("\n\n".join(error_report))) def load_mono(path: Optional[Path] = None): From eeed6ec9ccc1be5222eb192beaef28570223a47e Mon Sep 17 00:00:00 2001 From: Benedikt Reinartz Date: Thu, 14 Aug 2025 23:53:35 +0200 Subject: [PATCH 55/56] Fix format --- clr_loader/ffi/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clr_loader/ffi/__init__.py b/clr_loader/ffi/__init__.py index 289999d..7917af9 100644 --- a/clr_loader/ffi/__init__.py +++ b/clr_loader/ffi/__init__.py @@ -36,7 +36,10 @@ def load_hostfxr(dotnet_root: Path): except Exception as err: error_report.append(f"Path {hostfxr_path} gave the following error:\n{err}") - raise RuntimeError(f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n"+("\n\n".join(error_report))) + raise RuntimeError( + f"Could not find a suitable hostfxr library in {dotnet_root}. The following paths were scanned:\n\n" + + ("\n\n".join(error_report)) + ) def load_mono(path: Optional[Path] = None): From 8c123c6147b6b7094cd6a61031db47156bff5522 Mon Sep 17 00:00:00 2001 From: Steven Lovelock Date: Thu, 14 Aug 2025 23:01:30 +0100 Subject: [PATCH 56/56] Add get_coreclr_command_line which uses hostfxr_initialize_for_dotnet_command_line (#66) see https://github.com/dotnet/runtime/blob/main/docs/design/features/native-hosting.md#initializing-host-context --- clr_loader/__init__.py | 34 ++++++++++++++++++++++ clr_loader/hostfxr.py | 64 ++++++++++++++++++++++++++++++++++-------- tests/test_common.py | 13 +++++++++ 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/clr_loader/__init__.py b/clr_loader/__init__.py index 52ce603..2744cf0 100644 --- a/clr_loader/__init__.py +++ b/clr_loader/__init__.py @@ -11,6 +11,7 @@ "get_mono", "get_netfx", "get_coreclr", + "get_coreclr_command_line", "find_dotnet_root", "find_libmono", "find_runtimes", @@ -152,6 +153,39 @@ def get_coreclr( return impl +def get_coreclr_command_line( + *, + entry_dll: StrOrPath, + dotnet_root: Optional[StrOrPath] = None, + properties: Optional[Dict[str, str]] = None +) -> Runtime: + """Get a CoreCLR (.NET Core) runtime instance + The returned ``DotnetCoreRuntimeCommandLine`` 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 entry_dll: + The path to the entry dll. + :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.""" + from .hostfxr import DotnetCoreCommandRuntime + + dotnet_root = _maybe_path(dotnet_root) + if dotnet_root is None: + dotnet_root = find_dotnet_root() + + impl = DotnetCoreCommandRuntime(entry_dll=_maybe_path(entry_dll), dotnet_root=dotnet_root) + if properties: + for key, value in properties.items(): + impl[key] = value + + return impl + + def get_netfx( *, domain: Optional[str] = None, config_file: Optional[StrOrPath] = None ) -> Runtime: diff --git a/clr_loader/hostfxr.py b/clr_loader/hostfxr.py index 225b4c7..7a159f3 100644 --- a/clr_loader/hostfxr.py +++ b/clr_loader/hostfxr.py @@ -6,13 +6,15 @@ from .types import Runtime, RuntimeInfo, StrOrPath from .util import check_result -__all__ = ["DotnetCoreRuntime"] +__all__ = ["DotnetCoreRuntime", "DotnetCoreCommandRuntime"] _IS_SHUTDOWN = False -class DotnetCoreRuntime(Runtime): - def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): +class DotnetCoreRuntimeBase(Runtime): + _version: str + + def __init__(self, dotnet_root: Path): self._handle = None if _IS_SHUTDOWN: @@ -20,15 +22,8 @@ def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): self._dotnet_root = Path(dotnet_root) self._dll = load_hostfxr(self._dotnet_root) - self._handle = _get_handle(self._dll, self._dotnet_root, runtime_config) self._load_func = None - for key, value in params.items(): - self[key] = value - - # TODO: Get version - self._version = "" - @property def dotnet_root(self) -> Path: return self._dotnet_root @@ -122,7 +117,31 @@ def info(self): ) -def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): +class DotnetCoreRuntime(DotnetCoreRuntimeBase): + def __init__(self, runtime_config: Path, dotnet_root: Path, **params: str): + super().__init__(dotnet_root) + self._handle = _get_handle_for_runtime_config(self._dll, self._dotnet_root, runtime_config) + + for key, value in params.items(): + self[key] = value + + # TODO: Get version + self._version = "" + + +class DotnetCoreCommandRuntime(DotnetCoreRuntimeBase): + def __init__(self, entry_dll: Path, dotnet_root: Path, **params: str): + super().__init__(dotnet_root) + self._handle = _get_handle_for_dotnet_command_line(self._dll, self._dotnet_root, entry_dll) + + for key, value in params.items(): + self[key] = value + + # TODO: Get version + self._version = "" + + +def _get_handle_for_runtime_config(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)) @@ -140,6 +159,29 @@ def _get_handle(dll, dotnet_root: StrOrPath, runtime_config: StrOrPath): return handle_ptr[0] +def _get_handle_for_dotnet_command_line(dll, dotnet_root: StrOrPath, entry_dll: StrOrPath): + params = ffi.new("hostfxr_initialize_parameters*") + params.size = ffi.sizeof("hostfxr_initialize_parameters") + params.host_path = ffi.NULL + dotnet_root_p = ffi.new("char_t[]", encode(str(Path(dotnet_root)))) + params.dotnet_root = dotnet_root_p + + handle_ptr = ffi.new("hostfxr_handle*") + + args_ptr = ffi.new("char_t*[1]") + arg_ptr = ffi.new("char_t[]", encode(str(Path(entry_dll)))) + args_ptr[0] = arg_ptr + res = dll.hostfxr_initialize_for_dotnet_command_line( + 1, + args_ptr, + params, handle_ptr + ) + + check_result(res) + + return handle_ptr[0] + + def _get_load_func(dll, handle): delegate_ptr = ffi.new("void**") diff --git a/tests/test_common.py b/tests/test_common.py index 3eaf215..b9b0597 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -92,6 +92,19 @@ def test_coreclr(example_netcore: Path): run_tests(asm) +def test_coreclr_command_line(example_netcore: Path): + run_in_subprocess(_do_test_coreclr_command_line, example_netcore) + + +def _do_test_coreclr_command_line(example_netcore): + from clr_loader import get_coreclr_command_line + + coreclr = get_coreclr_command_line(entry_dll=example_netcore / "example.dll") + 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,