From 01e224ff7feca7004e726053c1d6af313af24bb9 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 14 Dec 2024 15:42:44 -0500 Subject: [PATCH 01/11] Remove deprecated generate option --- robotpy_build/config/pyproject_toml.py | 3 - robotpy_build/setup.py | 18 ---- tests/cpp/pyproject.toml.tmpl | 139 +++++++++++++------------ 3 files changed, 70 insertions(+), 90 deletions(-) diff --git a/robotpy_build/config/pyproject_toml.py b/robotpy_build/config/pyproject_toml.py index 60dc801c..83ae0f81 100644 --- a/robotpy_build/config/pyproject_toml.py +++ b/robotpy_build/config/pyproject_toml.py @@ -298,9 +298,6 @@ class WrapperConfig: #: autogen_headers: Optional[Dict[str, str]] = None - #: DEPRECATED: Same as autogen_headers, but more complicated - generate: Optional[List[Dict[str, str]]] = None - #: Path to a single data.yml to use during code generation, or a directory #: of yaml files. If a directory, generation data will be looked up #: using the key in the generate dictionary. diff --git a/robotpy_build/setup.py b/robotpy_build/setup.py index 81242a6f..3f44498d 100644 --- a/robotpy_build/setup.py +++ b/robotpy_build/setup.py @@ -79,24 +79,6 @@ def __init__(self): f"robotpy-build configuration in pyproject.toml is incorrect" ) from e - # Remove deprecated 'generate' data and migrate - for wname, wrapper in self.project.wrappers.items(): - if wrapper.generate: - if wrapper.autogen_headers: - raise ValueError( - "must not specify 'generate' and 'autogen_headers'" - ) - autogen_headers = {} - for l in wrapper.generate: - for name, header in l.items(): - if name in autogen_headers: - raise ValueError( - f"{wname}.generate: duplicate key '{name}'" - ) - autogen_headers[name] = header - wrapper.autogen_headers = autogen_headers - wrapper.generate = None - # Shared wrapper writer instance self.wwriter = WrapperWriter() diff --git a/tests/cpp/pyproject.toml.tmpl b/tests/cpp/pyproject.toml.tmpl index f22d1a3b..ccf16e87 100644 --- a/tests/cpp/pyproject.toml.tmpl +++ b/tests/cpp/pyproject.toml.tmpl @@ -41,75 +41,76 @@ sources = [ ] generation_data = "gen/ft" -generate = [ - { abstract = "abstract.h" }, - { base_qualname = "base_qualname.h" }, - { base_qualname_hidden = "base_qualname_hidden.h" }, - { buffers = "buffers.h" }, - { custom_type_caster = "custom_type_caster.h" }, - { defaults = "defaults.h" }, - { docstrings = "docstrings.h" }, - { docstrings_append = "docstrings_append.h" }, - { enums = "enums.h" }, - { factory = "factory.h" }, - { fields = "fields.h" }, - { gilsafe_container = "gilsafe_container.h" }, - { keepalive = "keepalive.h" }, - { ignore = "ignore.h" }, - { ignored_by_default = "ignored_by_default.h" }, - { inline_code = "inline_code.h" }, - { lifetime = "lifetime.h" }, - { nested = "nested.h" }, - { ns_class = "ns_class.h" }, - { ns_hidden = "ns_hidden.h" }, - { operators = "operators.h" }, - { overloads = "overloads.h" }, - { parameters = "parameters.h" }, - { refqual = "refqual.h" }, - { rename = "rename.h" }, - { retval = "retval.h" }, - { subpkg = "subpkg.h" }, - { static_only = "static_only.h" }, - { trampoline = "trampoline.h" }, - { type_caster = "type_caster.h" }, - { type_caster_nested = "type_caster_nested.h" }, - { using = "using.h" }, - { using2 = "using2.h" }, - { virtual_comma = "virtual_comma.h" }, - { virtual_xform = "virtual_xform.h" }, - - # Inheritance - { IBase = "inheritance/ibase.h" }, - { IChild = "inheritance/ichild.h" }, - { IMChild = "inheritance/imchild.h" }, - { IGChild = "inheritance/igchild.h" }, - { Overloaded = "inheritance/overloaded.h" }, - { mvi = "inheritance/mvi.h" }, - { usingparent = "inheritance/usingparent.h" }, - - # Protection - { PBase = "protection/pbase.h" }, - { PChild = "protection/pchild.h" }, - { PGChild = "protection/pgchild.h" }, - - # Templates - {tbase = "templates/tbase.h"}, - {tcrtp = "templates/tcrtp.h"}, - {tcrtpfwd = "templates/tcrtpfwd.h"}, - {tconcrete = "templates/tconcrete.h"}, - - {tvbase = "templates/tvbase.h"}, - {tvchild = "templates/tvchild.h"}, - - {tbasic = "templates/basic.h"}, - {tdependent_base = "templates/dependent_base.h"}, - {tdependent_param = "templates/dependent_param.h"}, - {tdependent_using = "templates/dependent_using.h"}, - {tdependent_using2 = "templates/dependent_using2.h"}, - {tfn = "templates/fn.h" }, - {tnumeric = "templates/numeric.h"}, - {tnested = "templates/nested.h"}, -] + +[tool.robotpy-build.wrappers."rpytest.ft".autogen_headers] +abstract = "abstract.h" +base_qualname = "base_qualname.h" +base_qualname_hidden = "base_qualname_hidden.h" +buffers = "buffers.h" +custom_type_caster = "custom_type_caster.h" +defaults = "defaults.h" +docstrings = "docstrings.h" +docstrings_append = "docstrings_append.h" +enums = "enums.h" +factory = "factory.h" +fields = "fields.h" +gilsafe_container = "gilsafe_container.h" +keepalive = "keepalive.h" +ignore = "ignore.h" +ignored_by_default = "ignored_by_default.h" +inline_code = "inline_code.h" +lifetime = "lifetime.h" +nested = "nested.h" +ns_class = "ns_class.h" +ns_hidden = "ns_hidden.h" +operators = "operators.h" +overloads = "overloads.h" +parameters = "parameters.h" +refqual = "refqual.h" +rename = "rename.h" +retval = "retval.h" +subpkg = "subpkg.h" +static_only = "static_only.h" +trampoline = "trampoline.h" +type_caster = "type_caster.h" +type_caster_nested = "type_caster_nested.h" +using = "using.h" +using2 = "using2.h" +virtual_comma = "virtual_comma.h" +virtual_xform = "virtual_xform.h" + +# Inheritance +IBase = "inheritance/ibase.h" +IChild = "inheritance/ichild.h" +IMChild = "inheritance/imchild.h" +IGChild = "inheritance/igchild.h" +Overloaded = "inheritance/overloaded.h" +mvi = "inheritance/mvi.h" +usingparent = "inheritance/usingparent.h" + +# Protection +PBase = "protection/pbase.h" +PChild = "protection/pchild.h" +PGChild = "protection/pgchild.h" + +# Templates +tbase = "templates/tbase.h" +tcrtp = "templates/tcrtp.h" +tcrtpfwd = "templates/tcrtpfwd.h" +tconcrete = "templates/tconcrete.h" + +tvbase = "templates/tvbase.h" +tvchild = "templates/tvchild.h" + +tbasic = "templates/basic.h" +tdependent_base = "templates/dependent_base.h" +tdependent_param = "templates/dependent_param.h" +tdependent_using = "templates/dependent_using.h" +tdependent_using2 = "templates/dependent_using2.h" +tfn = "templates/fn.h" +tnumeric = "templates/numeric.h" +tnested = "templates/nested.h" + [tool.robotpy-build.wrappers."rpytest.srconly"] name = "rpytest_srconly" From 6324394fceff2fd94a74e1c5b53b311e640de798 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 14 Dec 2024 16:04:21 -0500 Subject: [PATCH 02/11] Class name in YAML must have namespace now --- robotpy_build/autowrap/cxxparser.py | 6 +++++- robotpy_build/autowrap/generator_data.py | 2 +- robotpy_build/config/autowrap_yml.py | 2 +- tests/cpp/gen/ft/base_qualname.yml | 8 ++++---- tests/cpp/gen/ft/base_qualname_hidden.yml | 4 ++-- tests/cpp/gen/ft/tdependent_param.yml | 2 +- tests/cpp/gen/ft/tdependent_using.yml | 2 +- tests/cpp/gen/ft/tdependent_using2.yml | 2 +- tests/cpp/gen/ft/using.yml | 2 +- tests/cpp/gen/ft/using2.yml | 4 ++-- 10 files changed, 19 insertions(+), 15 deletions(-) diff --git a/robotpy_build/autowrap/cxxparser.py b/robotpy_build/autowrap/cxxparser.py index ce832ac8..08979098 100644 --- a/robotpy_build/autowrap/cxxparser.py +++ b/robotpy_build/autowrap/cxxparser.py @@ -792,10 +792,14 @@ def _process_class_name( if not isinstance(parent, ClassBlockState): # easy case -- namespace is the next user_data up - cls_key = cls_name cls_namespace = typing.cast(str, parent.user_data) if extra_segments: cls_namespace = f"{cls_namespace}::{extra_segments}" + + if cls_namespace: + cls_key = f"{cls_namespace}::{cls_name}" + else: + cls_key = cls_name else: # Use things the parent already computed cdata = typing.cast(ClassStateData, parent.user_data) diff --git a/robotpy_build/autowrap/generator_data.py b/robotpy_build/autowrap/generator_data.py index dd68b1ff..79b4a0a4 100644 --- a/robotpy_build/autowrap/generator_data.py +++ b/robotpy_build/autowrap/generator_data.py @@ -84,7 +84,7 @@ def __init__(self, data: AutowrapConfigYaml): def get_class_data(self, name: str) -> ClassData: """ - The 'name' is [parent_class::]class_name + The 'name' is namespace::[parent_class::]class_name """ data = self.data.classes.get(name) missing = data is None diff --git a/robotpy_build/config/autowrap_yml.py b/robotpy_build/config/autowrap_yml.py index 79e4b8c1..bb0737d8 100644 --- a/robotpy_build/config/autowrap_yml.py +++ b/robotpy_build/config/autowrap_yml.py @@ -510,7 +510,7 @@ class AutowrapConfigYaml: #: attributes: Dict[str, PropData] = dataclasses.field(default_factory=dict) - #: Key is the class name + #: Key is the namespace + class name #: #: .. code-block:: yaml #: diff --git a/tests/cpp/gen/ft/base_qualname.yml b/tests/cpp/gen/ft/base_qualname.yml index fbda2543..b01476eb 100644 --- a/tests/cpp/gen/ft/base_qualname.yml +++ b/tests/cpp/gen/ft/base_qualname.yml @@ -1,11 +1,11 @@ classes: - BaseQualname: + bq::detail::BaseQualname: base_qualnames: Hidden: bq::Hidden methods: BaseQualname: - THBaseQualname: + bq::detail::THBaseQualname: base_qualnames: THiddenBase1: bq::THiddenBase1> # parse with _parse_template_decl ... or split it out @@ -13,10 +13,10 @@ classes: methods: BaseQualname: - TVisibleBase1: + bq::detail::TVisibleBase1: template_params: - T - TVisibleBase2: + bq::detail::TVisibleBase2: template_params: - T diff --git a/tests/cpp/gen/ft/base_qualname_hidden.yml b/tests/cpp/gen/ft/base_qualname_hidden.yml index 3eecbdf0..964a6126 100644 --- a/tests/cpp/gen/ft/base_qualname_hidden.yml +++ b/tests/cpp/gen/ft/base_qualname_hidden.yml @@ -1,8 +1,8 @@ classes: - THiddenBase1: + bq::THiddenBase1: template_params: - T - THiddenBase2: + bq::THiddenBase2: template_params: - T diff --git a/tests/cpp/gen/ft/tdependent_param.yml b/tests/cpp/gen/ft/tdependent_param.yml index 218ccb66..63afe8b2 100644 --- a/tests/cpp/gen/ft/tdependent_param.yml +++ b/tests/cpp/gen/ft/tdependent_param.yml @@ -1,6 +1,6 @@ --- classes: - TDependentParam: + whatever::TDependentParam: template_params: - T typealias: diff --git a/tests/cpp/gen/ft/tdependent_using.yml b/tests/cpp/gen/ft/tdependent_using.yml index 312221df..f6158f79 100644 --- a/tests/cpp/gen/ft/tdependent_using.yml +++ b/tests/cpp/gen/ft/tdependent_using.yml @@ -1,6 +1,6 @@ --- classes: - TDependentUsing: + whatever::TDependentUsing: template_params: - T typealias: diff --git a/tests/cpp/gen/ft/tdependent_using2.yml b/tests/cpp/gen/ft/tdependent_using2.yml index 8955c57f..ceaa1f0f 100644 --- a/tests/cpp/gen/ft/tdependent_using2.yml +++ b/tests/cpp/gen/ft/tdependent_using2.yml @@ -1,6 +1,6 @@ --- classes: - TDependentUsing2: + whatever::TDependentUsing2: template_params: - T diff --git a/tests/cpp/gen/ft/using.yml b/tests/cpp/gen/ft/using.yml index e426a47e..67932c45 100644 --- a/tests/cpp/gen/ft/using.yml +++ b/tests/cpp/gen/ft/using.yml @@ -3,6 +3,6 @@ typealias: - cr::AlsoCantResolve classes: - ProtectedUsing: + cr::inner::ProtectedUsing: typealias: - cr::CantResolve \ No newline at end of file diff --git a/tests/cpp/gen/ft/using2.yml b/tests/cpp/gen/ft/using2.yml index 09891640..4797f4f2 100644 --- a/tests/cpp/gen/ft/using2.yml +++ b/tests/cpp/gen/ft/using2.yml @@ -3,10 +3,10 @@ extra_includes: - using.h classes: - fancy_list: + u::fancy_list: template_params: - T - Using3: + u::u2::Using3: typealias: - template using fancy_list = u::fancy_list From 26fb9580495ba9d9175667d94610271149323e4a Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Thu, 26 Dec 2024 12:53:00 -0500 Subject: [PATCH 03/11] All classes must be specified in .yml file now - This is because the generator will need to know that a trampoline is potentially being generated only by parsing pyproject.toml/autogen yaml --- examples/demo/gen/demo.yml | 9 ++++++++ robotpy_build/autowrap/cxxparser.py | 7 ++++-- robotpy_build/autowrap/generator_data.py | 7 +++--- robotpy_build/wrapper.py | 2 +- tests/cpp/gen/ft/IBase.yml | 18 +++++++++++++++ tests/cpp/gen/ft/IChild.yml | 12 ++++++++++ tests/cpp/gen/ft/IGChild.yml | 6 +++++ tests/cpp/gen/ft/IMChild.yml | 8 +++++++ tests/cpp/gen/ft/Overloaded.yml | 26 ++++++++++++++++++++++ tests/cpp/gen/ft/PBase.yml | 10 +++++++++ tests/cpp/gen/ft/PChild.yml | 7 ++++++ tests/cpp/gen/ft/PGChild.yml | 6 +++++ tests/cpp/gen/ft/abstract.yml | 10 +++++++++ tests/cpp/gen/ft/base_qualname.yml | 2 ++ tests/cpp/gen/ft/base_qualname_hidden.yml | 2 ++ tests/cpp/gen/ft/custom_type_caster.yml | 5 +++++ tests/cpp/gen/ft/defaults.yml | 13 +++++++++++ tests/cpp/gen/ft/enums.yml | 4 ++++ tests/cpp/gen/ft/fields.yml | 27 +++++++++++++++++++++++ tests/cpp/gen/ft/gilsafe_container.yml | 7 ++++++ tests/cpp/gen/ft/ignored_by_default.yml | 1 + tests/cpp/gen/ft/keepalive.yml | 12 ++++++++++ tests/cpp/gen/ft/lifetime.yml | 10 +++++++++ tests/cpp/gen/ft/mvi.yml | 12 +++++----- tests/cpp/gen/ft/ns_class.yml | 6 +++++ tests/cpp/gen/ft/ns_hidden.yml | 15 +++++++++++++ tests/cpp/gen/ft/operators.yml | 16 ++++++++++++++ tests/cpp/gen/ft/parameters.yml | 3 +++ tests/cpp/gen/ft/retval.yml | 11 +++++++++ tests/cpp/gen/ft/trampoline.yml | 5 +++-- tests/cpp/gen/ft/type_caster.yml | 4 ++++ tests/cpp/gen/ft/type_caster_nested.yml | 6 +++++ tests/cpp/gen/ft/using.yml | 3 ++- tests/cpp/gen/ft/using2.yml | 5 +++++ tests/cpp/gen/ft/usingparent.yml | 10 +++++++++ tests/cpp/gen/ft/virtual_comma.yml | 11 +++++++++ 36 files changed, 303 insertions(+), 15 deletions(-) create mode 100644 examples/demo/gen/demo.yml create mode 100644 tests/cpp/gen/ft/IBase.yml create mode 100644 tests/cpp/gen/ft/IChild.yml create mode 100644 tests/cpp/gen/ft/IGChild.yml create mode 100644 tests/cpp/gen/ft/IMChild.yml create mode 100644 tests/cpp/gen/ft/Overloaded.yml create mode 100644 tests/cpp/gen/ft/PBase.yml create mode 100644 tests/cpp/gen/ft/PChild.yml create mode 100644 tests/cpp/gen/ft/PGChild.yml create mode 100644 tests/cpp/gen/ft/abstract.yml create mode 100644 tests/cpp/gen/ft/custom_type_caster.yml create mode 100644 tests/cpp/gen/ft/defaults.yml create mode 100644 tests/cpp/gen/ft/fields.yml create mode 100644 tests/cpp/gen/ft/gilsafe_container.yml create mode 100644 tests/cpp/gen/ft/keepalive.yml create mode 100644 tests/cpp/gen/ft/lifetime.yml create mode 100644 tests/cpp/gen/ft/ns_class.yml create mode 100644 tests/cpp/gen/ft/ns_hidden.yml create mode 100644 tests/cpp/gen/ft/operators.yml create mode 100644 tests/cpp/gen/ft/retval.yml create mode 100644 tests/cpp/gen/ft/type_caster.yml create mode 100644 tests/cpp/gen/ft/type_caster_nested.yml create mode 100644 tests/cpp/gen/ft/usingparent.yml create mode 100644 tests/cpp/gen/ft/virtual_comma.yml diff --git a/examples/demo/gen/demo.yml b/examples/demo/gen/demo.yml new file mode 100644 index 00000000..a1b6811f --- /dev/null +++ b/examples/demo/gen/demo.yml @@ -0,0 +1,9 @@ +--- + +functions: + add2: +classes: + DemoClass: + methods: + setX: + getX: diff --git a/robotpy_build/autowrap/cxxparser.py b/robotpy_build/autowrap/cxxparser.py index 08979098..ce976e29 100644 --- a/robotpy_build/autowrap/cxxparser.py +++ b/robotpy_build/autowrap/cxxparser.py @@ -587,12 +587,15 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]: return False cls_key, cls_name, cls_namespace, parent_ctx = cls_name_result - class_data = self.gendata.get_class_data(cls_key) + class_data, missing = self.gendata.get_class_data(cls_key) - # Ignore explicitly ignored classes + # Ignore explicitly ignored classes (including default-ignore) if class_data.ignore: return False + if missing and not self.report_only: + raise ValueError(f"'{cls_key}' must be in {self.gendata.data_fname}") + for typename in class_data.force_type_casters: self._add_user_type_caster(typename) diff --git a/robotpy_build/autowrap/generator_data.py b/robotpy_build/autowrap/generator_data.py index 79b4a0a4..7cb69d90 100644 --- a/robotpy_build/autowrap/generator_data.py +++ b/robotpy_build/autowrap/generator_data.py @@ -66,8 +66,9 @@ class GeneratorData: data: AutowrapConfigYaml - def __init__(self, data: AutowrapConfigYaml): + def __init__(self, data: AutowrapConfigYaml, data_fname: str): self.data = data + self.data_fname = data_fname default_ignore = self.data.defaults.ignore self._default_enum_data = EnumData(ignore=default_ignore) @@ -82,7 +83,7 @@ def __init__(self, data: AutowrapConfigYaml): self.enums: EnumMissingData = {} self.attributes: AttrMissingData = {} - def get_class_data(self, name: str) -> ClassData: + def get_class_data(self, name: str) -> Tuple[ClassData, bool]: """ The 'name' is namespace::[parent_class::]class_name """ @@ -92,7 +93,7 @@ def get_class_data(self, name: str) -> ClassData: data = self._default_class_data self.classes[name] = ClsReportData(missing=missing) - return data + return data, missing def get_cls_enum_data( self, name: str, cls_key: str, cls_data: ClassData diff --git a/robotpy_build/wrapper.py b/robotpy_build/wrapper.py index 33e72552..2176d9e6 100644 --- a/robotpy_build/wrapper.py +++ b/robotpy_build/wrapper.py @@ -694,7 +694,7 @@ def on_build_gen( ) ) - gendata = GeneratorData(data) + gendata = GeneratorData(data, data_fname) try: hctx = parse_header( diff --git a/tests/cpp/gen/ft/IBase.yml b/tests/cpp/gen/ft/IBase.yml new file mode 100644 index 00000000..faba2579 --- /dev/null +++ b/tests/cpp/gen/ft/IBase.yml @@ -0,0 +1,18 @@ +--- + +classes: + inheritance::IBase: + methods: + IBase: + baseOnly: + baseAndGrandchild: + baseAndChild: + baseAndPyChild: + baseAndChildFinal: + getBaseOnly: + getBaseAndGrandchild: + getBaseAndChild: + getBaseAndPyChild: + getBaseAndChildFinal: + protectedMethod: + protectedOutMethod: diff --git a/tests/cpp/gen/ft/IChild.yml b/tests/cpp/gen/ft/IChild.yml new file mode 100644 index 00000000..69ad2af5 --- /dev/null +++ b/tests/cpp/gen/ft/IChild.yml @@ -0,0 +1,12 @@ +--- + +classes: + inheritance::IChild: + attributes: + i: + methods: + IChild: + baseAndChild: + baseAndChildFinal: + getI: + inheritance::IFinal: diff --git a/tests/cpp/gen/ft/IGChild.yml b/tests/cpp/gen/ft/IGChild.yml new file mode 100644 index 00000000..7291758f --- /dev/null +++ b/tests/cpp/gen/ft/IGChild.yml @@ -0,0 +1,6 @@ +--- + +classes: + inheritance::IGrandChild: + methods: + baseAndGrandchild: diff --git a/tests/cpp/gen/ft/IMChild.yml b/tests/cpp/gen/ft/IMChild.yml new file mode 100644 index 00000000..ab436e9e --- /dev/null +++ b/tests/cpp/gen/ft/IMChild.yml @@ -0,0 +1,8 @@ +--- + +classes: + inheritance::IMOther: + inheritance::IMChild: + methods: + baseAndChild: + baseAndChildFinal: diff --git a/tests/cpp/gen/ft/Overloaded.yml b/tests/cpp/gen/ft/Overloaded.yml new file mode 100644 index 00000000..7bb1ae88 --- /dev/null +++ b/tests/cpp/gen/ft/Overloaded.yml @@ -0,0 +1,26 @@ +--- + +functions: + OBinitOB: + OCinitOB: + OCinitOC: + OGinitOC: +classes: + overloaded_inheritance::OBInitializer: + methods: + doInit: + overloaded_inheritance::OB: + methods: + Init: + overloaded_inheritance::OCInitializer: + methods: + doInit: + overloaded_inheritance::OC: + methods: + Init: + overloads: + OBInitializer&: + OCInitializer&: + overloaded_inheritance::OG: + methods: + Init: diff --git a/tests/cpp/gen/ft/PBase.yml b/tests/cpp/gen/ft/PBase.yml new file mode 100644 index 00000000..b0cd4b37 --- /dev/null +++ b/tests/cpp/gen/ft/PBase.yml @@ -0,0 +1,10 @@ +--- + +classes: + PBase: + methods: + getChannel: + privateFinalTestC: + privateFinalTestGC: + privateOverrideTestC: + setChannel: diff --git a/tests/cpp/gen/ft/PChild.yml b/tests/cpp/gen/ft/PChild.yml new file mode 100644 index 00000000..7466e8d3 --- /dev/null +++ b/tests/cpp/gen/ft/PChild.yml @@ -0,0 +1,7 @@ +--- + +classes: + PChild: + methods: + privateFinalTestGC: + PChild: diff --git a/tests/cpp/gen/ft/PGChild.yml b/tests/cpp/gen/ft/PGChild.yml new file mode 100644 index 00000000..2eb166c8 --- /dev/null +++ b/tests/cpp/gen/ft/PGChild.yml @@ -0,0 +1,6 @@ +--- + +classes: + PGChild: + methods: + PGChild: diff --git a/tests/cpp/gen/ft/abstract.yml b/tests/cpp/gen/ft/abstract.yml new file mode 100644 index 00000000..dc54802d --- /dev/null +++ b/tests/cpp/gen/ft/abstract.yml @@ -0,0 +1,10 @@ +--- + +classes: + Abstract: + methods: + mustOverrideMe: + PrivateAbstract: + methods: + PrivateAbstract: + getPrivateOverride: diff --git a/tests/cpp/gen/ft/base_qualname.yml b/tests/cpp/gen/ft/base_qualname.yml index b01476eb..48b5ce32 100644 --- a/tests/cpp/gen/ft/base_qualname.yml +++ b/tests/cpp/gen/ft/base_qualname.yml @@ -20,6 +20,8 @@ classes: template_params: - T + bq::detail::TVBaseQualname: + templates: TVisibleBase: qualname: bq::detail::TVisibleBase1 diff --git a/tests/cpp/gen/ft/base_qualname_hidden.yml b/tests/cpp/gen/ft/base_qualname_hidden.yml index 964a6126..c8a7f93d 100644 --- a/tests/cpp/gen/ft/base_qualname_hidden.yml +++ b/tests/cpp/gen/ft/base_qualname_hidden.yml @@ -6,6 +6,8 @@ classes: template_params: - T + bq::Hidden: + templates: THiddenBase: qualname: bq::THiddenBase1 diff --git a/tests/cpp/gen/ft/custom_type_caster.yml b/tests/cpp/gen/ft/custom_type_caster.yml new file mode 100644 index 00000000..72eb8d35 --- /dev/null +++ b/tests/cpp/gen/ft/custom_type_caster.yml @@ -0,0 +1,5 @@ +--- + +functions: + convertRpyintToInt: + checkConvertRpyintToInt: diff --git a/tests/cpp/gen/ft/defaults.yml b/tests/cpp/gen/ft/defaults.yml new file mode 100644 index 00000000..c31e8c48 --- /dev/null +++ b/tests/cpp/gen/ft/defaults.yml @@ -0,0 +1,13 @@ +--- + +functions: + fnSimpleDefaultParam: + fnEmptyDefaultParam: +classes: + NotImportant: + HasDefaults: + attributes: + kDefVal: + methods: + getVal: + objectDefault: diff --git a/tests/cpp/gen/ft/enums.yml b/tests/cpp/gen/ft/enums.yml index 445f15c2..8d4e7516 100644 --- a/tests/cpp/gen/ft/enums.yml +++ b/tests/cpp/gen/ft/enums.yml @@ -7,7 +7,11 @@ enums: arithmetic: true classes: + EnumContainer: EnumContainer2: enums: InnerMathEnum: arithmetic: true + enum_container_ns::NSEnumContainer: + enum_container_ns::NSEnumContainer2: + enum_container_ns::NSEnumContainer2::InnerEnumContainer: diff --git a/tests/cpp/gen/ft/fields.yml b/tests/cpp/gen/ft/fields.yml new file mode 100644 index 00000000..84bcee5c --- /dev/null +++ b/tests/cpp/gen/ft/fields.yml @@ -0,0 +1,27 @@ +--- + +classes: + ClassWithFields: + attributes: + array_of_two: + actual_int: + ref_int: + const_field: + static_int: + static_const: + static_constexpr: + should_be_ignored: + methods: + ClassWithFields: + get_array_of_two: + StructWithBitfields: + attributes: + enabled: + autonomous: + test: + eStop: + fmsAttached: + dsAttached: + control_reserved: + methods: + StructWithBitfields: diff --git a/tests/cpp/gen/ft/gilsafe_container.yml b/tests/cpp/gen/ft/gilsafe_container.yml new file mode 100644 index 00000000..26c8f906 --- /dev/null +++ b/tests/cpp/gen/ft/gilsafe_container.yml @@ -0,0 +1,7 @@ +--- + +classes: + GilsafeContainer: + methods: + assign: + check: diff --git a/tests/cpp/gen/ft/ignored_by_default.yml b/tests/cpp/gen/ft/ignored_by_default.yml index dc5a4243..38c035be 100644 --- a/tests/cpp/gen/ft/ignored_by_default.yml +++ b/tests/cpp/gen/ft/ignored_by_default.yml @@ -9,6 +9,7 @@ functions: id_fnEnable: ignore: false classes: + # id_IgnoreClass doesn't need to be specified because of default-ignore id_EnabledClass: ignore: false enums: diff --git a/tests/cpp/gen/ft/keepalive.yml b/tests/cpp/gen/ft/keepalive.yml new file mode 100644 index 00000000..0bd95d34 --- /dev/null +++ b/tests/cpp/gen/ft/keepalive.yml @@ -0,0 +1,12 @@ +--- + +classes: + PatientRef: + attributes: + dead: + Nurse: + attributes: + m_p: + methods: + Nurse: + patientDead: diff --git a/tests/cpp/gen/ft/lifetime.yml b/tests/cpp/gen/ft/lifetime.yml new file mode 100644 index 00000000..59bdb9a7 --- /dev/null +++ b/tests/cpp/gen/ft/lifetime.yml @@ -0,0 +1,10 @@ +--- + +classes: + LTWithVirtual: + methods: + get_bool: + LTTester: + methods: + set_val: + get_bool: diff --git a/tests/cpp/gen/ft/mvi.yml b/tests/cpp/gen/ft/mvi.yml index ea253273..ecb2f79f 100644 --- a/tests/cpp/gen/ft/mvi.yml +++ b/tests/cpp/gen/ft/mvi.yml @@ -1,31 +1,31 @@ classes: - MVB: + inheritance::MVB: attributes: b: methods: MVB: get_b_b: - MVC: + inheritance::MVC: attributes: c: methods: get_c_b: get_c_c: - MVD0: + inheritance::MVD0: attributes: d0: methods: get_d0_b: get_d0_c: get_d0_d0: - MVD1: + inheritance::MVD1: attributes: d1: methods: get_d1_b: get_d1_c: get_d1_d1: - MVE: + inheritance::MVE: attributes: e: methods: @@ -34,7 +34,7 @@ classes: get_e_d0: get_e_d1: get_e_e: - MVF: + inheritance::MVF: force_multiple_inheritance: true attributes: f: diff --git a/tests/cpp/gen/ft/ns_class.yml b/tests/cpp/gen/ft/ns_class.yml new file mode 100644 index 00000000..6d8f94a8 --- /dev/null +++ b/tests/cpp/gen/ft/ns_class.yml @@ -0,0 +1,6 @@ +--- + +classes: + ::ns::NSClass: + methods: + getN: diff --git a/tests/cpp/gen/ft/ns_hidden.yml b/tests/cpp/gen/ft/ns_hidden.yml new file mode 100644 index 00000000..b67e3385 --- /dev/null +++ b/tests/cpp/gen/ft/ns_hidden.yml @@ -0,0 +1,15 @@ +--- + +enums: + E: +classes: + o::O: + methods: + O: + n::h::C: + methods: + fn: + ::o::AnotherC: + methods: + AnotherC: + fn: diff --git a/tests/cpp/gen/ft/operators.yml b/tests/cpp/gen/ft/operators.yml new file mode 100644 index 00000000..1ebb185d --- /dev/null +++ b/tests/cpp/gen/ft/operators.yml @@ -0,0 +1,16 @@ +--- + +classes: + HasOperator: + methods: + HasOperator: + overloads: + "": + int: + operator==: + HasOperatorNoDefault: + attributes: + x: + methods: + HasOperatorNoDefault: + operator==: diff --git a/tests/cpp/gen/ft/parameters.yml b/tests/cpp/gen/ft/parameters.yml index e6f40c6b..7d4b9c7a 100644 --- a/tests/cpp/gen/ft/parameters.yml +++ b/tests/cpp/gen/ft/parameters.yml @@ -23,3 +23,6 @@ functions: param_override: p: no_default: True + +classes: + Param: \ No newline at end of file diff --git a/tests/cpp/gen/ft/retval.yml b/tests/cpp/gen/ft/retval.yml new file mode 100644 index 00000000..6daee48d --- /dev/null +++ b/tests/cpp/gen/ft/retval.yml @@ -0,0 +1,11 @@ +--- + +classes: + RetvalClass: + enums: + Retval: + methods: + get: + overloads: + "": + int: diff --git a/tests/cpp/gen/ft/trampoline.yml b/tests/cpp/gen/ft/trampoline.yml index 4b522110..4fd2af0f 100644 --- a/tests/cpp/gen/ft/trampoline.yml +++ b/tests/cpp/gen/ft/trampoline.yml @@ -1,5 +1,6 @@ --- classes: + MoveOnlyParam: ClassWithTrampoline: trampoline_inline_code: | int get42() { return 42; } @@ -17,8 +18,8 @@ classes: [&](py::function fn) -> int { return py::cast(fn(param.i)); } - - + ConstexprTrampoline: + ChildConstexprTrampoline: inline_code: | cls_ClassWithTrampoline diff --git a/tests/cpp/gen/ft/type_caster.yml b/tests/cpp/gen/ft/type_caster.yml new file mode 100644 index 00000000..e3f1f8ea --- /dev/null +++ b/tests/cpp/gen/ft/type_caster.yml @@ -0,0 +1,4 @@ +--- + +functions: + get123: diff --git a/tests/cpp/gen/ft/type_caster_nested.yml b/tests/cpp/gen/ft/type_caster_nested.yml new file mode 100644 index 00000000..031fdc64 --- /dev/null +++ b/tests/cpp/gen/ft/type_caster_nested.yml @@ -0,0 +1,6 @@ +--- + +classes: + NestedTypecaster: + methods: + callWithList: diff --git a/tests/cpp/gen/ft/using.yml b/tests/cpp/gen/ft/using.yml index 67932c45..0a2b3179 100644 --- a/tests/cpp/gen/ft/using.yml +++ b/tests/cpp/gen/ft/using.yml @@ -5,4 +5,5 @@ typealias: classes: cr::inner::ProtectedUsing: typealias: - - cr::CantResolve \ No newline at end of file + - cr::CantResolve + u::FwdDecl: \ No newline at end of file diff --git a/tests/cpp/gen/ft/using2.yml b/tests/cpp/gen/ft/using2.yml index 4797f4f2..96e668ff 100644 --- a/tests/cpp/gen/ft/using2.yml +++ b/tests/cpp/gen/ft/using2.yml @@ -6,9 +6,14 @@ classes: u::fancy_list: template_params: - T + u::u2::Using1: + u::u2::Using2: u::u2::Using3: typealias: - template using fancy_list = u::fancy_list + u::u2::Using4: + u::u2::Using5a: + u::u2::Using5b: templates: fancy_int_list: diff --git a/tests/cpp/gen/ft/usingparent.yml b/tests/cpp/gen/ft/usingparent.yml new file mode 100644 index 00000000..7edf9a8a --- /dev/null +++ b/tests/cpp/gen/ft/usingparent.yml @@ -0,0 +1,10 @@ +--- + +classes: + UPBase: + methods: + get5: + UPChild: + methods: + get5: + get6: diff --git a/tests/cpp/gen/ft/virtual_comma.yml b/tests/cpp/gen/ft/virtual_comma.yml new file mode 100644 index 00000000..8fe13bdf --- /dev/null +++ b/tests/cpp/gen/ft/virtual_comma.yml @@ -0,0 +1,11 @@ +--- + +classes: + VirtualComma: + methods: + getTwoTwo: + getRval: + overloads: + "": + int: + VirtualComma::RVal: From 1022d4d76cb82dace8224a26efb6ea1731cb93af Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Sat, 1 Mar 2025 23:07:20 -0500 Subject: [PATCH 04/11] Rename most of the project to semiwrap - Switch to src layout - Docs not updated - internals not fully updated --- .gitignore | 2 +- .gitmodules | 4 +-- MANIFEST.in | 2 -- README.md | 12 ++++---- docs/conf.py | 4 +-- pyproject.toml | 29 +++++++++---------- {robotpy_build => src/semiwrap}/__init__.py | 0 {robotpy_build => src/semiwrap}/__main__.py | 0 .../semiwrap}/autowrap/__init__.py | 0 .../semiwrap}/autowrap/buffer.py | 0 .../semiwrap}/autowrap/context.py | 0 .../semiwrap}/autowrap/cxxparser.py | 2 +- .../semiwrap}/autowrap/generator_data.py | 0 .../semiwrap}/autowrap/mangle.py | 0 .../semiwrap}/autowrap/render_cls_prologue.py | 0 .../autowrap/render_cls_rpy_include.py | 0 .../semiwrap}/autowrap/render_pybind11.py | 0 .../semiwrap}/autowrap/render_tmpl_inst.py | 0 .../semiwrap}/autowrap/render_wrapped.py | 0 .../semiwrap}/autowrap/writer.py | 0 .../semiwrap}/command/__init__.py | 0 .../semiwrap}/command/_built_env.py | 0 .../semiwrap}/command/build_dl.py | 0 .../semiwrap}/command/build_ext.py | 0 .../semiwrap}/command/build_gen.py | 0 .../semiwrap}/command/build_py.py | 0 .../semiwrap}/command/build_pyi.py | 0 .../semiwrap}/command/develop.py | 0 .../semiwrap}/command/editable_wheel.py | 0 .../semiwrap}/command/update_init.py | 0 .../semiwrap}/command/util.py | 0 .../semiwrap}/config/__init__.py | 0 .../semiwrap}/config/autowrap_yml.py | 2 +- .../semiwrap}/config/dev_yml.py | 0 .../semiwrap}/config/pyproject_toml.py | 0 .../semiwrap}/config/util.py | 0 {robotpy_build => src/semiwrap}/download.py | 0 .../semiwrap}/include/gilsafe_object.h | 0 .../semiwrap}/include/pybind11_typing.h | 0 .../semiwrap}/include/robotpy_build.h | 0 {robotpy_build => src/semiwrap}/maven.py | 0 {robotpy_build => src/semiwrap}/overrides.py | 0 {robotpy_build => src/semiwrap}/pkgcfg.py | 0 .../semiwrap}/pkgcfg_provider.py | 0 {robotpy_build => src/semiwrap}/platforms.py | 2 +- {robotpy_build => src/semiwrap}/pybind11 | 0 .../semiwrap}/relink_libs.py | 0 {robotpy_build => src/semiwrap}/setup.py | 0 .../semiwrap}/static_libs.py | 0 .../semiwrap}/tool/__init__.py | 0 .../semiwrap}/tool/__main__.py | 0 .../semiwrap}/tool/build_dep.py | 0 .../semiwrap}/tool/create_gen.py | 0 .../semiwrap}/tool/create_imports.py | 0 .../semiwrap}/tool/parse_maven.py | 0 .../semiwrap}/tool/platform_info.py | 0 .../semiwrap}/tool/scan_headers.py | 0 .../semiwrap}/tool/show_override.py | 0 {robotpy_build => src/semiwrap}/tool/util.py | 0 {robotpy_build => src/semiwrap}/wrapper.py | 0 60 files changed, 28 insertions(+), 31 deletions(-) delete mode 100644 MANIFEST.in rename {robotpy_build => src/semiwrap}/__init__.py (100%) rename {robotpy_build => src/semiwrap}/__main__.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/__init__.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/buffer.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/context.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/cxxparser.py (99%) rename {robotpy_build => src/semiwrap}/autowrap/generator_data.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/mangle.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/render_cls_prologue.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/render_cls_rpy_include.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/render_pybind11.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/render_tmpl_inst.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/render_wrapped.py (100%) rename {robotpy_build => src/semiwrap}/autowrap/writer.py (100%) rename {robotpy_build => src/semiwrap}/command/__init__.py (100%) rename {robotpy_build => src/semiwrap}/command/_built_env.py (100%) rename {robotpy_build => src/semiwrap}/command/build_dl.py (100%) rename {robotpy_build => src/semiwrap}/command/build_ext.py (100%) rename {robotpy_build => src/semiwrap}/command/build_gen.py (100%) rename {robotpy_build => src/semiwrap}/command/build_py.py (100%) rename {robotpy_build => src/semiwrap}/command/build_pyi.py (100%) rename {robotpy_build => src/semiwrap}/command/develop.py (100%) rename {robotpy_build => src/semiwrap}/command/editable_wheel.py (100%) rename {robotpy_build => src/semiwrap}/command/update_init.py (100%) rename {robotpy_build => src/semiwrap}/command/util.py (100%) rename {robotpy_build => src/semiwrap}/config/__init__.py (100%) rename {robotpy_build => src/semiwrap}/config/autowrap_yml.py (99%) rename {robotpy_build => src/semiwrap}/config/dev_yml.py (100%) rename {robotpy_build => src/semiwrap}/config/pyproject_toml.py (100%) rename {robotpy_build => src/semiwrap}/config/util.py (100%) rename {robotpy_build => src/semiwrap}/download.py (100%) rename {robotpy_build => src/semiwrap}/include/gilsafe_object.h (100%) rename {robotpy_build => src/semiwrap}/include/pybind11_typing.h (100%) rename {robotpy_build => src/semiwrap}/include/robotpy_build.h (100%) rename {robotpy_build => src/semiwrap}/maven.py (100%) rename {robotpy_build => src/semiwrap}/overrides.py (100%) rename {robotpy_build => src/semiwrap}/pkgcfg.py (100%) rename {robotpy_build => src/semiwrap}/pkgcfg_provider.py (100%) rename {robotpy_build => src/semiwrap}/platforms.py (97%) rename {robotpy_build => src/semiwrap}/pybind11 (100%) rename {robotpy_build => src/semiwrap}/relink_libs.py (100%) rename {robotpy_build => src/semiwrap}/setup.py (100%) rename {robotpy_build => src/semiwrap}/static_libs.py (100%) rename {robotpy_build => src/semiwrap}/tool/__init__.py (100%) rename {robotpy_build => src/semiwrap}/tool/__main__.py (100%) rename {robotpy_build => src/semiwrap}/tool/build_dep.py (100%) rename {robotpy_build => src/semiwrap}/tool/create_gen.py (100%) rename {robotpy_build => src/semiwrap}/tool/create_imports.py (100%) rename {robotpy_build => src/semiwrap}/tool/parse_maven.py (100%) rename {robotpy_build => src/semiwrap}/tool/platform_info.py (100%) rename {robotpy_build => src/semiwrap}/tool/scan_headers.py (100%) rename {robotpy_build => src/semiwrap}/tool/show_override.py (100%) rename {robotpy_build => src/semiwrap}/tool/util.py (100%) rename {robotpy_build => src/semiwrap}/wrapper.py (100%) diff --git a/.gitignore b/.gitignore index cc00707f..a28df6f5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ __pycache__ /dist /build -/robotpy_build/version.py \ No newline at end of file +/src/semiwrap/version.py \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 8daf168a..5d872ea0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "robotpy_build/pybind11"] - path = robotpy_build/pybind11 +[submodule "src/semiwrap/pybind11"] + path = src/semiwrap/pybind11 url = https://github.com/pybind/pybind11.git diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 0396ccfa..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -recursive-include robotpy_build/pybind11/include *.h -recursive-include robotpy_build/include *.h \ No newline at end of file diff --git a/README.md b/README.md index 27a4e659..d44537ae 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -robotpy-build -============= +semiwrap +======== This is a build tool intended to be generally useful for any python project that has binary dependencies. It is especially designed to meet the needs @@ -14,13 +14,15 @@ Requires Python 3.8+ Documentation ------------- -Documentation can be found at https://robotpy-build.readthedocs.io/ +Documentation can be found at https://semiwrap.readthedocs.io/ Author ------ -Dustin Spicuzza is the primary author of robotpy-build, but it is the +Dustin Spicuzza is the primary author of semiwrap. + +Semiwrap is a direct decendant of the robotpy-build project, and is culmination of many years of experimentation with automated wrapper generation by members of the RobotPy community. -robotpy-build is available under the BSD 3-clause license. +semiwrap is available under the BSD 3-clause license. diff --git a/docs/conf.py b/docs/conf.py index c586ea3a..2c8c507e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ os.environ["GENERATING_DOCUMENTATION"] = "True" -__version__ = pkg_resources.get_distribution("robotpy-build").version +__version__ = pkg_resources.get_distribution("semiwrap").version # -- RTD configuration ------------------------------------------------ @@ -25,7 +25,7 @@ # -- Project information ----------------------------------------------------- -project = "robotpy-build" +project = "semiwrap" copyright = "2020, RobotPy Development Team" author = "RobotPy Development Team" diff --git a/pyproject.toml b/pyproject.toml index 84d755ed..0fc30918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,13 @@ requires = ["hatchling>=1.26.2", "hatch-vcs"] build-backend = "hatchling.build" [project] -name = "robotpy-build" +name = "semiwrap" dynamic = ["version"] -description = "Build tool for RobotPy projects" +description = "Semi-automated tool to wrap C++ code for use by python" readme = "README.md" requires-python = ">=3.8" license = "BSD-3-Clause" -license-files = ["LICENSE.txt", "robotpy_build/pybind11/LICENSE"] +license-files = ["LICENSE.txt", "src/semiwrap/pybind11/LICENSE"] authors = [ {name = "Dustin Spicuzza", email = "robotpy@googlegroups.com"}, ] @@ -41,39 +41,36 @@ dependencies = [ "distro; platform_system == 'Linux'", ] -[project.entry-points.robotpybuild] -robotpy-build = "robotpy_build.pkgcfg" - [project.scripts] -robotpy-build = "robotpy_build.tool.__main__:main" +semiwrap = "semiwrap.tool.__main__:main" [project.urls] -"Source code" = "https://github.com/robotpy/robotpy-build" +"Source code" = "https://github.com/robotpy/semiwrap" [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] -version-file = "robotpy_build/version.py" +version-file = "src/semiwrap/version.py" [tool.hatch.build.targets.sdist] -packages = ["robotpy_build"] +packages = ["src/semiwrap"] exclude = [ - "/robotpy_build/pybind11" + "/src/semiwrap/pybind11" ] [tool.hatch.build.targets.sdist.force-include] -"./robotpy_build/pybind11/include" = "./robotpy_build/pybind11/include" +"./src/semiwrap/pybind11/include" = "./semiwrap/pybind11/include" [tool.hatch.build.targets.wheel] -packages = ["robotpy_build"] +packages = ["semiwrap"] include = [ - "/robotpy_build/pybind11/include", - "/robotpy_build/include", + "/semiwrap/pybind11/include", + "/semiwrap/include", ] [tool.black] target-version = ["py38"] extend-exclude = ''' -^/robotpy_build/pybind11 +^/src/semiwrap/pybind11 ''' diff --git a/robotpy_build/__init__.py b/src/semiwrap/__init__.py similarity index 100% rename from robotpy_build/__init__.py rename to src/semiwrap/__init__.py diff --git a/robotpy_build/__main__.py b/src/semiwrap/__main__.py similarity index 100% rename from robotpy_build/__main__.py rename to src/semiwrap/__main__.py diff --git a/robotpy_build/autowrap/__init__.py b/src/semiwrap/autowrap/__init__.py similarity index 100% rename from robotpy_build/autowrap/__init__.py rename to src/semiwrap/autowrap/__init__.py diff --git a/robotpy_build/autowrap/buffer.py b/src/semiwrap/autowrap/buffer.py similarity index 100% rename from robotpy_build/autowrap/buffer.py rename to src/semiwrap/autowrap/buffer.py diff --git a/robotpy_build/autowrap/context.py b/src/semiwrap/autowrap/context.py similarity index 100% rename from robotpy_build/autowrap/context.py rename to src/semiwrap/autowrap/context.py diff --git a/robotpy_build/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py similarity index 99% rename from robotpy_build/autowrap/cxxparser.py rename to src/semiwrap/autowrap/cxxparser.py index ce976e29..c626e936 100644 --- a/robotpy_build/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -395,7 +395,7 @@ def on_template_inst(self, state: AWState, inst: TemplateInst) -> None: pass def on_variable(self, state: AWState, v: Variable) -> None: - # TODO: robotpy-build doesn't wrap global variables at this time + # TODO: semiwrap doesn't wrap global variables at this time pass def on_function(self, state: AWNonClassBlockState, fn: Function) -> None: diff --git a/robotpy_build/autowrap/generator_data.py b/src/semiwrap/autowrap/generator_data.py similarity index 100% rename from robotpy_build/autowrap/generator_data.py rename to src/semiwrap/autowrap/generator_data.py diff --git a/robotpy_build/autowrap/mangle.py b/src/semiwrap/autowrap/mangle.py similarity index 100% rename from robotpy_build/autowrap/mangle.py rename to src/semiwrap/autowrap/mangle.py diff --git a/robotpy_build/autowrap/render_cls_prologue.py b/src/semiwrap/autowrap/render_cls_prologue.py similarity index 100% rename from robotpy_build/autowrap/render_cls_prologue.py rename to src/semiwrap/autowrap/render_cls_prologue.py diff --git a/robotpy_build/autowrap/render_cls_rpy_include.py b/src/semiwrap/autowrap/render_cls_rpy_include.py similarity index 100% rename from robotpy_build/autowrap/render_cls_rpy_include.py rename to src/semiwrap/autowrap/render_cls_rpy_include.py diff --git a/robotpy_build/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py similarity index 100% rename from robotpy_build/autowrap/render_pybind11.py rename to src/semiwrap/autowrap/render_pybind11.py diff --git a/robotpy_build/autowrap/render_tmpl_inst.py b/src/semiwrap/autowrap/render_tmpl_inst.py similarity index 100% rename from robotpy_build/autowrap/render_tmpl_inst.py rename to src/semiwrap/autowrap/render_tmpl_inst.py diff --git a/robotpy_build/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py similarity index 100% rename from robotpy_build/autowrap/render_wrapped.py rename to src/semiwrap/autowrap/render_wrapped.py diff --git a/robotpy_build/autowrap/writer.py b/src/semiwrap/autowrap/writer.py similarity index 100% rename from robotpy_build/autowrap/writer.py rename to src/semiwrap/autowrap/writer.py diff --git a/robotpy_build/command/__init__.py b/src/semiwrap/command/__init__.py similarity index 100% rename from robotpy_build/command/__init__.py rename to src/semiwrap/command/__init__.py diff --git a/robotpy_build/command/_built_env.py b/src/semiwrap/command/_built_env.py similarity index 100% rename from robotpy_build/command/_built_env.py rename to src/semiwrap/command/_built_env.py diff --git a/robotpy_build/command/build_dl.py b/src/semiwrap/command/build_dl.py similarity index 100% rename from robotpy_build/command/build_dl.py rename to src/semiwrap/command/build_dl.py diff --git a/robotpy_build/command/build_ext.py b/src/semiwrap/command/build_ext.py similarity index 100% rename from robotpy_build/command/build_ext.py rename to src/semiwrap/command/build_ext.py diff --git a/robotpy_build/command/build_gen.py b/src/semiwrap/command/build_gen.py similarity index 100% rename from robotpy_build/command/build_gen.py rename to src/semiwrap/command/build_gen.py diff --git a/robotpy_build/command/build_py.py b/src/semiwrap/command/build_py.py similarity index 100% rename from robotpy_build/command/build_py.py rename to src/semiwrap/command/build_py.py diff --git a/robotpy_build/command/build_pyi.py b/src/semiwrap/command/build_pyi.py similarity index 100% rename from robotpy_build/command/build_pyi.py rename to src/semiwrap/command/build_pyi.py diff --git a/robotpy_build/command/develop.py b/src/semiwrap/command/develop.py similarity index 100% rename from robotpy_build/command/develop.py rename to src/semiwrap/command/develop.py diff --git a/robotpy_build/command/editable_wheel.py b/src/semiwrap/command/editable_wheel.py similarity index 100% rename from robotpy_build/command/editable_wheel.py rename to src/semiwrap/command/editable_wheel.py diff --git a/robotpy_build/command/update_init.py b/src/semiwrap/command/update_init.py similarity index 100% rename from robotpy_build/command/update_init.py rename to src/semiwrap/command/update_init.py diff --git a/robotpy_build/command/util.py b/src/semiwrap/command/util.py similarity index 100% rename from robotpy_build/command/util.py rename to src/semiwrap/command/util.py diff --git a/robotpy_build/config/__init__.py b/src/semiwrap/config/__init__.py similarity index 100% rename from robotpy_build/config/__init__.py rename to src/semiwrap/config/__init__.py diff --git a/robotpy_build/config/autowrap_yml.py b/src/semiwrap/config/autowrap_yml.py similarity index 99% rename from robotpy_build/config/autowrap_yml.py rename to src/semiwrap/config/autowrap_yml.py index bb0737d8..27414a7d 100644 --- a/robotpy_build/config/autowrap_yml.py +++ b/src/semiwrap/config/autowrap_yml.py @@ -143,7 +143,7 @@ class OverloadData: #: If specified, put the function in a sub.pack.age subpackage: Optional[str] = None - #: By default, robotpy-build will release the GIL whenever a wrapped + #: By default, semiwrap will release the GIL whenever a wrapped #: function is called. no_release_gil: Optional[bool] = None diff --git a/robotpy_build/config/dev_yml.py b/src/semiwrap/config/dev_yml.py similarity index 100% rename from robotpy_build/config/dev_yml.py rename to src/semiwrap/config/dev_yml.py diff --git a/robotpy_build/config/pyproject_toml.py b/src/semiwrap/config/pyproject_toml.py similarity index 100% rename from robotpy_build/config/pyproject_toml.py rename to src/semiwrap/config/pyproject_toml.py diff --git a/robotpy_build/config/util.py b/src/semiwrap/config/util.py similarity index 100% rename from robotpy_build/config/util.py rename to src/semiwrap/config/util.py diff --git a/robotpy_build/download.py b/src/semiwrap/download.py similarity index 100% rename from robotpy_build/download.py rename to src/semiwrap/download.py diff --git a/robotpy_build/include/gilsafe_object.h b/src/semiwrap/include/gilsafe_object.h similarity index 100% rename from robotpy_build/include/gilsafe_object.h rename to src/semiwrap/include/gilsafe_object.h diff --git a/robotpy_build/include/pybind11_typing.h b/src/semiwrap/include/pybind11_typing.h similarity index 100% rename from robotpy_build/include/pybind11_typing.h rename to src/semiwrap/include/pybind11_typing.h diff --git a/robotpy_build/include/robotpy_build.h b/src/semiwrap/include/robotpy_build.h similarity index 100% rename from robotpy_build/include/robotpy_build.h rename to src/semiwrap/include/robotpy_build.h diff --git a/robotpy_build/maven.py b/src/semiwrap/maven.py similarity index 100% rename from robotpy_build/maven.py rename to src/semiwrap/maven.py diff --git a/robotpy_build/overrides.py b/src/semiwrap/overrides.py similarity index 100% rename from robotpy_build/overrides.py rename to src/semiwrap/overrides.py diff --git a/robotpy_build/pkgcfg.py b/src/semiwrap/pkgcfg.py similarity index 100% rename from robotpy_build/pkgcfg.py rename to src/semiwrap/pkgcfg.py diff --git a/robotpy_build/pkgcfg_provider.py b/src/semiwrap/pkgcfg_provider.py similarity index 100% rename from robotpy_build/pkgcfg_provider.py rename to src/semiwrap/pkgcfg_provider.py diff --git a/robotpy_build/platforms.py b/src/semiwrap/platforms.py similarity index 97% rename from robotpy_build/platforms.py rename to src/semiwrap/platforms.py index dc474e12..237b3585 100644 --- a/robotpy_build/platforms.py +++ b/src/semiwrap/platforms.py @@ -99,7 +99,7 @@ def get_platform(name: typing.Optional[str] = None) -> WPILibMavenPlatform: try: return _platforms[name] except KeyError: - raise KeyError(f"platform {name} is not supported by robotpy-build!") + raise KeyError(f"platform {name} is not supported by semiwrap!") def get_platform_override_keys(platform: WPILibMavenPlatform): diff --git a/robotpy_build/pybind11 b/src/semiwrap/pybind11 similarity index 100% rename from robotpy_build/pybind11 rename to src/semiwrap/pybind11 diff --git a/robotpy_build/relink_libs.py b/src/semiwrap/relink_libs.py similarity index 100% rename from robotpy_build/relink_libs.py rename to src/semiwrap/relink_libs.py diff --git a/robotpy_build/setup.py b/src/semiwrap/setup.py similarity index 100% rename from robotpy_build/setup.py rename to src/semiwrap/setup.py diff --git a/robotpy_build/static_libs.py b/src/semiwrap/static_libs.py similarity index 100% rename from robotpy_build/static_libs.py rename to src/semiwrap/static_libs.py diff --git a/robotpy_build/tool/__init__.py b/src/semiwrap/tool/__init__.py similarity index 100% rename from robotpy_build/tool/__init__.py rename to src/semiwrap/tool/__init__.py diff --git a/robotpy_build/tool/__main__.py b/src/semiwrap/tool/__main__.py similarity index 100% rename from robotpy_build/tool/__main__.py rename to src/semiwrap/tool/__main__.py diff --git a/robotpy_build/tool/build_dep.py b/src/semiwrap/tool/build_dep.py similarity index 100% rename from robotpy_build/tool/build_dep.py rename to src/semiwrap/tool/build_dep.py diff --git a/robotpy_build/tool/create_gen.py b/src/semiwrap/tool/create_gen.py similarity index 100% rename from robotpy_build/tool/create_gen.py rename to src/semiwrap/tool/create_gen.py diff --git a/robotpy_build/tool/create_imports.py b/src/semiwrap/tool/create_imports.py similarity index 100% rename from robotpy_build/tool/create_imports.py rename to src/semiwrap/tool/create_imports.py diff --git a/robotpy_build/tool/parse_maven.py b/src/semiwrap/tool/parse_maven.py similarity index 100% rename from robotpy_build/tool/parse_maven.py rename to src/semiwrap/tool/parse_maven.py diff --git a/robotpy_build/tool/platform_info.py b/src/semiwrap/tool/platform_info.py similarity index 100% rename from robotpy_build/tool/platform_info.py rename to src/semiwrap/tool/platform_info.py diff --git a/robotpy_build/tool/scan_headers.py b/src/semiwrap/tool/scan_headers.py similarity index 100% rename from robotpy_build/tool/scan_headers.py rename to src/semiwrap/tool/scan_headers.py diff --git a/robotpy_build/tool/show_override.py b/src/semiwrap/tool/show_override.py similarity index 100% rename from robotpy_build/tool/show_override.py rename to src/semiwrap/tool/show_override.py diff --git a/robotpy_build/tool/util.py b/src/semiwrap/tool/util.py similarity index 100% rename from robotpy_build/tool/util.py rename to src/semiwrap/tool/util.py diff --git a/robotpy_build/wrapper.py b/src/semiwrap/wrapper.py similarity index 100% rename from robotpy_build/wrapper.py rename to src/semiwrap/wrapper.py From 8c50a1cbed0c09ffc2328364b00e9e294a567800 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 6 May 2025 01:37:13 -0400 Subject: [PATCH 05/11] Large refactor to be a modular hatchling + meson project --- pyproject.toml | 26 +- src/semiwrap/autowrap/context.py | 10 + src/semiwrap/autowrap/cxxparser.py | 30 +- src/semiwrap/autowrap/generator_data.py | 10 +- src/semiwrap/autowrap/render_cls_prologue.py | 6 +- ...nclude.py => render_cls_trampoline_hpp.py} | 39 +- src/semiwrap/autowrap/render_pybind11.py | 2 +- src/semiwrap/autowrap/render_tmpl_inst.py | 14 +- src/semiwrap/autowrap/render_wrapped.py | 14 +- src/semiwrap/autowrap/writer.py | 74 -- src/semiwrap/casters.py | 55 ++ src/semiwrap/{command => cmd}/__init__.py | 0 src/semiwrap/cmd/dat2cpp.py | 35 + src/semiwrap/cmd/dat2tmplcpp.py | 41 + src/semiwrap/cmd/dat2tmplhpp.py | 33 + src/semiwrap/cmd/dat2trampoline.py | 65 ++ src/semiwrap/cmd/gen_libinit.py | 53 ++ src/semiwrap/cmd/gen_modinit_hpp.py | 114 +++ src/semiwrap/cmd/gen_pkgconf.py | 46 + src/semiwrap/cmd/header2dat.py | 140 +++ src/semiwrap/cmd/make_pyi.py | 119 +++ src/semiwrap/cmd/publish_casters.py | 95 ++ src/semiwrap/cmd/resolve_casters.py | 71 ++ src/semiwrap/command/_built_env.py | 79 -- src/semiwrap/command/build_dl.py | 80 -- src/semiwrap/command/build_ext.py | 260 ------ src/semiwrap/command/build_gen.py | 35 - src/semiwrap/command/build_py.py | 23 - src/semiwrap/command/build_pyi.py | 90 -- src/semiwrap/command/develop.py | 10 - src/semiwrap/command/editable_wheel.py | 9 - src/semiwrap/command/update_init.py | 73 -- src/semiwrap/command/util.py | 18 - src/semiwrap/config/autowrap_yml.py | 105 ++- src/semiwrap/config/dev_yml.py | 31 - src/semiwrap/config/pyproject_toml.py | 420 ++------- src/semiwrap/config/util.py | 6 +- src/semiwrap/depfile.py | 32 + src/semiwrap/download.py | 113 --- src/semiwrap/hooks.py | 108 +++ src/semiwrap/include/gilsafe_object.h | 10 +- .../include/{robotpy_build.h => semiwrap.h} | 40 +- src/semiwrap/makeplan.py | 660 ++++++++++++++ src/semiwrap/maven.py | 67 -- src/semiwrap/mkpc.py | 51 ++ src/semiwrap/overrides.py | 67 -- src/semiwrap/pkgcfg.py | 67 -- src/semiwrap/pkgcfg_provider.py | 207 ----- src/semiwrap/pkgconf_cache.py | 116 +++ src/semiwrap/platforms.py | 111 --- src/semiwrap/pyproject.py | 73 ++ src/semiwrap/relink_libs.py | 177 ---- src/semiwrap/render_meson.py | 454 ++++++++++ src/semiwrap/semiwrap-pybind11.pc | 8 + src/semiwrap/semiwrap.pc | 9 + src/semiwrap/semiwrap.pybind11.json | 66 ++ src/semiwrap/setup.py | 226 ----- src/semiwrap/static_libs.py | 114 --- src/semiwrap/util.py | 27 + src/semiwrap/wrapper.py | 829 ------------------ 60 files changed, 2763 insertions(+), 3200 deletions(-) rename src/semiwrap/autowrap/{render_cls_rpy_include.py => render_cls_trampoline_hpp.py} (90%) delete mode 100644 src/semiwrap/autowrap/writer.py create mode 100644 src/semiwrap/casters.py rename src/semiwrap/{command => cmd}/__init__.py (100%) create mode 100644 src/semiwrap/cmd/dat2cpp.py create mode 100644 src/semiwrap/cmd/dat2tmplcpp.py create mode 100644 src/semiwrap/cmd/dat2tmplhpp.py create mode 100644 src/semiwrap/cmd/dat2trampoline.py create mode 100644 src/semiwrap/cmd/gen_libinit.py create mode 100644 src/semiwrap/cmd/gen_modinit_hpp.py create mode 100644 src/semiwrap/cmd/gen_pkgconf.py create mode 100644 src/semiwrap/cmd/header2dat.py create mode 100644 src/semiwrap/cmd/make_pyi.py create mode 100644 src/semiwrap/cmd/publish_casters.py create mode 100644 src/semiwrap/cmd/resolve_casters.py delete mode 100644 src/semiwrap/command/_built_env.py delete mode 100644 src/semiwrap/command/build_dl.py delete mode 100644 src/semiwrap/command/build_ext.py delete mode 100644 src/semiwrap/command/build_gen.py delete mode 100644 src/semiwrap/command/build_py.py delete mode 100644 src/semiwrap/command/build_pyi.py delete mode 100644 src/semiwrap/command/develop.py delete mode 100644 src/semiwrap/command/editable_wheel.py delete mode 100644 src/semiwrap/command/update_init.py delete mode 100644 src/semiwrap/command/util.py delete mode 100644 src/semiwrap/config/dev_yml.py create mode 100644 src/semiwrap/depfile.py delete mode 100644 src/semiwrap/download.py create mode 100644 src/semiwrap/hooks.py rename src/semiwrap/include/{robotpy_build.h => semiwrap.h} (78%) create mode 100644 src/semiwrap/makeplan.py delete mode 100644 src/semiwrap/maven.py create mode 100644 src/semiwrap/mkpc.py delete mode 100644 src/semiwrap/overrides.py delete mode 100644 src/semiwrap/pkgcfg.py delete mode 100644 src/semiwrap/pkgcfg_provider.py create mode 100644 src/semiwrap/pkgconf_cache.py delete mode 100644 src/semiwrap/platforms.py create mode 100644 src/semiwrap/pyproject.py delete mode 100644 src/semiwrap/relink_libs.py create mode 100644 src/semiwrap/render_meson.py create mode 100644 src/semiwrap/semiwrap-pybind11.pc create mode 100644 src/semiwrap/semiwrap.pc create mode 100644 src/semiwrap/semiwrap.pybind11.json delete mode 100644 src/semiwrap/setup.py delete mode 100644 src/semiwrap/static_libs.py create mode 100644 src/semiwrap/util.py delete mode 100644 src/semiwrap/wrapper.py diff --git a/pyproject.toml b/pyproject.toml index 0fc30918..230902b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,29 +18,28 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Topic :: Software Development", ] dependencies = [ - "setuptools >= 45", - "setuptools_scm >= 6.2, < 8", + "pkgconf", "sphinxify >= 0.7.3", "validobj ~= 1.2", - "cxxheaderparser[pcpp] ~= 1.4.1", + "cxxheaderparser[pcpp] ~= 1.5", "tomli", "tomli_w", "toposort", "typing-extensions", "pyyaml >= 5.1", - "patch == 1.*", "pybind11-stubgen ~= 2.5.1", - "delocate; platform_system == 'Darwin'", "distro; platform_system == 'Linux'", ] +[project.entry-points.hatch] +semiwrap = "semiwrap.hooks" + +[project.entry-points.pkg_config] +semiwrap = "semiwrap" + [project.scripts] semiwrap = "semiwrap.tool.__main__:main" @@ -50,7 +49,7 @@ semiwrap = "semiwrap.tool.__main__:main" [tool.hatch.version] source = "vcs" -[tool.hatch.build.hooks.vcs] +[tool.hatch.build.targets.sdist.hooks.vcs] version-file = "src/semiwrap/version.py" [tool.hatch.build.targets.sdist] @@ -61,13 +60,6 @@ exclude = [ [tool.hatch.build.targets.sdist.force-include] "./src/semiwrap/pybind11/include" = "./semiwrap/pybind11/include" -[tool.hatch.build.targets.wheel] -packages = ["semiwrap"] -include = [ - "/semiwrap/pybind11/include", - "/semiwrap/include", -] - [tool.black] target-version = ["py38"] diff --git a/src/semiwrap/autowrap/context.py b/src/semiwrap/autowrap/context.py index 94afdd5d..c209b39d 100644 --- a/src/semiwrap/autowrap/context.py +++ b/src/semiwrap/autowrap/context.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field import enum +import pathlib import typing from cxxheaderparser.types import Function, PQName @@ -372,6 +373,9 @@ class ClassContext: parent: typing.Optional["ClassContext"] + #: lookup key for dat2trampoline + yml_id: str + #: Namespace that this class lives in namespace: str @@ -514,6 +518,9 @@ class HeaderContext: # Name in toml hname: str + # config file for this + orig_yaml: pathlib.Path + extra_includes_first: typing.List[str] extra_includes: typing.List[str] inline_code: typing.Optional[str] @@ -535,6 +542,9 @@ class HeaderContext: # same as classes, but only those that have trampolines classes_with_trampolines: typing.List[ClassContext] = field(default_factory=list) + # for diagnostic purposes + ignored_classes: typing.List[str] = field(default_factory=list) + functions: typing.List[FunctionContext] = field(default_factory=list) # trampolines diff --git a/src/semiwrap/autowrap/cxxparser.py b/src/semiwrap/autowrap/cxxparser.py index c626e936..78f0d283 100644 --- a/src/semiwrap/autowrap/cxxparser.py +++ b/src/semiwrap/autowrap/cxxparser.py @@ -58,6 +58,7 @@ Variable, ) +from ..casters import CastersData from ..config.autowrap_yml import ( AutowrapConfigYaml, BufferData, @@ -87,18 +88,24 @@ TrampolineData, ) +from ..util import relpath_walk_up + class HasSubpackage(Protocol): - subpackage: typing.Optional[str] + @property + def subpackage(self) -> typing.Optional[str]: ... class HasDoc(Protocol): - doc: str - doc_append: str + @property + def doc(self) -> typing.Optional[str]: ... + @property + def doc_append(self) -> typing.Optional[str]: ... class HasNameData(Protocol): - rename: str + @property + def rename(self) -> typing.Optional[str]: ... # TODO: this isn't the best solution @@ -329,7 +336,7 @@ def __init__( self, hctx: HeaderContext, gendata: GeneratorData, - casters: typing.Dict[str, typing.Dict[str, typing.Any]], + casters: CastersData, report_only: bool, ) -> None: self.hctx = hctx @@ -591,6 +598,7 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]: # Ignore explicitly ignored classes (including default-ignore) if class_data.ignore: + self.hctx.ignored_classes.append(cls_key) return False if missing and not self.report_only: @@ -721,6 +729,7 @@ def on_class_start(self, state: AWClassBlockState) -> typing.Optional[bool]: ctx = ClassContext( parent=parent_ctx, + yml_id=cls_key, namespace=cls_namespace, cpp_name=cls_name, full_cpp_name=cls_qualname, @@ -1897,8 +1906,8 @@ def _add_default_arg_cast( typename = _fmt_nameonly(ntype.typename) if typename: ccfg = self.casters.get(typename) - if ccfg and ccfg.get("darg"): - found_typename = ccfg["typename"] + if ccfg and ccfg.default_arg_cast: + found_typename = ccfg.typename name = f"({found_typename}){name}" return name @@ -1974,7 +1983,7 @@ def _set_type_caster_includes(self): for typename in self.types: ccfg = casters.get(typename) if ccfg: - includes.add(ccfg["hdr"]) + includes.add(ccfg.header) self.hctx.type_caster_includes = sorted(includes) @@ -1985,7 +1994,7 @@ def parse_header( header_root: pathlib.Path, gendata: GeneratorData, parser_options: ParserOptions, - casters: typing.Dict[str, typing.Dict[str, typing.Any]], + casters: CastersData, report_only: bool, ) -> HeaderContext: user_cfg = gendata.data @@ -1993,10 +2002,11 @@ def parse_header( # Initialize the header context with user configuration hctx = HeaderContext( hname=name, + orig_yaml=gendata.data_fname, extra_includes_first=user_cfg.extra_includes_first, extra_includes=user_cfg.extra_includes, inline_code=user_cfg.inline_code, - rel_fname=str(header_path.relative_to(header_root)), + rel_fname=relpath_walk_up(header_path, header_root).as_posix(), ) # Parse the header using a custom visitor diff --git a/src/semiwrap/autowrap/generator_data.py b/src/semiwrap/autowrap/generator_data.py index 7cb69d90..72f6f0be 100644 --- a/src/semiwrap/autowrap/generator_data.py +++ b/src/semiwrap/autowrap/generator_data.py @@ -13,7 +13,8 @@ from cxxheaderparser.types import Function import dataclasses -from typing import Dict, List, Optional, Tuple +import pathlib +from typing import Any, Dict, List, Optional, Tuple @dataclasses.dataclass @@ -66,7 +67,7 @@ class GeneratorData: data: AutowrapConfigYaml - def __init__(self, data: AutowrapConfigYaml, data_fname: str): + def __init__(self, data: AutowrapConfigYaml, data_fname: pathlib.Path): self.data = data self.data_fname = data_fname @@ -202,7 +203,7 @@ def get_prop_data(self, name) -> PropData: return data - def report_missing(self, name: str, reporter: "MissingReporter"): + def report_missing(self, name: pathlib.Path, reporter: "MissingReporter"): """ Generate a structure that can be copy/pasted into the generation data yaml and print it out if there's missing data @@ -356,6 +357,9 @@ class MissingReporter: def __init__(self): self.reports = {} + def __bool__(self) -> bool: + return len(self.reports) > 0 + def _merge(self, src, dst): for k, v in src.items(): if isinstance(v, dict): diff --git a/src/semiwrap/autowrap/render_cls_prologue.py b/src/semiwrap/autowrap/render_cls_prologue.py index 98d0ce64..6a75d744 100644 --- a/src/semiwrap/autowrap/render_cls_prologue.py +++ b/src/semiwrap/autowrap/render_cls_prologue.py @@ -6,7 +6,7 @@ def render_class_prologue(r: RenderBuffer, hctx: HeaderContext): # fmt: off r.writeln( "// This file is autogenerated. DO NOT EDIT\n" - "#include " + "#include " ) # fmt: on @@ -36,6 +36,6 @@ def render_class_prologue(r: RenderBuffer, hctx: HeaderContext): for cls in hctx.classes_with_trampolines: r.writeln() r.writeln( - f"#define RPYGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" + f"#define SWGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" ) - r.writeln(f"#include ") + r.writeln(f"#include ") diff --git a/src/semiwrap/autowrap/render_cls_rpy_include.py b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py similarity index 90% rename from src/semiwrap/autowrap/render_cls_rpy_include.py rename to src/semiwrap/autowrap/render_cls_trampoline_hpp.py index c1e169ad..cfb0c6e3 100644 --- a/src/semiwrap/autowrap/render_cls_rpy_include.py +++ b/src/semiwrap/autowrap/render_cls_trampoline_hpp.py @@ -23,9 +23,9 @@ def using_signature(cls: ClassContext, fn: FunctionContext) -> str: return f"{cls.full_cpp_name_identifier}_{fn.cpp_name}" -def render_cls_rpy_include_hpp(ctx: HeaderContext, cls: ClassContext) -> str: +def render_cls_trampoline_hpp(ctx: HeaderContext, cls: ClassContext) -> str: """ - Pieces that go into an rpy-include file for a class + Pieces that go into an trampoline file for a class - Trampoline base class (if applicable) - Template constructors/method fillers (if applicable) @@ -36,7 +36,7 @@ def render_cls_rpy_include_hpp(ctx: HeaderContext, cls: ClassContext) -> str: "// This file is autogenerated. DO NOT EDIT\n" "\n" "#pragma once\n" - "#include " + "#include " ) if ctx.extra_includes_first: @@ -58,6 +58,11 @@ def render_cls_rpy_include_hpp(ctx: HeaderContext, cls: ClassContext) -> str: if cls.template is not None: _render_cls_template_impl(r, ctx, cls, cls.template) + if not cls.template and not cls.trampoline: + # TODO: maybe replace this with something that triggers an error + # if the trampoline is used? and a better error message + r.writeln(f'#error "{cls.full_cpp_name} does not have a trampoline"') + return r.getvalue() @@ -80,7 +85,7 @@ def _render_cls_trampoline( Each trampoline type is placed in a different namespace to make our life easier. - Trampoline functions can be disabled via RPY_DISABLE_name_[type_type..] + Trampoline functions can be disabled via SWGEN_DISABLE_name_[type_type..] """ if cls.template: @@ -94,13 +99,13 @@ def _render_cls_trampoline( if trampoline.methods_to_disable: r.writeln() for fn in trampoline.methods_to_disable: - r.writeln(f"#define RPYGEN_DISABLE_{ trampoline_signature(fn) }") + r.writeln(f"#define SWGEN_DISABLE_{ trampoline_signature(fn) }") # include override files for each base -- TODO: exclude some bases? if cls.bases: r.writeln() for base in cls.bases: - r.writeln(f"#include ") + r.writeln(f"#include ") if cls.namespace: r.writeln(f"\nnamespace {cls.namespace.strip('::')} {{") @@ -121,7 +126,7 @@ def _render_cls_trampoline( # r.writeln( - f"\ntemplate <{postcomma(template_parameter_list)}typename CfgBase = rpygen::EmptyTrampolineCfg>" + f"\ntemplate <{postcomma(template_parameter_list)}typename CfgBase = swgen::EmptyTrampolineCfg>" ) if cls.bases: @@ -228,7 +233,7 @@ def _render_cls_trampoline( for fn in trampoline.protected_constructors: r.writeln( - f"\n#ifdef RPYGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" + f"\n#ifdef SWGEN_ENABLE_{cls.full_cpp_name_identifier}_PROTECTED_CONSTRUCTORS" ) with r.indent(): all_decls = ", ".join(p.decl for p in fn.all_params) @@ -257,18 +262,18 @@ def _render_cls_trampoline( # for fn in trampoline.non_virtual_protected_methods: - r.writeln(f"\n#ifndef RPYBLD_DISABLE_{ trampoline_signature(fn) }") + r.writeln(f"\n#ifndef SWGEN_DISABLE_{ trampoline_signature(fn) }") # hack to ensure we don't do 'using' twice' in the same class, while # also ensuring that the overrides can be selectively disabled by # child trampoline functions with r.indent(): - r.writeln(f"#ifndef RPYBLD_UDISABLE_{ using_signature(cls, fn) }") + r.writeln(f"#ifndef SWGEN_UDISABLE_{ using_signature(cls, fn) }") with r.indent(): r.write_trim( f""" using { cls.full_cpp_name }::{ fn.cpp_name }; - #define RPYBLD_UDISABLE_{ using_signature(cls, fn) } + #define SWGEN_UDISABLE_{ using_signature(cls, fn) } """ ) r.writeln("#endif") @@ -292,7 +297,7 @@ def _render_cls_trampoline( def _render_cls_trampoline_virtual_method( r: RenderBuffer, cls: ClassContext, fn: FunctionContext ): - r.writeln(f"\n#ifndef RPYGEN_DISABLE_{ trampoline_signature(fn) }") + r.writeln(f"\n#ifndef SWGEN_DISABLE_{ trampoline_signature(fn) }") with r.indent(): all_decls = ", ".join(p.decl for p in fn.all_params) @@ -325,14 +330,14 @@ def _render_cls_trampoline_virtual_method( if fn.is_pure_virtual and fn.virtual_xform: r.write_trim( f""" - RPYBUILD_OVERRIDE_PURE_CUSTOM_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + SEMIWRAP_OVERRIDE_PURE_CUSTOM_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, "{fn.py_name}", {fn.cpp_name}, {all_names}); """ ) elif fn.is_pure_virtual: r.write_trim( f""" - RPYBUILD_OVERRIDE_PURE_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + SEMIWRAP_OVERRIDE_PURE_NAME({cls.cpp_name}, PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, "{fn.py_name}", {fn.cpp_name}, {all_names}); """ ) @@ -340,7 +345,7 @@ def _render_cls_trampoline_virtual_method( r.write_trim( f""" using CxxCallBase = typename PyTrampolineCfg::override_base_{trampoline_signature(fn)}; - RPYBUILD_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, + SEMIWRAP_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE({fn.cpp_return_type}), LookupBase, "{fn.py_name}", {fn.cpp_name}, {all_names}); return CxxCallBase::{fn.cpp_name}({all_vnames}); """ @@ -367,7 +372,7 @@ def _render_cls_template_impl( for inc in hctx.type_caster_includes: r.writeln(f"#include <{inc}>") - r.writeln("\nnamespace rpygen {") + r.writeln("\nnamespace swgen {") if cls.namespace: r.writeln(f"\nusing namespace {cls.namespace};") @@ -432,6 +437,6 @@ def _render_cls_template_impl( f""" }}; // struct bind_{cls.full_cpp_name_identifier} - }}; // namespace rpygen + }}; // namespace swgen """ ) diff --git a/src/semiwrap/autowrap/render_pybind11.py b/src/semiwrap/autowrap/render_pybind11.py index 483bb74d..b22c589d 100644 --- a/src/semiwrap/autowrap/render_pybind11.py +++ b/src/semiwrap/autowrap/render_pybind11.py @@ -330,7 +330,7 @@ def cls_decl(r: RenderBuffer, cls: ClassContext): """ ) r.writeln( - f'static_assert(std::is_abstract<{tctx.var}>::value == false, "{cls.full_cpp_name} " RPYBUILD_BAD_TRAMPOLINE);' + f'static_assert(std::is_abstract<{tctx.var}>::value == false, "{cls.full_cpp_name} " SEMIWRAP_BAD_TRAMPOLINE);' ) class_params = [f"typename {cls.full_cpp_name}"] diff --git a/src/semiwrap/autowrap/render_tmpl_inst.py b/src/semiwrap/autowrap/render_tmpl_inst.py index b8bfe270..b935d18d 100644 --- a/src/semiwrap/autowrap/render_tmpl_inst.py +++ b/src/semiwrap/autowrap/render_tmpl_inst.py @@ -14,12 +14,12 @@ def render_template_inst_cpp( r.write_trim( f""" - #include + #include #include "{ hctx.hname }_tmpl.hpp" - namespace rpygen {{ + namespace swgen {{ - using BindType = rpygen::bind_{ tmpl_data.full_cpp_name_identifier }<{tmpl_params}>; + using BindType = swgen::bind_{ tmpl_data.full_cpp_name_identifier }<{tmpl_params}>; static std::unique_ptr inst; { tmpl_data.binder_typename }::{ tmpl_data.binder_typename }(py::module &m, const char * clsName) @@ -33,7 +33,7 @@ def render_template_inst_cpp( inst.reset(); }} - }}; // namespace rpygen + }}; // namespace swgen """ ) r.writeln() @@ -46,9 +46,9 @@ def render_template_inst_hpp(hctx: HeaderContext) -> str: f""" // This file is autogenerated. DO NOT EDIT #pragma once - #include + #include - namespace rpygen {{ + namespace swgen {{ """ ) @@ -63,5 +63,5 @@ def render_template_inst_hpp(hctx: HeaderContext) -> str: """ ) - r.writeln("\n}; // namespace rpygen") + r.writeln("\n}; // namespace swgen") return r.getvalue() diff --git a/src/semiwrap/autowrap/render_wrapped.py b/src/semiwrap/autowrap/render_wrapped.py index 7ed29616..21d56217 100644 --- a/src/semiwrap/autowrap/render_wrapped.py +++ b/src/semiwrap/autowrap/render_wrapped.py @@ -52,7 +52,7 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: for ns in hctx.namespaces: r.writeln(f"using namespace {ns};") - r.writeln(f"\nstruct rpybuild_{hctx.hname}_initializer {{\n") + r.writeln(f"\nstruct semiwrap_{hctx.hname}_initializer {{\n") with r.indent(): for cls in hctx.classes: @@ -72,7 +72,7 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: # template decls for tmpl_data in hctx.template_instances: if not tmpl_data.matched: - r.writeln(f"rpygen::{tmpl_data.binder_typename} {tmpl_data.var_name};") + r.writeln(f"swgen::{tmpl_data.binder_typename} {tmpl_data.var_name};") # class decls for cls in hctx.classes: @@ -83,11 +83,11 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: r.writeln() for tmpl_data in cls.template.instances: r.writeln( - f"rpygen::{tmpl_data.binder_typename} {tmpl_data.var_name};" + f"swgen::{tmpl_data.binder_typename} {tmpl_data.var_name};" ) r.writeln("\npy::module &m;\n") - r.writeln(f"rpybuild_{hctx.hname}_initializer(py::module &m) :") + r.writeln(f"semiwrap_{hctx.hname}_initializer(py::module &m) :") with r.indent(): for pkg, vname in hctx.subpackages.items(): @@ -179,12 +179,12 @@ def render_wrapped_cpp(hctx: HeaderContext) -> str: r.writeln("}") r.writeln( - f"}}; // struct rpybuild_{hctx.hname}_initializer\n" + f"}}; // struct semiwrap_{hctx.hname}_initializer\n" "\n" - f"static std::unique_ptr cls;\n" + f"static std::unique_ptr cls;\n" "\n" f"void begin_init_{hctx.hname}(py::module &m) {{\n" - f" cls = std::make_unique(m);\n" + f" cls = std::make_unique(m);\n" "}\n" "\n" f"void finish_init_{hctx.hname}() {{\n" diff --git a/src/semiwrap/autowrap/writer.py b/src/semiwrap/autowrap/writer.py deleted file mode 100644 index 3b06da62..00000000 --- a/src/semiwrap/autowrap/writer.py +++ /dev/null @@ -1,74 +0,0 @@ -import json -import os -from os.path import join -import pprint -import typing - -from .context import HeaderContext -from .render_wrapped import render_wrapped_cpp -from .render_cls_rpy_include import render_cls_rpy_include_hpp -from .render_tmpl_inst import render_template_inst_cpp, render_template_inst_hpp - -_emit_j2_debug = os.getenv("RPYBUILD_J2_DEBUG") == "1" - - -class WrapperWriter: - - def write_files( - self, - hctx: HeaderContext, - name: str, - cxx_gen_dir: str, - hppoutdir: str, - classdeps_json_fname: str, - ) -> typing.List[str]: - """Generates all files needed for a single processed header""" - - generated_sources: typing.List[str] = [] - - # Jinja requires input as a dictionary - data = hctx.__dict__ - - if _emit_j2_debug: - with open(join(cxx_gen_dir, f"{name}.txt"), "w") as fp: - fp.write(pprint.pformat(hctx)) - - # Write the cpp file first - fname = join(cxx_gen_dir, f"{name}.cpp") - generated_sources.append(fname) - with open(fname, "w", encoding="utf-8") as fp: - fp.write(render_wrapped_cpp(hctx)) - - # Then the json - with open(classdeps_json_fname, "w", encoding="utf-8") as fp: - json.dump(hctx.class_hierarchy, fp) - - # Generate an rpy-include file for each class that has either a trampoline - # or a template class - for cls in hctx.classes: - if not cls.template and not cls.trampoline: - continue - - fname = join( - hppoutdir, f"{cls.namespace.replace(':', '_')}__{cls.cpp_name}.hpp" - ) - with open(fname, "w", encoding="utf-8") as fp: - fp.write(render_cls_rpy_include_hpp(hctx, cls)) - - # Each class template is instantiated in a separate cpp file to lessen - # compiler memory requirements when compiling obnoxious templates - if hctx.template_instances: - # Single header output that holds all the struct outlines - fname = join(cxx_gen_dir, f"{name}_tmpl.hpp") - with open(fname, "w", encoding="utf-8") as fp: - fp.write(render_template_inst_hpp(hctx)) - - # Each cpp file has a single class template instance - for i, tmpl_data in enumerate(hctx.template_instances): - data["tmpl_data"] = tmpl_data - fname = join(hppoutdir, f"{name}_tmpl{i+1}.cpp") - generated_sources.append(fname) - with open(fname, "w", encoding="utf-8") as fp: - fp.write(render_template_inst_cpp(hctx, tmpl_data)) - - return generated_sources diff --git a/src/semiwrap/casters.py b/src/semiwrap/casters.py new file mode 100644 index 00000000..1da280ef --- /dev/null +++ b/src/semiwrap/casters.py @@ -0,0 +1,55 @@ +import dataclasses +import json +import pathlib +import typing as T + +from .config.util import parse_input + +PKGCONF_CASTER_EXT = ".pybind11.json" + +# +# JSON caster data +# + + +@dataclasses.dataclass +class TypeCasterJsonHeader: + header: str + types: T.List[str] + default_arg_cast: bool = False + + +#: content of .pybind11.json +@dataclasses.dataclass +class TypeCasterJsonData: + """ + Stored in *.pybind11.json + """ + + headers: T.List[TypeCasterJsonHeader] = dataclasses.field(default_factory=list) + + +def load_typecaster_json_data(fname) -> TypeCasterJsonData: + with open(fname, encoding="utf-8") as fp: + return parse_input(json.load(fp), TypeCasterJsonData, fname) + + +def save_typecaster_json_data(fname: pathlib.Path, data: TypeCasterJsonData): + with open(fname, "w", encoding="utf-8") as fp: + json.dump(dataclasses.asdict(data), fp) + + +# +# Pickled caster lookup as stored by resolve_casters +# + + +@dataclasses.dataclass +class TypeData: + header: pathlib.Path + typename: str + default_arg_cast: bool + + +#: content of pickle file used internally +CastersData = T.Dict[str, TypeData] diff --git a/src/semiwrap/command/__init__.py b/src/semiwrap/cmd/__init__.py similarity index 100% rename from src/semiwrap/command/__init__.py rename to src/semiwrap/cmd/__init__.py diff --git a/src/semiwrap/cmd/dat2cpp.py b/src/semiwrap/cmd/dat2cpp.py new file mode 100644 index 00000000..391a3156 --- /dev/null +++ b/src/semiwrap/cmd/dat2cpp.py @@ -0,0 +1,35 @@ +""" +Creates an output .cpp file from a .dat file created by parsing a header +""" + +import inspect +import pathlib +import pickle +import sys + +from ..autowrap.context import HeaderContext +from ..autowrap.render_wrapped import render_wrapped_cpp + + +def _write_wrapper_cpp(input_dat: pathlib.Path, output_cpp: pathlib.Path): + with open(input_dat, "rb") as fp: + hctx = pickle.load(fp) + + assert isinstance(hctx, HeaderContext) + + content = render_wrapped_cpp(hctx) + output_cpp.write_text(content, encoding="utf-8") + + +def main(): + try: + _, input_dat, output_cpp = sys.argv + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + _write_wrapper_cpp(pathlib.Path(input_dat), pathlib.Path(output_cpp)) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/dat2tmplcpp.py b/src/semiwrap/cmd/dat2tmplcpp.py new file mode 100644 index 00000000..a162cff6 --- /dev/null +++ b/src/semiwrap/cmd/dat2tmplcpp.py @@ -0,0 +1,41 @@ +""" +Creates a template instance .cpp file from a .dat file created by parsing a header +""" + +import inspect +import pathlib +import pickle +import sys + +from ..autowrap.context import HeaderContext +from ..autowrap.render_tmpl_inst import render_template_inst_cpp + + +def _write_wrapper_cpp(input_dat: pathlib.Path, py_name: str, output_cpp: pathlib.Path): + with open(input_dat, "rb") as fp: + hctx = pickle.load(fp) + + assert isinstance(hctx, HeaderContext) + + for tmpl in hctx.template_instances: + if tmpl.py_name == py_name: + break + else: + raise ValueError(f"internal error: cannot find {py_name} in {hctx.orig_yaml}") + + content = render_template_inst_cpp(hctx, tmpl) + output_cpp.write_text(content, encoding="utf-8") + + +def main(): + try: + _, input_dat, py_name, output_cpp = sys.argv + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + _write_wrapper_cpp(pathlib.Path(input_dat), py_name, pathlib.Path(output_cpp)) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/dat2tmplhpp.py b/src/semiwrap/cmd/dat2tmplhpp.py new file mode 100644 index 00000000..c28b00e2 --- /dev/null +++ b/src/semiwrap/cmd/dat2tmplhpp.py @@ -0,0 +1,33 @@ +""" """ + +import inspect +import pathlib +import pickle +import sys + +from ..autowrap.context import HeaderContext +from ..autowrap.render_tmpl_inst import render_template_inst_hpp + + +def _write_tmpl_hpp(input_dat: pathlib.Path, output_hpp: pathlib.Path): + with open(input_dat, "rb") as fp: + hctx = pickle.load(fp) + + assert isinstance(hctx, HeaderContext) + + content = render_template_inst_hpp(hctx) + output_hpp.write_text(content, encoding="utf-8") + + +def main(): + try: + _, input_dat, output_hpp = sys.argv + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + _write_tmpl_hpp(pathlib.Path(input_dat), pathlib.Path(output_hpp)) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/dat2trampoline.py b/src/semiwrap/cmd/dat2trampoline.py new file mode 100644 index 00000000..e089a5e6 --- /dev/null +++ b/src/semiwrap/cmd/dat2trampoline.py @@ -0,0 +1,65 @@ +""" +Creates an output .hpp file from a .dat file created by parsing a header +""" + +import inspect +import pathlib +import pickle +import sys + +from ..autowrap.context import HeaderContext, ClassContext +from ..autowrap.render_cls_trampoline_hpp import render_cls_trampoline_hpp + + +def _get_classes(hctx: HeaderContext): + def _get_child_classes(c: ClassContext): + for c in c.child_classes: + yield c + _get_child_classes(c) + + for cls in hctx.classes: + yield cls + yield from _get_child_classes(cls) + + +def _write_wrapper_cpp(input_dat: pathlib.Path, yml_id: str, output_hpp: pathlib.Path): + with open(input_dat, "rb") as fp: + hctx = pickle.load(fp) + + assert isinstance(hctx, HeaderContext) + + avail = [] + for cls in _get_classes(hctx): + avail.append(cls.yml_id) + if cls.yml_id == yml_id: + break + else: + msg = [ + f"cannot find {yml_id} in {hctx.rel_fname}", + f"- config: {hctx.orig_yaml}", + ] + + if avail: + msg.append("- found " + ", ".join(avail)) + + if hctx.ignored_classes: + msg.append("- ignored " + ", ".join(hctx.ignored_classes)) + + raise ValueError("\n".join(msg)) + + content = render_cls_trampoline_hpp(hctx, cls) + output_hpp.write_text(content, encoding="utf-8") + + +def main(): + try: + _, input_dat, yml_id, output_hpp = sys.argv + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + _write_wrapper_cpp(pathlib.Path(input_dat), yml_id, pathlib.Path(output_hpp)) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/gen_libinit.py b/src/semiwrap/cmd/gen_libinit.py new file mode 100644 index 00000000..d8ebd44c --- /dev/null +++ b/src/semiwrap/cmd/gen_libinit.py @@ -0,0 +1,53 @@ +""" +Generates a file that loads shared library dependencies for the module + +Arguments: pyproject.toml package.name /path/to/_init_name.py +""" + +from __future__ import annotations + +import inspect +import pathlib +import sys +import typing as T + +from ..autowrap.buffer import RenderBuffer + + +def _write_libinit_py( + init_py: pathlib.Path, + modules: T.List[str], +) -> None: + """ + :param init_py: the _init module for the library(ies) that is written out + :param modules: the python modules to import + """ + + r = RenderBuffer() + + r.writeln("# This file is automatically generated, DO NOT EDIT") + r.writeln("# fmt: off") + r.writeln() + + for mod in modules: + r.writeln(f"import {mod}") + + init_py.write_text(r.getvalue(), encoding="utf-8") + + +def main(): + try: + _, libinit_py = sys.argv[:2] + modules = sys.argv[2:] + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + _write_libinit_py( + pathlib.Path(libinit_py), + modules, + ) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/gen_modinit_hpp.py b/src/semiwrap/cmd/gen_modinit_hpp.py new file mode 100644 index 00000000..a181da2b --- /dev/null +++ b/src/semiwrap/cmd/gen_modinit_hpp.py @@ -0,0 +1,114 @@ +""" +Usage: module_name output_hpp input_dat[, input_dat...] + +Generates a header file that contains initialization functions for pybind11 bindings + +You must include the header "autogen_module_init.hpp", and call initWrapper() from +your pybind11 module declaration. +""" + +import pathlib +import pickle +import sys +import typing as T + +import toposort + +from ..autowrap.buffer import RenderBuffer +from ..autowrap.context import HeaderContext + + +def _write_wrapper_hpp( + module_name: str, output_hpp: pathlib.Path, *input_dat: pathlib.Path +): + # Need to ensure that wrapper initialization is called in base order + # so we have to toposort it here based on the class hierarchy determined + # while parsing the header + types2name = {} + types2deps = {} + ordering = [] + + for datfile in input_dat: + with open(datfile, "rb") as fp: + hctx = pickle.load(fp) + + assert isinstance(hctx, HeaderContext) + + name = hctx.hname + dep = hctx.class_hierarchy + + # make sure objects without classes are also included! + if not dep: + ordering.append(name) + + for clsname, bases in dep.items(): + if clsname in types2name: + raise ValueError(f"{name} ({input_dat}): duplicate class {clsname}") + types2name[clsname] = name + types2deps[clsname] = bases[:] + + to_sort: T.Dict[str, T.Set[str]] = {} + for clsname, bases in types2deps.items(): + clsname = types2name[clsname] + deps = to_sort.setdefault(clsname, set()) + for base in bases: + base = types2name.get(base) + if base and base != clsname: + deps.add(base) + + ordering.extend(toposort.toposort_flatten(to_sort, sort=True)) + + r = RenderBuffer() + r.writeln("// This file is autogenerated, DO NOT EDIT") + r.writeln("") + r.write_trim( + f""" + #pragma once + #include + + // Use this to define your module instead of PYBIND11_MODULE + #define SEMIWRAP_PYBIND11_MODULE(variable) PYBIND11_MODULE({module_name}, variable) + + // TODO: namespace semiwrap::autogen {{ + + // forward declarations + """ + ) + for name in ordering: + r.write_trim( + f""" + void begin_init_{name}(py::module &m); + void finish_init_{name}(); + """ + ) + r.writeln() + + r.writeln("static void initWrapper(py::module &m) {") + with r.indent(): + for name in ordering: + r.writeln(f"begin_init_{name}(m);") + r.writeln() + for name in ordering: + r.writeln(f"finish_init_{name}();") + + r.writeln("}") + + output_hpp.write_text(r.getvalue(), encoding="utf-8") + + +def main(): + try: + module_name = sys.argv[1] + output_hpp = sys.argv[2] + inputs = sys.argv[3:] + except Exception as e: + print(__doc__, file=sys.stderr) + sys.exit(1) + + _write_wrapper_hpp( + module_name, pathlib.Path(output_hpp), *[pathlib.Path(i) for i in inputs] + ) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/gen_pkgconf.py b/src/semiwrap/cmd/gen_pkgconf.py new file mode 100644 index 00000000..d4406289 --- /dev/null +++ b/src/semiwrap/cmd/gen_pkgconf.py @@ -0,0 +1,46 @@ +""" +Generates a pkg-config file for this extension +""" + +import argparse +import inspect +import pathlib + +from ..mkpc import make_pc_file +from ..pyproject import PyProject + + +def main(): + parser = argparse.ArgumentParser(usage=inspect.cleandoc(__doc__ or "")) + parser.add_argument("module_package_name") + parser.add_argument("name") + parser.add_argument("pyproject_toml", type=pathlib.Path) + parser.add_argument("pcfile", type=pathlib.Path) + parser.add_argument("--libinit-py") + args = parser.parse_args() + + module_package_name = args.module_package_name + project = PyProject(args.pyproject_toml) + + module = project.get_extension(module_package_name) + depends = project.get_extension_deps(module) + + pc_install_path = project.package_root / pathlib.Path( + *module_package_name.split(".")[:-1] + ) + make_pc_file( + project_root=project.root, + pcfile=args.pcfile, + pc_install_path=pc_install_path, + name=args.name, + desc="semiwrap pybind11 module", + version="", + includes=module.includes, + depends=depends, + libinit_py=args.libinit_py, + generator_name="semiwrap.cmd_gen_pkgconf", + ) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/header2dat.py b/src/semiwrap/cmd/header2dat.py new file mode 100644 index 00000000..67f0ca1c --- /dev/null +++ b/src/semiwrap/cmd/header2dat.py @@ -0,0 +1,140 @@ +""" +Parses a header file and writes an intermediate dat file that other tools +can turn into other things +""" + +import argparse +import os +import pathlib +import pickle +import typing + +from cxxheaderparser.options import ParserOptions +from cxxheaderparser import preprocessor + +from ..autowrap.cxxparser import parse_header +from ..autowrap.generator_data import GeneratorData, MissingReporter +from ..casters import CastersData +from ..config.autowrap_yml import AutowrapConfigYaml + +# TODO: eventually provide native preprocessor by default and allow it +# to be enabled/disabled per-file just in case +# TODO: should this also be a command line option? +if os.getenv("SEMIWRAP_PP_GCC") == "1": + # GCC preprocessor can be 10x faster than pcpp for very complex files + def make_preprocessor(*args, **kwargs): + return preprocessor.make_gcc_preprocessor(print_cmd=False, *args, **kwargs) + +else: + make_preprocessor = preprocessor.make_pcpp_preprocessor + + +def generate_wrapper( + *, + name: str, + src_yml: pathlib.Path, + src_h: pathlib.Path, + src_h_root: pathlib.Path, + include_paths: typing.List[pathlib.Path], + pp_defines: typing.List[str], + casters: CastersData, + dst_dat: typing.Optional[pathlib.Path], + dst_depfile: typing.Optional[pathlib.Path], + missing_reporter: MissingReporter, + report_only: bool, +): + + try: + # semiwrap requires user to create yaml files first using create-yaml + data = AutowrapConfigYaml.from_file(src_yml) + except FileNotFoundError: + if not report_only: + raise + + print("WARNING: could not find", src_yml) + data = AutowrapConfigYaml() + + deptarget = None + if dst_depfile is not None: + assert dst_dat is not None + deptarget = [str(dst_dat)] + + popts = ParserOptions( + preprocessor=make_preprocessor( + defines=pp_defines, + include_paths=include_paths, + encoding=data.encoding, + depfile=dst_depfile, + deptarget=deptarget, + ) + ) + + gendata = GeneratorData(data, src_yml) + + try: + hctx = parse_header( + name, + src_h, + src_h_root, + gendata, + popts, + casters, + report_only, + ) + except Exception as e: + raise ValueError(f"processing {src_h}") from e + + gendata.report_missing(src_yml, missing_reporter) + + if not report_only and missing_reporter and not data.defaults.ignore: + print("WARNING: some items not in", src_yml, "for", src_h) + for name, contents in missing_reporter.as_yaml(): + print(contents) + + if dst_dat is not None: + with open(dst_dat, "wb") as fp: + pickle.dump(hctx, fp) + + +def make_argparser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("-I", "--include-paths", action="append", default=[]) + parser.add_argument("-D", "--pp-defines", action="append", default=[]) + parser.add_argument("--cpp") + parser.add_argument("name") + parser.add_argument("src_yml", type=pathlib.Path) + parser.add_argument("src_h", type=pathlib.Path) + parser.add_argument("src_h_root", type=pathlib.Path) + parser.add_argument("in_casters", type=pathlib.Path) + parser.add_argument("dst_dat", type=pathlib.Path) + parser.add_argument("dst_depfile", type=pathlib.Path) + return parser + + +def main(): + parser = make_argparser() + args = parser.parse_args() + + with open(args.in_casters, "rb") as fp: + casters = pickle.load(fp) + + if args.cpp: + args.pp_defines.append(f"__cplusplus {args.cpp}") + + generate_wrapper( + name=args.name, + src_yml=args.src_yml, + src_h=args.src_h, + src_h_root=args.src_h_root, + dst_dat=args.dst_dat, + dst_depfile=args.dst_depfile, + include_paths=args.include_paths, + casters=casters, + pp_defines=args.pp_defines, + missing_reporter=MissingReporter(), + report_only=False, + ) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/make_pyi.py b/src/semiwrap/cmd/make_pyi.py new file mode 100644 index 00000000..43110572 --- /dev/null +++ b/src/semiwrap/cmd/make_pyi.py @@ -0,0 +1,119 @@ +""" +Creates an output .pyi file from a given python module. + +Arguments are: + package outpath [subpackage outpath...] -- package mapped_file +""" + +import importlib.util +import inspect +import os +from os.path import dirname, join +import pathlib +import shutil +import sys +import tempfile +import typing as T + +import pybind11_stubgen + + +class _PackageFinder: + """ + Custom loader to allow loading built modules from their location + in the build directory (as opposed to their install location) + """ + + # Set this to mapping returned from _BuiltEnv.setup_built_env + mapping: T.Dict[str, str] = {} + + @classmethod + def find_spec(cls, fullname, path, target=None): + m = cls.mapping.get(fullname) + if m: + return importlib.util.spec_from_file_location(fullname, m) + + +def _write_pyi( + package_name, generated_pyi: T.Dict[pathlib.PurePosixPath, pathlib.Path] +): + + # We can't control where stubgen writes files, so tell it to output + # to a temporary directory and then we copy the files from there to + # our desired location + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir_pth = pathlib.Path(tmpdir) + + # Call pybind11-stubgen + sys.argv = [ + "", + "--exit-code", + "--ignore-invalid-expressions=<.*>", + "--root-suffix=", + "-o", + tmpdir, + package_name, + ] + + # Create the parent directories in the temporary directory + for infile in generated_pyi.keys(): + (tmpdir_pth / infile).parent.mkdir(parents=True, exist_ok=True) + + pybind11_stubgen.main() + + # stubgen doesn't take a direct output filename, so move the file + # to our desired location + for infile, output in generated_pyi.items(): + output.unlink(missing_ok=True) + shutil.move(tmpdir_pth / infile, output) + + +def main(): + + generated_pyi: T.Dict[pathlib.PurePosixPath, pathlib.Path] = {} + argv = sys.argv + + if len(argv) < 3: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + # Package name first + package_name = argv[1] + + # Output file map: input output + idx = 2 + while idx < len(argv): + if argv[idx] == "--": + idx += 1 + break + + generated_pyi[pathlib.PurePosixPath(argv[idx])] = pathlib.Path(argv[idx + 1]) + idx += 2 + + # Arguments are used to set up the package map + package_map = _PackageFinder.mapping + for i in range(idx, len(argv), 2): + # python 3.9 requires paths to be resolved + package_map[argv[i]] = os.fspath(pathlib.Path(argv[i + 1]).resolve()) + + # Add parent packages too + # .. assuming there are __init__.py in each package + for pkg in list(package_map.keys()): + while True: + idx = pkg.rfind(".") + if idx == -1: + break + ppkg = pkg[:idx] + if ppkg not in package_map: + package_map[ppkg] = join( + dirname(dirname(package_map[pkg])), "__init__.py" + ) + pkg = ppkg + + sys.meta_path.insert(0, _PackageFinder) + + _write_pyi(package_name, generated_pyi) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/publish_casters.py b/src/semiwrap/cmd/publish_casters.py new file mode 100644 index 00000000..d3fadc66 --- /dev/null +++ b/src/semiwrap/cmd/publish_casters.py @@ -0,0 +1,95 @@ +""" +Determines which casters are in the pyproject.toml and publishes them so they +can be consumed and used by other wheels. + +Generates a FOO.pc file and a FOO.pybind11.json file +""" + +import pathlib +import sys + +from ..casters import ( + TypeCasterJsonData, + TypeCasterJsonHeader, + save_typecaster_json_data, +) +from ..mkpc import make_pc_file +from ..pyproject import PyProject + + +def main(): + try: + _, pyproject_toml, caster_name, output_json, output_pc = sys.argv + except ValueError: + print(__doc__, file=sys.stderr) + sys.exit(1) + + project = PyProject(pathlib.Path(pyproject_toml)) + cfg = project.project.export_type_casters[caster_name] + + # make sure the include directories actually exist + include_dirs = [] + for inc in cfg.includedir: + includedir = project.root / pathlib.Path(inc) + include_dirs.append(includedir) + if not includedir.exists(): + print(f"ERROR: {includedir} does not exist", file=sys.stderr) + print( + f"- specified at [tool.semiwrap.export_type_casters.{caster_name}].includedir", + file=sys.stderr, + ) + sys.exit(1) + + pc_install_path = project.package_root / pathlib.Path(*cfg.pypackage.split(".")) + make_pc_file( + project_root=project.root, + pcfile=pathlib.Path(output_pc), + pc_install_path=pc_install_path, + name=caster_name, + desc="pybind11 type casters", + version="", + includes=cfg.includedir, + depends=cfg.requires, + libinit_py=None, + generator_name="semiwrap.cmd_publish_casters", + ) + + # + # Gather the data and write it next to the pc file + # + + data = TypeCasterJsonData() + for hdr in cfg.headers: + + # Ensure the published header actually exists + searched = [] + for inc in include_dirs: + full_hdr = inc / hdr.header + if full_hdr.exists(): + break + + searched.append(full_hdr) + else: + + print(f"ERROR: {hdr.header} does not exist", file=sys.stderr) + print( + f"- specified at [[tool.semiwrap.export_type_casters.{caster_name}.headers]].header", + file=sys.stderr, + ) + for s in searched: + print(f"- searched '{s}'") + sys.exit(1) + + data.headers.append( + TypeCasterJsonHeader( + header=hdr.header, + types=hdr.types, + default_arg_cast=hdr.default_arg_cast, + ) + ) + + save_typecaster_json_data(pathlib.Path(output_json), data) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/cmd/resolve_casters.py b/src/semiwrap/cmd/resolve_casters.py new file mode 100644 index 00000000..125fa4f6 --- /dev/null +++ b/src/semiwrap/cmd/resolve_casters.py @@ -0,0 +1,71 @@ +""" +Creates a per-module data file mapping type names to header files containing +pybind11 type caster implementations. +""" + +import inspect +import pathlib +import pickle +import sys + +from ..casters import CastersData, load_typecaster_json_data, TypeData +from ..depfile import Depfile + + +def _update_all_casters(type_caster_cfg: pathlib.Path, all_casters: CastersData): + data = load_typecaster_json_data(type_caster_cfg) + + # flatten it + for item in data.headers: + header = pathlib.Path(item.header) + if header.is_absolute(): + raise ValueError( + f"{type_caster_cfg} contains absolute path to header: {header}" + ) + + for typ in item.types: + td = TypeData( + header=header, typename=typ, default_arg_cast=item.default_arg_cast + ) + + if typ not in all_casters: + all_casters[typ] = td + + # in addition to the type, add a non-namespaced version too + # - in theory this could cause conflicts, but in practice its useful + # - this could be solved by making the parser resolve namespaces, but + # that has downsides too + ntyp = typ.split("::")[-1] + if ntyp not in all_casters: + all_casters[ntyp] = td + + +def main(): + try: + _, outfile_arg, depfile_arg = sys.argv[:3] + caster_json_files = sys.argv[3:] + except ValueError: + print(inspect.cleandoc(__doc__ or ""), file=sys.stderr) + sys.exit(1) + + outfile = pathlib.Path(outfile_arg) + depfile = pathlib.Path(depfile_arg) + + d = Depfile(outfile) + content: CastersData = {} + + for f in caster_json_files: + path = pathlib.Path(f) + d.add(path) + _update_all_casters(path, content) + + # write the depfile + d.write(depfile) + + # write the pickled data + with open(outfile, "wb") as fp: + pickle.dump(content, fp) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/command/_built_env.py b/src/semiwrap/command/_built_env.py deleted file mode 100644 index 91e8608d..00000000 --- a/src/semiwrap/command/_built_env.py +++ /dev/null @@ -1,79 +0,0 @@ -import importlib.util -import os -from os.path import abspath, exists, dirname, join - -from setuptools import Command - -from .util import get_install_root - - -class _BuiltEnv(Command): - - user_options = [("build-lib=", "d", 'directory to "build" (copy) to')] - - def initialize_options(self): - self.build_lib = None - - def finalize_options(self): - self.set_undefined_options("build", ("build_lib", "build_lib")) - - def setup_built_env(self): - - # Gather information for n - data = {"mapping": {}} - - # OSX-specific: need to set DYLD_LIBRARY_PATH otherwise modules don't - # work. Luckily, that information was computed when building the - # extensions... - env = os.environ.copy() - dyld_path = set() - - # Requires information from build_ext to work - build_ext = self.get_finalized_command("build_ext") - if build_ext.inplace: - data["out"] = get_install_root(self) - else: - data["out"] = self.build_lib - - # Ensure that the associated packages can always be found locally - for wrapper in build_ext.wrappers: - pkgdir = wrapper.package_name.split(".") - init_py = abspath(join(self.build_lib, *pkgdir, "__init__.py")) - if exists(init_py): - data["mapping"][wrapper.package_name] = init_py - - # Ensure that the built extension can always be found - build_ext.resolve_libs() - for ext in build_ext.extensions: - fname = build_ext.get_ext_filename(ext.name) - data["mapping"][ext.name] = abspath(join(self.build_lib, fname)) - - rpybuild_libs = getattr(ext, "rpybuild_libs", None) - if rpybuild_libs: - for pth, _ in rpybuild_libs.values(): - dyld_path.add(dirname(pth)) - - # OSX-specific - if dyld_path: - dyld_path = ":".join(dyld_path) - if "DYLD_LIBRARY_PATH" in env: - dyld_path += ":" + env["DYLD_LIBRARY_PATH"] - env["DYLD_LIBRARY_PATH"] = dyld_path - - return data, env - - -class _PackageFinder: - """ - Custom loader to allow loading built modules from their location - in the build directory (as opposed to their install location) - """ - - # Set this to mapping returned from _BuiltEnv.setup_built_env - mapping = {} - - @classmethod - def find_spec(cls, fullname, path, target=None): - m = cls.mapping.get(fullname) - if m: - return importlib.util.spec_from_file_location(fullname, m) diff --git a/src/semiwrap/command/build_dl.py b/src/semiwrap/command/build_dl.py deleted file mode 100644 index 10f35f5a..00000000 --- a/src/semiwrap/command/build_dl.py +++ /dev/null @@ -1,80 +0,0 @@ -from distutils.core import Command -from typing import List -import os -import os.path -import subprocess -import sys -import sysconfig - -from ..platforms import get_platform -from ..static_libs import StaticLib -from ..wrapper import Wrapper -from .util import get_install_root - - -debug = os.environ.get("RPYBUILD_DEBUG") == "1" - - -class BuildDl(Command): - command_name = "build_dl" - description = "Downloads files" - user_options = [ - ("build-base=", "b", "base directory for build library"), - ("build-temp=", "t", "temporary build directory"), - ("build-cache=", None, "build directory to cache downloaded objects"), - ("src-unpack-to=", None, "build directory to unpack sources to"), - ("lib-unpack-to=", None, "build directory to unpack static libs to"), - ] - wrappers: List[Wrapper] = [] - static_libs: List[StaticLib] = [] - - def initialize_options(self): - self.build_base = None - self.build_cache = None - self.build_temp = None - self.src_unpack_to = None - self.lib_unpack_to = None - - def finalize_options(self): - self.set_undefined_options( - "build", ("build_base", "build_base"), ("build_temp", "build_temp") - ) - if self.build_cache is None: - self.build_cache = os.path.join(self.build_base, "cache") - if self.src_unpack_to is None: - self.src_unpack_to = os.path.join(self.build_temp, "dlsrc") - if self.lib_unpack_to is None: - self.lib_unpack_to = os.path.join(self.build_temp, "dlstatic") - - def run(self): - all_libs = [] - - for lib in self.static_libs: - lib.on_build_dl(self.build_cache, self.lib_unpack_to) - for wrapper in self.wrappers: - all_libs += wrapper.on_build_dl(self.build_cache, self.src_unpack_to) - - # On OSX, fix library loader paths for embedded libraries - # -> this happens here so that the libs are modified before build_py - # copies them. Extensions are fixed after build - platform = get_platform() - if platform.os == "osx": - from ..relink_libs import relink_libs - - install_root = get_install_root(self) - for wrapper in self.wrappers: - relink_libs(install_root, wrapper, self.rpybuild_pkgcfg) - - elif not debug and platform.os == "linux": - # strip any downloaded libraries - strip_exe = "strip" - if getattr(sys, "cross_compiling", False): - # This is a hack, but the information doesn't seem to be available - # in other accessible ways - ar_exe = sysconfig.get_config_var("AR") - if ar_exe.endswith("-ar"): - strip_exe = f"{ar_exe[:-3]}-strip" - - for lib in all_libs: - print(strip_exe, lib) - subprocess.check_call([strip_exe, lib]) diff --git a/src/semiwrap/command/build_ext.py b/src/semiwrap/command/build_ext.py deleted file mode 100644 index 135e3e21..00000000 --- a/src/semiwrap/command/build_ext.py +++ /dev/null @@ -1,260 +0,0 @@ -# -# Portions copied from pybind11's setup_helpers.py -# - -import os -from os.path import join -from setuptools.command.build_ext import build_ext -import platform -import setuptools -import sys -import sysconfig -import tempfile - -from .util import get_install_root -from ..platforms import get_platform - -# TODO: only works for GCC -debug = os.environ.get("RPYBUILD_DEBUG") == "1" - -WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform() -MACOS = sys.platform.startswith("darwin") -STD_TMPL = "/std:c++{}" if WIN else "-std=c++{}" - - -# As of Python 3.6, CCompiler has a `has_flag` method. -# cf http://bugs.python.org/issue26689 -def has_flag(compiler, flagname): - """Return a boolean indicating whether a flag name is supported on - the specified compiler. - """ - with tempfile.TemporaryDirectory() as tmpdir: - fname = join(tmpdir, "test.cpp") - with open(fname, "w") as fp: - fp.write("int main (int argc, char **argv) { return 0; }") - try: - compiler.compile([fname], output_dir=tmpdir, extra_postargs=[flagname]) - except setuptools.distutils.errors.CompileError: - return False - return True - - -def cxx_std(compiler) -> int: - """Return the -std=c++[11/14/17/20] compiler flag. - The newer version is prefered over c++11 (when it is available). - """ - - for level in (20, 17, 17, 11): - if has_flag(compiler, STD_TMPL.format(level)): - return level - - raise RuntimeError("Unsupported compiler -- at least C++11 support is needed!") - - -def get_opts(typ, std): - c_opts = {"msvc": ["/EHsc", "/bigobj"], "unix": []} - l_opts = {"msvc": [], "unix": []} - - plat = get_platform() - if plat.os == "osx": - darwin_opts = ["-stdlib=libc++"] - - if "MACOSX_DEPLOYMENT_TARGET" not in os.environ: - # C++17 requires a higher min version of macOS. An earlier version - # (10.12 or 10.13) can be set manually via environment variable if - # you are careful in your feature usage, but 10.14 is the safest - # setting for general use. However, never set higher than the - # current macOS version! - current_macos = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2]) - if std == 20: - desired_macos = (10, 15) - elif std == 17: - desired_macos = (10, 14) - else: - desired_macos = (10, 9) - - macos_string = ".".join(str(x) for x in min(current_macos, desired_macos)) - darwin_opts.append(f"-mmacosx-version-min={macos_string}") - - c_opts["unix"] += darwin_opts - l_opts["unix"] += darwin_opts + ["-headerpad_max_install_names"] - - return c_opts.get(typ, []), l_opts.get(typ, []) - - -class BuildExt(build_ext): - """A custom build extension for adding compiler-specific options.""" - - def build_extensions(self): - ct = self.compiler.compiler_type - std = cxx_std(self.compiler) - opts, link_opts = get_opts(ct, std) - - # To support ccache on windows - cc_launcher = os.environ.get("RPYBUILD_CC_LAUNCHER") - - if ct == "unix": - opts.append("-s") # strip - if debug: - opts.append("-ggdb3") - opts.append("-UNDEBUG") - else: - opts.append("-g0") # remove debug symbols - opts.append(STD_TMPL.format(std)) - if has_flag(self.compiler, "-fvisibility=hidden"): - opts.append("-fvisibility=hidden") - - if cc_launcher: - self.compiler.compiler.insert(0, cc_launcher) - self.compiler.compiler_so.insert(0, cc_launcher) - try: - # setuptools v72.2.0 added C++ support - self.compiler.compiler_so_cxx.insert(0, cc_launcher) - except AttributeError: - pass - # compiler_cxx is only used for linking, so we don't mess with it - # .. distutils is so weird - # self.compiler.compiler_cxx.insert(0, cc_launcher) - elif ct == "msvc": - opts.append(STD_TMPL.format(std)) - opts.append("/Zc:__cplusplus") - # Enable standards-compliant preprocessor - if has_flag(self.compiler, "/Zc:preprocessor"): - opts.append("/Zc:preprocessor") - # Enable utf-8 source files.. this probably shouldn't be set globally - if has_flag(self.compiler, "/utf-8"): - opts.append("/utf-8") - if cc_launcher: - # yes, this is terrible. There's really no other way with distutils - def _spawn(cmd): - if cmd[0] == self.compiler.cc: - cmd.insert(0, cc_launcher) - self.compiler._rpy_spawn(cmd) - - self.compiler._rpy_spawn = self.compiler.spawn - self.compiler.spawn = _spawn - for ext in self.extensions: - if debug: - ext.define_macros.append( - ("PYBIND11_ASSERT_GIL_HELD_INCREF_DECREF", "1") - ) - ext.extra_compile_args = opts - ext.extra_link_args = link_opts - - # self._gather_global_includes() - - build_ext.build_extensions(self) - - # Fix Libraries on macOS - # Uses @loader_path, is compatible with macOS >= 10.4 - platform = get_platform() - if platform.os == "osx": - from ..relink_libs import relink_extension - - install_root = get_install_root(self) - - for ext in self.extensions: - libs = relink_extension( - install_root, - self.get_ext_fullpath(ext.name), - self.get_ext_filename(ext.name), - ext.rpybuild_wrapper, - self.rpybuild_pkgcfg, - ) - - # Used in build_pyi - ext.rpybuild_libs = libs - - def resolve_libs(self): - # used in _built_env - platform = get_platform() - if platform.os == "osx": - for wrapper in self.wrappers: - wrapper.finalize_extension() - - from ..relink_libs import resolve_libs - - install_root = get_install_root(self) - - for ext in self.extensions: - libs = resolve_libs( - install_root, - ext.rpybuild_wrapper, - self.rpybuild_pkgcfg, - ) - - # Used in build_pyi - ext.rpybuild_libs = libs - - def run(self): - # files need to be generated before building can occur - self.run_command("build_gen") - - for wrapper in self.wrappers: - wrapper.finalize_extension() - - build_ext.run(self) - - # pyi can only be built after ext is built - self.run_command("build_pyi") - - def get_libraries(self, ext): - libraries = build_ext.get_libraries(self, ext) - - if ( - sys.platform != "win32" - and os.environ.get("RPYBUILD_STRIP_LIBPYTHON") == "1" - ): - pythonlib = "python{}.{}".format( - sys.hexversion >> 24, (sys.hexversion >> 16) & 0xFF - ) - libraries = [lib for lib in libraries if not lib.startswith(pythonlib)] - - return libraries - - -parallel = int(os.environ.get("RPYBUILD_PARALLEL", "0")) -if parallel > 0: - # don't enable this hack by default, because not really sure of the - # ramifications -- however, it's really useful for development - # - # .. the real answer to this is cmake o_O - - # monkey-patch for parallel compilation - # -> https://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils/13176803#13176803 - def parallelCCompile( - self, - sources, - output_dir=None, - macros=None, - include_dirs=None, - debug=0, - extra_preargs=None, - extra_postargs=None, - depends=None, - ): - # those lines are copied from distutils.ccompiler.CCompiler directly - macros, objects, extra_postargs, pp_opts, build = self._setup_compile( - output_dir, macros, include_dirs, sources, depends, extra_postargs - ) - cc_args = self._get_cc_args(pp_opts, debug, extra_preargs) - # parallel code - import multiprocessing - import multiprocessing.pool - - N = multiprocessing.cpu_count() if parallel == 1 else parallel - - def _single_compile(obj): - try: - src, ext = build[obj] - except KeyError: - return - self._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) - - for _ in multiprocessing.pool.ThreadPool(N).imap(_single_compile, objects): - pass - return objects - - import distutils.ccompiler - - distutils.ccompiler.CCompiler.compile = parallelCCompile diff --git a/src/semiwrap/command/build_gen.py b/src/semiwrap/command/build_gen.py deleted file mode 100644 index 04efea87..00000000 --- a/src/semiwrap/command/build_gen.py +++ /dev/null @@ -1,35 +0,0 @@ -from distutils.core import Command -from typing import List -import os.path - -from ..wrapper import Wrapper - - -class BuildGen(Command): - command_name = "build_gen" - description = "Generates source files" - user_options = [ - ("build-base=", "b", "base directory for build library"), - ("build-temp=", "t", "temporary build directory"), - ("cxx-gen-dir=", "b", "Directory to write generated C++ files"), - ] - wrappers: List[Wrapper] = [] - - def initialize_options(self): - self.build_base = None - self.build_temp = None - self.cxx_gen_dir = None - - def finalize_options(self): - self.set_undefined_options( - "build", ("build_base", "build_base"), ("build_temp", "build_temp") - ) - if self.cxx_gen_dir is None: - self.cxx_gen_dir = os.path.join(self.build_temp, "gensrc") - - def run(self): - # files need to be downloaded before building can occur - self.run_command("build_dl") - - for wrapper in self.wrappers: - wrapper.on_build_gen(self.cxx_gen_dir) diff --git a/src/semiwrap/command/build_py.py b/src/semiwrap/command/build_py.py deleted file mode 100644 index ab9234ce..00000000 --- a/src/semiwrap/command/build_py.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import List - -from setuptools.command.build_py import build_py - -from ..wrapper import Wrapper - - -class BuildPy(build_py): - wrappers: List[Wrapper] = [] - - def run(self): - # files need to be generated before building can occur - # -> otherwise they're not included in the bdist - self.run_command("build_gen") - - # Add the generated files to the package data - for package, _, _, filenames in self.data_files: - for wrapper in self.wrappers: - if wrapper.package_name == package: - filenames.extend(wrapper.additional_data_files) - break - - build_py.run(self) diff --git a/src/semiwrap/command/build_pyi.py b/src/semiwrap/command/build_pyi.py deleted file mode 100644 index 3eaaf3a1..00000000 --- a/src/semiwrap/command/build_pyi.py +++ /dev/null @@ -1,90 +0,0 @@ -import json -import os -from os.path import exists, dirname, join -import subprocess -import sys - -import pybind11_stubgen - -try: - from setuptools.errors import BaseError -except ImportError: - from distutils.errors import DistutilsError as BaseError - -from ._built_env import _BuiltEnv, _PackageFinder - - -class GeneratePyiError(BaseError): - pass - - -class BuildPyi(_BuiltEnv): - base_package: str - - command_name = "build_pyi" - description = "Generates pyi files from built extensions" - - def run(self): - # cannot build pyi files when cross-compiling - if ( - "_PYTHON_HOST_PLATFORM" in os.environ - or "PYTHON_CROSSENV" in os.environ - or os.environ.get("RPYBUILD_SKIP_PYI") == "1" - ): - return - - # Gather information for needed stubs - data, env = self.setup_built_env() - data["stubs"] = [] - - # Ensure that the built extension can always be found - build_ext = self.get_finalized_command("build_ext") - for ext in build_ext.extensions: - data["stubs"].append(ext.name) - - # Don't do anything if nothing is needed - if not data["stubs"]: - return - - data_json = json.dumps(data) - - # Execute in a subprocess in case it crashes - args = [sys.executable, "-m", __name__] - try: - subprocess.run(args, input=data_json.encode("utf-8"), env=env, check=True) - except subprocess.CalledProcessError: - raise GeneratePyiError( - "Failed to generate .pyi file (see above, or set RPYBUILD_SKIP_PYI=1 to ignore) via %s" - % (args,) - ) from None - - # Create a py.typed for PEP 561 - with open(join(data["out"], *self.base_package.split("."), "py.typed"), "w"): - pass - - -def main(): - cfg = json.load(sys.stdin) - - # Configure custom loader - _PackageFinder.mapping = cfg["mapping"] - sys.meta_path.insert(0, _PackageFinder) - - # Generate pyi modules - out = cfg["out"] - for stub in cfg["stubs"]: - sys.argv = [ - "", - "--exit-code", - "--ignore-invalid-expressions=<.*>", - "--root-suffix=", - "-o", - out, - stub, - ] - - pybind11_stubgen.main() - - -if __name__ == "__main__": - main() diff --git a/src/semiwrap/command/develop.py b/src/semiwrap/command/develop.py deleted file mode 100644 index 088428b0..00000000 --- a/src/semiwrap/command/develop.py +++ /dev/null @@ -1,10 +0,0 @@ -from os.path import abspath -from setuptools.command.develop import develop - - -class Develop(develop): - def run(self): - self.distribution.rpybuild_develop_path = abspath(self.egg_base) - develop.run(self) - - # if not uninstall, perform fixups on OSX? diff --git a/src/semiwrap/command/editable_wheel.py b/src/semiwrap/command/editable_wheel.py deleted file mode 100644 index e0f85ff6..00000000 --- a/src/semiwrap/command/editable_wheel.py +++ /dev/null @@ -1,9 +0,0 @@ -from os.path import abspath -from setuptools.command.editable_wheel import editable_wheel - - -class EditableWheel(editable_wheel): - def run(self): - # you aren't supposed to do this, but... they broke my workflow - self.distribution.rpybuild_develop_path = abspath(self.project_dir) - editable_wheel.run(self) diff --git a/src/semiwrap/command/update_init.py b/src/semiwrap/command/update_init.py deleted file mode 100644 index 6c58d2b5..00000000 --- a/src/semiwrap/command/update_init.py +++ /dev/null @@ -1,73 +0,0 @@ -import json -import os -import subprocess -import sys -import typing - -try: - from setuptools.errors import BaseError -except ImportError: - from distutils.errors import DistutilsError as BaseError - -from ._built_env import _BuiltEnv, _PackageFinder - - -class UpdateInitError(BaseError): - pass - - -class UpdateInit(_BuiltEnv): - update_list: typing.List[str] - - command_name = "update_init" - description = ( - "Updates __init__.py files using settings from tool.robotpy-build.update_init" - ) - - def run(self): - # cannot use when cross-compiling - if ( - "_PYTHON_HOST_PLATFORM" in os.environ - or "PYTHON_CROSSENV" in os.environ - or not self.update_list - ): - return - - data, env = self.setup_built_env() - data["update_list"] = self.update_list - - data_json = json.dumps(data) - - # Execute in a subprocess in case it crashes - args = [sys.executable, "-m", __name__] - try: - subprocess.run(args, input=data_json.encode("utf-8"), env=env, check=True) - except subprocess.CalledProcessError: - raise UpdateInitError( - "Failed to generate .pyi file (see above, or set RPYBUILD_SKIP_PYI=1 to ignore) via %s" - % (args,) - ) from None - - -def main(): - cfg = json.load(sys.stdin) - - # Configure custom loader - _PackageFinder.mapping = cfg["mapping"] - sys.meta_path.insert(0, _PackageFinder) - - from .. import tool - - # Update init - - for to_update in cfg["update_list"]: - - sys.argv = ["", "create-imports", "-w"] + to_update.split(" ", 1) - - retval = tool.main() - if retval != 0: - break - - -if __name__ == "__main__": - main() diff --git a/src/semiwrap/command/util.py b/src/semiwrap/command/util.py deleted file mode 100644 index 1dca29dd..00000000 --- a/src/semiwrap/command/util.py +++ /dev/null @@ -1,18 +0,0 @@ -def get_install_root(cmd): - # hack - install_root = getattr(cmd.distribution, "rpybuild_develop_path", None) - if not install_root: - inst_command = cmd.distribution.get_command_obj("install") - inst_command.ensure_finalized() - install_root = inst_command.install_platlib - - return install_root - - -def get_build_temp_path(): - import distutils.dist - import distutils.command.build - - b = distutils.command.build.build(distutils.dist.Distribution()) - b.finalize_options() - return b.build_temp diff --git a/src/semiwrap/config/autowrap_yml.py b/src/semiwrap/config/autowrap_yml.py index 27414a7d..f7376469 100644 --- a/src/semiwrap/config/autowrap_yml.py +++ b/src/semiwrap/config/autowrap_yml.py @@ -62,6 +62,10 @@ class BufferType(str, enum.Enum): @dataclasses.dataclass(frozen=True) class BufferData: + """ + Specify that a parameter uses the buffer protocol + """ + #: Indicates what type of python buffer is required type: BufferType @@ -83,12 +87,25 @@ class ReturnValuePolicy(enum.Enum): for what each of these values mean. """ + #: Reference an existing object (i.e. do not create a new copy) and take ownership. take_ownership = "take_ownership" + + #: Create a new copy of the returned object, which will be owned by Python. copy = "copy" + + #: Use std::move to move the return value contents into a new instance that will be owned by Python. move = "move" + + #: Reference an existing object, but do not take ownership. reference = "reference" + + #: Indicates that the lifetime of the return value is tied to the lifetime of a parent object reference_internal = "reference_internal" + + #: pybind11 determines the policy to use automatic = "automatic" + + #: pybind11 determines the policy to use, but falls back to a reference automatic_reference = "automatic_reference" @@ -137,7 +154,16 @@ class OverloadData: #: When applied to a constructor, creates a static method for the overload instead. rename: Optional[str] = None - #: Mechanism to override individual parameters + #: Mechanism to override individual parameters. For example, to rename the first + #: parameter of ``void my_function(int param1)``: + #: + #: .. code-block:: yaml + #: + #: functions: + #: my_function: + #: param_override: + #: param1: + #: param_override: Dict[str, ParamData] = dataclasses.field(default_factory=dict) #: If specified, put the function in a sub.pack.age @@ -147,6 +173,7 @@ class OverloadData: #: function is called. no_release_gil: Optional[bool] = None + #: Configures parameters that can receive objects that implement the buffer protocol buffers: List[BufferData] = dataclasses.field(default_factory=list) #: Adds py::keep_alive to the function. Overrides automatic @@ -212,6 +239,7 @@ class FunctionData(OverloadData): # customizations for `my_overloaded_fn(int, int, int)` """ + #: See above overloads: Dict[str, OverloadData] = dataclasses.field(default_factory=dict) @@ -288,6 +316,7 @@ class EnumData: #: enums that are part of classes) subpackage: Optional[str] = None + #: Enum value configuration values: Dict[str, EnumValue] = dataclasses.field(default_factory=dict) #: This will insert code right before the semicolon ending the enum py @@ -309,6 +338,7 @@ class ClassData: #: Text to append to the (autoconverted) docstring doc_append: Optional[str] = None + #: Ignore this class ignore: bool = False #: List of bases to ignore. Name must include any template specializations. @@ -319,12 +349,43 @@ class ClassData: #: detected directly from the text. base_qualnames: Dict[str, str] = dataclasses.field(default_factory=dict) + #: Customize attributes of the class + #: + #: .. code-block:: yaml + #: + #: classes: + #: MyClass: + #: attributes: + #: m_attr: attributes: Dict[str, PropData] = dataclasses.field(default_factory=dict) + + #: Customize enums + #: + #: .. code-block:: yaml + #: + #: classes: + #: MyClass: + #: enums: + #: MyEnum: enums: Dict[str, EnumData] = dataclasses.field(default_factory=dict) + + #: Customize methods + #: + #: .. code-block:: yaml + #: + #: classes: + #: MyClass: + #: methods: + #: mymethod: methods: Dict[str, FunctionData] = dataclasses.field(default_factory=dict) + #: Inform the autogenerator that this class is polymorphic is_polymorphic: Optional[bool] = None + + #: Disable generating a trampoline for this class force_no_trampoline: bool = False + + #: Disable generating a default constructor for this class force_no_default_constructor: bool = False #: pybind11 will detect multiple inheritance automatically if a @@ -351,9 +412,6 @@ class ClassData: #: Set the python name of the class to this rename: Optional[str] = None - #: This is deprecated and has no effect - shared_ptr: bool = True - #: If specified, put the class in a sub.pack.age. Ignored #: for functions attached to a class. When template parameters #: are used, must define subpackage on template instances @@ -447,6 +505,13 @@ class MyClass {}; class Defaults: """ Defaults to apply to everything + + .. code-block:: yaml + + defaults: + ignore: true + report_ignored_missing: false + """ #: Set this to True to ignore functions/enums/classes at namespace scope @@ -460,21 +525,38 @@ class Defaults: @dataclasses.dataclass(frozen=True) class AutowrapConfigYaml: """ - Format of the file in [tool.robotpy-build.wrappers."PACKAGENAME"] - generation_data + Format of the files specified by ``[tool.semiwrap.extension_modules."PACKAGENAME".headers]``. + Each key in this data structure is a toplevel key in the YAML file. """ + #: Provides a mechanism to specify certain default behaviors defaults: Defaults = dataclasses.field(default_factory=Defaults) + #: When specified, + #: + #: .. code-block:: yaml + #: + #: strip_prefixes: + #: - FOO_ # any functions that begin with FOO_ will have it stripped strip_prefixes: List[str] = dataclasses.field(default_factory=list) #: Adds ``#include `` directives to the top of the autogenerated #: C++ file, after autodetected include dependencies are inserted. + #: + #: .. code-block:: yaml + #: + #: extra_includes: + #: - foo.h extra_includes: List[str] = dataclasses.field(default_factory=list) - #: Adds ``#include `` directives after robotpy_build.h is + #: Adds ``#include `` directives after semiwrap.h is #: included, but before any autodetected include dependencies. Only use #: this when dealing with broken headers. + #: + #: .. code-block:: yaml + #: + #: extra_includes_first: + #: - foo.h extra_includes_first: List[str] = dataclasses.field(default_factory=list) #: Specify raw C++ code that will be inserted at the end of the @@ -515,7 +597,7 @@ class AutowrapConfigYaml: #: .. code-block:: yaml #: #: classes: - #: CLASSNAME: + #: NAMESPACE::CLASSNAME: #: # customizations here, see ClassData #: classes: Dict[str, ClassData] = dataclasses.field(default_factory=dict) @@ -552,6 +634,13 @@ class AutowrapConfigYaml: #: Extra 'using' directives to insert into the trampoline and the #: wrapping scope + #: + #: .. code-block:: yaml + #: + #: typealias: + #: # makes SomeClass available in the wrapping scope + #: - some_namespace::SomeClass + #: typealias: List[str] = dataclasses.field(default_factory=list) #: Encoding to use when opening this header file diff --git a/src/semiwrap/config/dev_yml.py b/src/semiwrap/config/dev_yml.py deleted file mode 100644 index 869ab694..00000000 --- a/src/semiwrap/config/dev_yml.py +++ /dev/null @@ -1,31 +0,0 @@ -import dataclasses -import os -from typing import Optional, List -import yaml - - -@dataclasses.dataclass -class DevConfig: - """ - Configuration options useful for developing robotpy-build wrappers. - To use these, set the environment variable RPYBUILD_GEN_FILTER=filename.yml - """ - - #: When set this will only generate new wrapping code for the specified - #: headers (left side of generate in pyproject.toml). Existing wrapping - #: code will not be deleted during a build. - #: - #: Useful in conjunction with ccache - only_generate: Optional[List[str]] = None - - -def get_dev_config(name: str) -> Optional[DevConfig]: - # name is the wrapper config, not used currently - genfilter = os.environ.get("RPYBUILD_GEN_FILTER") - if genfilter: - with open(genfilter) as fp: - data = yaml.safe_load(fp) - else: - data = {} - - return DevConfig(**data) diff --git a/src/semiwrap/config/pyproject_toml.py b/src/semiwrap/config/pyproject_toml.py index 83ae0f81..a68452b2 100644 --- a/src/semiwrap/config/pyproject_toml.py +++ b/src/semiwrap/config/pyproject_toml.py @@ -11,248 +11,86 @@ @dataclasses.dataclass -class PatchInfo: - """ - A unified diff to apply to downloaded source code before building a - a wrapper. - - .. code-block:: toml - - [[tool.robotpy-build.wrappers."MY.PACKAGE.NAME".maven_lib_download.patches]] - patch = "path/to/my.patch" - strip = 0 - """ - - #: Name of patch file to apply - patch: str - - #: Number of directories to strip - strip: int = 0 - - -@dataclasses.dataclass -class MavenLibDownload: - """ - Used to download artifacts from a maven repository. This can download - headers, shared libraries, and sources. - - .. code-block:: toml - - [tool.robotpy-build.wrappers."PACKAGENAME".maven_lib_download] - artifact_id = "mything" - group_id = "com.example.thing" - repo_url = "http://example.com/maven" - version = "1.2.3" - - .. note:: For FIRST Robotics libraries, the information required can - be found in the vendor JSON file - """ - - #: Maven artifact ID - artifact_id: str - - #: Maven group ID - group_id: str +class TypeCasterHeader: - #: Maven repository URL - repo_url: str - - #: Version of artifact to download - version: str - - #: Configure the sources classifier - sources_classifier: str = "sources" - - #: When set, download sources instead of downloading libraries. When - #: using this, you need to manually add the sources to the configuration - #: to be compiled via :attr:`sources`. - use_sources: bool = False - - # common with Download - - #: Names of contained shared libraries (in loading order). If None, - #: set to artifact_id. - libs: Optional[List[str]] = None - - #: Names of contained shared link only libraries (in loading order). If None, - #: set to name. If empty list, link only libs will not be downloaded. - dlopenlibs: Optional[List[str]] = None - - #: Library extensions map - libexts: Dict[str, str] = dataclasses.field(default_factory=dict) - - #: Compile time extensions map - linkexts: Dict[str, str] = dataclasses.field(default_factory=dict) - - #: If :attr:`use_sources` is set, this is the list of sources to compile - sources: Optional[List[str]] = None - - #: If :attr:`use_sources` is set, apply the following patches to the sources. Patches - #: must be in unified diff format. - patches: Optional[List[PatchInfo]] = None - - #: Patches to downloaded header files. Patches must be in unified diff format. - header_patches: Optional[List[PatchInfo]] = None - - -@dataclasses.dataclass -class Download: - """ - Download sources/libs/includes from a single file - - .. code-block:: toml - - [[tool.robotpy-build.wrappers."PACKAGENAME".download]] - url = "https://my/url/something.zip" - incdir = "include" - libs = ["mylib"] - - """ - - #: URL of zipfile to download - #: - #: {{ARCH}} and {{OS}} are replaced with the architecture/os name - url: str - - #: Directory that contains include files. - #: - #: {{ARCH}} and {{OS}} are replaced with the architecture/os name - incdir: Optional[str] = None + #: Header file to include when one of the types are detected in a wrapper + header: str - #: Directory that contains library files - #: - #: {{ARCH}} and {{OS}} are replaced with the architecture/os name - libdir: str = "" + #: Types to look for to indicate that this type caster header should be + #: included. + types: List[str] - #: Extra include paths, relative to the include directory + #: If a parameter type that requires this type caster requires a default + #: argument, a C-style ``(type)`` cast is used on the default argument. #: - #: {{ARCH}} and {{OS}} are replaced with the architecture/os name - extra_includes: List[str] = dataclasses.field(default_factory=list) - - # Common with MavenLibDownload - - #: If specified, names of contained shared libraries (in loading order) - libs: Optional[List[str]] = None - - #: If specified, names of contained shared link only libraries (in loading order). - #: If None, set to name. If empty list, link only libs will not be downloaded. - dlopenlibs: Optional[List[str]] = None - - #: Library extensions map - libexts: Dict[str, str] = dataclasses.field(default_factory=dict) - - #: Compile time extensions map - linkexts: Dict[str, str] = dataclasses.field(default_factory=dict) - - #: List of sources to compile - sources: Optional[List[str]] = None - - #: If :attr:`sources` is set, apply the following patches to the sources. Patches - #: must be in unified diff format. - patches: Optional[List[PatchInfo]] = None - - #: Patches to downloaded header files in incdir. Patches must be in unified - #: diff format. - header_patches: Optional[List[PatchInfo]] = None - - def _update_with_platform(self, platform): - for n in ("url", "incdir", "libdir"): - v = getattr(self, n, None) - if v is not None: - v = _os_re.sub(platform.os, _arch_re.sub(platform.arch, v)) - setattr(self, n, v) - - if self.extra_includes: - self.extra_includes = [ - _os_re.sub(platform.os, _arch_re.sub(platform.arch, v)) - for v in self.extra_includes - ] - - -@dataclasses.dataclass -class StaticLibConfig: - """ - Static libraries that can be consumed as a dependency by other wrappers - in the same project. Static libraries are not directly installed, and - as a result cannot be consumed by other projects. - - .. code-block:: toml - - [tool.robotpy-build.static_libs."MY.PACKAGE.NAME"] - - """ - - #: If this project depends on external libraries stored in a maven repo - #: specify it here - maven_lib_download: Optional[MavenLibDownload] = None - - #: If this project depends on external libraries downloadable from some URL - #: specify it here - download: Optional[List[Download]] = None - - #: If True, skip this library; typically used in conjection with an override - ignore: bool = False + #: The default cast can be disabled via param_override's ``disable_type_caster_default_cast`` + default_arg_cast: bool = False @dataclasses.dataclass class TypeCasterConfig: """ - Specifies type casters that this package exports. robotpy-build - will attempt to detect these types at generation time and include - them in generated wrappers. + Specifies type casters to be exported as a pkgconf package. When a semiwrap package has a + dependency on such a package, it will use this type information at generation time and + include the type caster headers in generated wrappers. .. code-block:: toml - [[tool.robotpy-build.wrappers."PACKAGENAME".type_casters]] + [tool.semiwrap.export_type_casters.NAME] + pypackage = "mypkg" + includedirs = ["src/mypkg/type-casters"] + + [[tool.semiwrap.export_type_casters.NAME.headers]] header = "my_type_caster.h" types = ["foo_t", "ns::ins::bar_t"] .. seealso:: :ref:`type_casters` """ - #: Header file to include when one of the types are detected in a wrapper - header: str + #: Python package to write the .pc and .json file to + pypackage: str - #: Types to look for to indicate that this type caster header should be - #: included. - types: List[str] + #: Location of type caster header files, relative to the root of the project + includedir: List[str] - #: If a parameter type that requires this type caster requires a default - #: argument, a C-style ``(type)`` cast is used on the default argument. - #: - #: The default cast can be disabled via param_override's ``disable_type_caster_default_cast`` - default_arg_cast: bool = False + #: pkgconf 'requires' + requires: List[str] = dataclasses.field(default_factory=list) + + #: Contains header/type mappings for type casters + headers: List[TypeCasterHeader] = dataclasses.field(default_factory=list) @dataclasses.dataclass -class WrapperConfig: +class ExtensionModuleConfig: """ - Configuration for building a C++ python extension module, optionally - using autogenerated wrappers around existing library code. + Configuration for generating pybind11 wrappers around specified header files. .. code-block:: toml - [tool.robotpy-build.wrappers."PACKAGENAME"] - name = "package_name" + [tool.semiwrap.extension_modules."PACKAGE.NAME"] - The PACKAGENAME above is a python package (eg "example.package.name"). - A robotpy-build project can contain many different wrappers and packages. """ - #: Name that other projects/wrappers use in their 'depends' list - name: str - - #: Name of extension to build. If None, set to _{name} - extension: Optional[str] = None + #: The name used internally to refer to this extension module. The name is + #: used as the prefix of variable names in build files. + #: + #: If not specified, the default name is the package name with invalid + #: characters replaced by underscores. + name: Optional[str] = None #: Name of generated file that ensures the shared libraries and any - #: dependencies are loaded. Defaults to ``_init{extension}.py`` + #: dependencies are loaded. Defaults to ``_init_XXX.py``, where XXX + #: is the last element of the package name #: #: Generally, you should create an ``__init__.py`` file that imports #: this module, otherwise your users will need to do so. libinit: Optional[str] = None - #: List of robotpy-build library dependencies. This affects this wrapper + #: Name of pkgconf libraries that this is wrapping. This is also added to + #: depends list. + wraps: List[str] = dataclasses.field(default_factory=list) + + #: List of pkgconf library dependencies. This affects this wrapper #: library in the following ways: #: #: * Any include file directories exported by the dependency will be added @@ -262,21 +100,8 @@ class WrapperConfig: #: ``_init{extension}.py`` file. depends: List[str] = dataclasses.field(default_factory=list) - #: If this project depends on external libraries stored in a maven repo - #: specify it here. - maven_lib_download: Optional[MavenLibDownload] = None - - #: If this project depends on external libraries downloadable from some URL - #: specify it here - download: Optional[List[Download]] = None - - #: List of extra include directories to export, relative to the - #: project root. - extra_includes: List[str] = dataclasses.field(default_factory=list) - - #: Optional source files to compile. Path is relative to the root of - #: the project. - sources: List[str] = dataclasses.field(default_factory=list) + #: List of include directories for this module relative to the project root + includes: List[str] = dataclasses.field(default_factory=list) #: Specifies header files that autogenerated pybind11 wrappers will be #: created for. Simple C++ headers will most likely 'just work', but @@ -285,165 +110,86 @@ class WrapperConfig: #: #: List of dictionaries: each dictionary key is used for the function #: name of the initialization function, the value is the header that is - #: being wrapped. The header is first looked for relative to the - #: package, then relative to each include directory (including - #: downloaded and extracted packages). + #: being wrapped. The header is searched for in the following order: + #: + #: - Relative to include directories specified by 'wraps' dependencies + #: - Relative to the extra include directories for this module + #: - Relative to the package directory #: #: .. code-block:: toml #: - #: [tool.robotpy-build.wrappers."PACKAGENAME".autogen_headers] + #: [tool.semiwrap.extension_modules."PACKAGE.NAME".headers] #: Name = "header.h" #: #: .. seealso:: :ref:`autowrap` #: - autogen_headers: Optional[Dict[str, str]] = None + headers: Dict[str, str] = dataclasses.field(default_factory=dict) - #: Path to a single data.yml to use during code generation, or a directory - #: of yaml files. If a directory, generation data will be looked up - #: using the key in the generate dictionary. + #: Path to a directory of yaml files. Generation data will be looked up + #: using the key in the headers dictionary. #: - #: These YAML files can be generated via the robotpy-build command line tool: + #: These YAML files can be generated via the semiwrap command line tool: #: #: .. code-block:: sh #: - #: robotpy-build create-gen --write + #: semiwrap create-yaml --write #: #: .. seealso:: :ref:`gendata` #: - generation_data: Optional[str] = None - - #: Specifies type casters that this package exports. - type_casters: List[TypeCasterConfig] = dataclasses.field(default_factory=list) + yaml_path: Optional[str] = None - #: Preprocessor definitions to apply when compiling this wrapper. - pp_defines: List[str] = dataclasses.field(default_factory=list) - - #: If True, skip this wrapper; typically used in conjection with an override. + #: If True, skip this wrapper ignore: bool = False @dataclasses.dataclass -class DistutilsMetadata: +class SemiwrapToolConfig: """ - Configures the metadata that robotpy-build passes to setuptools when - the project is installed. The keys in this section match the standard - arguments passed to the ``setuptools.setup`` function. + Contains information for configuring the project .. code-block:: toml - [tool.robotpy-build.metadata] - name = "my-awesome-dist" - description = "Cool thing" - license = "MIT" - - robotpy-build will automatically detect/set the following keys: - - * cmdclass - * ext_modules - * include_package_data - ``True`` - * long_description - Contents of README.md/README.rst - * long_description_content_type - If required - * packages - * python_requires - ``>=3.6`` - * version - via setuptools_scm - * zip_safe - ``False`` + [tool.semiwrap] .. note:: This section is required """ - #: The name of the package - name: str - - #: The name of the package author - author: str - - #: The email address of the package author - author_email: str - - #: A URL for the package (homepage) - url: str - - #: The license for the package - license: str - - #: A string or list of strings specifying what other distributions need - #: to be installed when this one is. If the requirement is ``==THIS_VERSION``, - #: the requirement is set to be the same version as this package - install_requires: List[str] + #: List of headers for the scan-headers tool to ignore + scan_headers_ignore: List[str] = dataclasses.field(default_factory=list) - #: A single line describing the package - description: Optional[str] = None + #: List of python packages with __init__.py to update when ``python setup.py update_init`` + #: is called -- this is an argument to the ``semiwrap create-imports`` command, and + #: may contain a space and the second argument to create-imports. + update_init: List[str] = dataclasses.field(default_factory=list) - entry_points: Optional[Dict[str, List[str]]] = dataclasses.field( + #: Modules to autogenerate. The key is the name of the python package that will + #: be generated. For example, "package.name" will cause the shared library + #: 'package/name-{abi tag}.so' to be installed + extension_modules: Dict[str, ExtensionModuleConfig] = dataclasses.field( default_factory=dict ) - -@dataclasses.dataclass -class SupportedPlatform: - """ - Supported platforms for this project. Currently this information is - merely advisory, and is used to generate error messages when platform - specific downloads fail. - - .. code-block:: toml - - [tool.robotpy-build] - base_package = "my.package" - supported_platforms = [ - { os = "windows", arch = "x86-64" }, - ] - - .. seealso:: List of supported :ref:`platforms ` - - """ - - #: Platform operating system name - os: Optional[str] = None - - #: Platform architecture - arch: Optional[str] = None + #: Type casters to export. The key is the pkgconf package name. + export_type_casters: Dict[str, TypeCasterConfig] = dataclasses.field( + default_factory=dict + ) @dataclasses.dataclass -class RobotpyBuildConfig: +class SemiwrapHatchlingConfig: """ - Contains information for configuring the project + semiwrap hatchling hook configuration .. code-block:: toml - [tool.robotpy-build] - base_package = "my.package" + [tool.hatch.build.hooks.semiwrap] .. note:: This section is required """ - #: Python package to store version information and robotpy-build metadata in - base_package: str - - #: project metadata - metadata: DistutilsMetadata - - #: List of headers for the scan-headers tool to ignore - scan_headers_ignore: List[str] = dataclasses.field(default_factory=list) - - #: List of python packages with __init__.py to update when ``python setup.py update_init`` - #: is called -- this is an argument to the ``robotpy-build create-imports`` command, and - #: may contain a space and the second argument to create-imports. - update_init: List[str] = dataclasses.field(default_factory=list) - - #: - #: .. seealso:: :class:`.SupportedPlatform` - #: - supported_platforms: List[SupportedPlatform] = dataclasses.field( - default_factory=list - ) - - # - # These are all documented in their class, it's more confusing to document - # them here too. - # - - wrappers: Dict[str, WrapperConfig] = dataclasses.field(default_factory=dict) + #: Output directory to write meson.build to + autogen_build_path: str = "semiwrap" - static_libs: Dict[str, StaticLibConfig] = dataclasses.field(default_factory=dict) + #: Output directory to write second stage meson.build to (defaults + #: to ``autogen_build_path / 'modules'``) + module_build_path: Optional[str] = None diff --git a/src/semiwrap/config/util.py b/src/semiwrap/config/util.py index 82d2afef..fc5879f0 100644 --- a/src/semiwrap/config/util.py +++ b/src/semiwrap/config/util.py @@ -10,9 +10,7 @@ class ValidationError(Exception): pass -def _convert_validation_error( - fname: str, ve: errors.ValidationError -) -> ValidationError: +def _convert_validation_error(fname, ve: errors.ValidationError) -> ValidationError: locs = [] msg = [] @@ -36,7 +34,7 @@ def _convert_validation_error( return ValidationError(vmsg) -def parse_input(value: typing.Any, spec: typing.Type[T], fname: str) -> T: +def parse_input(value: typing.Any, spec: typing.Type[T], fname) -> T: try: return validobj.validation.parse_input(value, spec) except errors.ValidationError as ve: diff --git a/src/semiwrap/depfile.py b/src/semiwrap/depfile.py new file mode 100644 index 00000000..20c79fcb --- /dev/null +++ b/src/semiwrap/depfile.py @@ -0,0 +1,32 @@ +import dataclasses +import os +import pathlib +import typing as T + + +def _escape_dep(dep: str): + dep = dep.replace("\\", "\\\\") + dep = dep.replace(" ", "\\ ") + return dep + + +@dataclasses.dataclass +class Depfile: + # TODO: currently only supports single output target + + target: pathlib.Path + deps: T.List[pathlib.Path] = dataclasses.field(default_factory=list) + + def add(self, dep: pathlib.Path): + self.deps.append(dep) + + def write(self, path: pathlib.Path): + """ + Write make-compatible depfile + """ + with open(path, "w") as fp: + target = _escape_dep(str(self.target.absolute())) + fp.write(f"{target}:") + for dep in self.deps: + fp.write(f" \\\n {_escape_dep(str(dep.absolute()))}") + fp.write("\n") diff --git a/src/semiwrap/download.py b/src/semiwrap/download.py deleted file mode 100644 index 0dad5b1c..00000000 --- a/src/semiwrap/download.py +++ /dev/null @@ -1,113 +0,0 @@ -import atexit -import contextlib -import os -from os.path import dirname, exists, join, normpath -import posixpath -import shutil -import sys -import urllib.request -import tempfile -import zipfile - -from .version import version - - -USER_AGENT = f"robotpy-build/{version}" -SHOW_PROGRESS = "CI" not in os.environ - - -def _download(url: str, dst_fname: str): - """ - Downloads a file to a specified directory - """ - - def _reporthook(count, blocksize, totalsize): - if SHOW_PROGRESS: - percent = int(count * blocksize * 100 / totalsize) - sys.stdout.write("\r%02d%%" % percent) - sys.stdout.flush() - - print("Downloading", url) - - request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) - - with contextlib.closing(urllib.request.urlopen(request)) as fp: - headers = fp.info() - - with open(dst_fname, "wb") as tfp: - # copied from urlretrieve source code, Python license - bs = 1024 * 8 - size = -1 - blocknum = 0 - read = 0 - if "content-length" in headers: - size = int(headers["Content-Length"]) - - while True: - block = fp.read(bs) - if not block: - break - read += len(block) - tfp.write(block) - blocknum += 1 - if _reporthook: - _reporthook(blocknum, bs, size) - - if SHOW_PROGRESS: - sys.stdout.write("\n") - sys.stdout.flush() - - -def download_and_extract_zip(url, to, cache): - """ - Utility method intended to be useful for downloading/extracting - third party source zipfiles - - :param to: is either a string or a dict of {src: dst} - """ - - os.makedirs(cache, exist_ok=True) - zip_fname = join(cache, posixpath.basename(url)) - if not exists(zip_fname): - _download(url, zip_fname) - - with zipfile.ZipFile(zip_fname) as z: - if isinstance(to, str): - to = {"": to} - - for src, dst in to.items(): - if src == "": - z.extractall(dst) - else: - # if is directory, copy whole thing recursively - try: - info = z.getinfo(src) - except KeyError as e: - osrc = src - src = src + "/" - try: - info = z.getinfo(src) - except KeyError: - info = None - if info is None: - msg = f"error extracting {osrc} from {zip_fname}" - raise ValueError(msg) from e - if info.is_dir(): - ilen = len(info.filename) - for minfo in z.infolist(): - if minfo.is_dir(): - continue - srcname = posixpath.normpath(minfo.filename) - if srcname.startswith(info.filename): - dstname = join(dst, normpath(srcname[ilen:])) - dstdir = dirname(dstname) - if not exists(dstdir): - os.makedirs(dstdir) - with z.open(minfo.filename, "r") as zfp, open( - dstname, "wb" - ) as fp: - shutil.copyfileobj(zfp, fp) - else: - # otherwise write a single file - with z.open(src, "r") as zfp, open(dst, "wb") as fp: - shutil.copyfileobj(zfp, fp) diff --git a/src/semiwrap/hooks.py b/src/semiwrap/hooks.py new file mode 100644 index 00000000..0789ad5d --- /dev/null +++ b/src/semiwrap/hooks.py @@ -0,0 +1,108 @@ +# TODO: rename this + +import os +import pathlib +import sys +import traceback +import typing as T + +from hatchling.plugin import hookimpl +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + +from .render_meson import render_meson_to_file + +from .config.pyproject_toml import SemiwrapHatchlingConfig +from .config.util import parse_input + + +@hookimpl +def hatch_register_build_hook(): + return SemiwrapBuildHook + + +class SemiwrapBuildHook(BuildHookInterface): + """ + Sets up code generation to be ran by meson + """ + + PLUGIN_NAME = "semiwrap" + + def initialize(self, version: str, build_data: T.Dict[str, T.Any]) -> None: + # Only needed for building wheels + if self.target_name != "wheel": + return + + project_root = pathlib.Path(self.root).resolve() + + config = parse_input( + self.config, SemiwrapHatchlingConfig, "[tool.hatch.build.hooks.semiwrap]" + ) + stage0_build_path = project_root / config.autogen_build_path + stage0_meson_build = stage0_build_path / "meson.build" + stage0_gitignore = stage0_build_path / ".gitignore" + + if config.module_build_path is not None: + stage1_build_path = project_root / config.module_build_path + else: + stage1_build_path = stage0_build_path / "modules" + + stage1_meson_build = stage1_build_path / "meson.build" + + # This is used to generate files installed into a directory called 'trampolines' + # so due to https://github.com/mesonbuild/meson/issues/2320 this must also be + # in a directory called 'trampolines' + # - unlike stage0 and stage1, this is included by stage0 + trampoline_build_path = stage0_build_path / "trampolines" + trampoline_meson_build = trampoline_build_path / "meson.build" + + try: + eps = render_meson_to_file( + project_root, + stage0_meson_build, + stage1_meson_build, + trampoline_meson_build, + ) + except Exception as e: + # Reading the stack trace is annoying, most of the time the exception content + # is enough to figure out what you did wrong. + if os.environ.get("SEMIWRAP_ERROR_VERBOSE") == "1": + raise + + msg = [ + "ERROR: exception occurred when processing `pyproject.toml`\n\n", + ] + + msg += traceback.format_exception_only(type(e), e) + cause = e.__context__ + while cause is not None: + if "prepare_metadata_for_build_editable" in str(cause): + break + + el = traceback.format_exception_only(type(cause), cause) + el[0] = f"- caused by {el[0]}" + msg += el + + if cause.__suppress_context__: + break + + cause = cause.__context__ + + msg.append( + "\nSet environment variable SEMIWRAP_ERROR_VERBOSE=1 for stacktrace" + ) + + print("".join(msg), file=sys.stderr) + sys.exit(1) + + if eps: + # .. not documented but it works? + for ep in eps: + g = self.metadata.core.entry_points.setdefault(ep.group, {}) + g[ep.name] = ep.package + + if not stage0_gitignore.exists(): + stage0_gitignore.write_text( + "/meson.build\n" + f"/{stage1_build_path.name}/meson.build\n" + f"/{trampoline_build_path.name}/meson.build\n" + ) diff --git a/src/semiwrap/include/gilsafe_object.h b/src/semiwrap/include/gilsafe_object.h index 38906e45..92d2c5e9 100644 --- a/src/semiwrap/include/gilsafe_object.h +++ b/src/semiwrap/include/gilsafe_object.h @@ -10,7 +10,7 @@ namespace py = pybind11; #define Py_IsFinalizing _Py_IsFinalizing #endif -namespace rpy { +namespace semiwrap { /* This object holds a python object, and can be stored in C++ containers that @@ -111,7 +111,7 @@ class gilsafe_t final { // convenience alias using gilsafe_object = gilsafe_t; -} // namespace rpy +} // namespace semiwrap @@ -119,7 +119,7 @@ PYBIND11_NAMESPACE_BEGIN(PYBIND11_NAMESPACE) PYBIND11_NAMESPACE_BEGIN(detail) template -struct type_caster> { +struct type_caster> { bool load(handle src, bool convert) { value = src; return true; @@ -129,11 +129,11 @@ struct type_caster> { return src.inc_ref(); } - PYBIND11_TYPE_CASTER(rpy::gilsafe_t, handle_type_name::name); + PYBIND11_TYPE_CASTER(semiwrap::gilsafe_t, handle_type_name::name); }; template -struct handle_type_name> { +struct handle_type_name> { static constexpr auto name = handle_type_name::name; }; diff --git a/src/semiwrap/include/robotpy_build.h b/src/semiwrap/include/semiwrap.h similarity index 78% rename from src/semiwrap/include/robotpy_build.h rename to src/semiwrap/include/semiwrap.h index 0efd2cfb..188d1972 100644 --- a/src/semiwrap/include/robotpy_build.h +++ b/src/semiwrap/include/semiwrap.h @@ -1,6 +1,6 @@ #pragma once -// Base definitions used by all robotpy-build projects +// Base definitions used by all semiwrap projects #include @@ -10,28 +10,26 @@ namespace py = pybind11; typedef py::call_guard release_gil; // empty trampoline configuration base -namespace rpygen { +namespace swgen { struct EmptyTrampolineCfg {}; }; -#define RPYBUILD_BAD_TRAMPOLINE \ +#define SEMIWRAP_BAD_TRAMPOLINE \ "has an abstract trampoline -- and they must never be abstract! One of " \ "the generated override methods doesn't match the original class or its " \ "bases, or is missing. You will need to provide method and/or param " \ "overrides for that method. It is likely the following compiler error " \ "messages will tell you which one it is." -// Use this to define your module instead of PYBIND11_MODULE -#define RPYBUILD_PYBIND11_MODULE(variable) PYBIND11_MODULE(RPYBUILD_MODULE_NAME, variable) -// only for use by RPYBUILD_OVERRIDE_PURE_POST_IMPL +// only for use by SEMIWRAP_OVERRIDE_PURE_POST_IMPL template py::handle __get_handle(const T *this_ptr) { auto this_type = py::detail::get_type_info(typeid(T)); if (!this_type) return py::handle(); return py::detail::get_object_handle(this_ptr, this_type); } -#define RPYBUILD_OVERRIDE_CUSTOM_IMPL(ret_type, cname, name, ...) \ +#define SEMIWRAP_OVERRIDE_CUSTOM_IMPL(ret_type, cname, name, ...) \ do { \ py::gil_scoped_acquire gil; \ py::function override = py::get_override(static_cast(this), name); \ @@ -41,7 +39,7 @@ template py::handle __get_handle(const T *this_ptr) { } \ } while (false) -#define RPYBUILD_OVERRIDE_PURE_POST_IMPL(pyname, cname, name) \ +#define SEMIWRAP_OVERRIDE_PURE_POST_IMPL(pyname, cname, name) \ std::string __msg(" does not override required function \"" PYBIND11_STRINGIFY(pyname) "::" name "\""); \ try \ { \ @@ -60,29 +58,29 @@ template py::handle __get_handle(const T *this_ptr) { py::pybind11_fail(__msg); \ } -#define RPYBUILD_OVERRIDE_PURE_CUSTOM_NAME(pyname, ret_type, cname, name, fn, ...) \ +#define SEMIWRAP_OVERRIDE_PURE_CUSTOM_NAME(pyname, ret_type, cname, name, fn, ...) \ { \ - RPYBUILD_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, __VA_ARGS__);\ - RPYBUILD_OVERRIDE_PURE_POST_IMPL(pyname, PYBIND11_TYPE(cname), name); \ + SEMIWRAP_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, __VA_ARGS__);\ + SEMIWRAP_OVERRIDE_PURE_POST_IMPL(pyname, PYBIND11_TYPE(cname), name); \ } -#define RPYBUILD_OVERRIDE_PURE_NAME(pyname, ret_type, cname, name, fn, ...) \ +#define SEMIWRAP_OVERRIDE_PURE_NAME(pyname, ret_type, cname, name, fn, ...) \ { \ PYBIND11_OVERRIDE_IMPL(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, __VA_ARGS__);\ - RPYBUILD_OVERRIDE_PURE_POST_IMPL(pyname, PYBIND11_TYPE(cname), name); \ + SEMIWRAP_OVERRIDE_PURE_POST_IMPL(pyname, PYBIND11_TYPE(cname), name); \ } -#define RPYBUILD_OVERRIDE_CUSTOM_NAME(ret_type, cname, name, fn, ...) \ - RPYBUILD_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, __VA_ARGS__);\ +#define SEMIWRAP_OVERRIDE_CUSTOM_NAME(ret_type, cname, name, fn, ...) \ + SEMIWRAP_OVERRIDE_CUSTOM_IMPL(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, __VA_ARGS__);\ return cname::fn(__VA_ARGS__) // // backwards compat // -#define RPYBUILD_OVERLOAD_PURE_CUSTOM_NAME(pyname, ret_type, cname, name, fn, ...) \ - RPYBUILD_OVERRIDE_PURE_CUSTOM_NAME(pyname, PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) -#define RPYBUILD_OVERLOAD_CUSTOM_NAME(ret_type, cname, name, fn, ...) \ - RPYBUILD_OVERRIDE_CUSTOM_NAME(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) -#define RPYBUILD_OVERLOAD_PURE_NAME(pyname, ret_type, cname, name, fn, ...) \ - RPYBUILD_OVERRIDE_PURE_NAME(pyname, PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) +#define SEMIWRAP_OVERLOAD_PURE_CUSTOM_NAME(pyname, ret_type, cname, name, fn, ...) \ + SEMIWRAP_OVERRIDE_PURE_CUSTOM_NAME(pyname, PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) +#define SEMIWRAP_OVERLOAD_CUSTOM_NAME(ret_type, cname, name, fn, ...) \ + SEMIWRAP_OVERRIDE_CUSTOM_NAME(PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) +#define SEMIWRAP_OVERLOAD_PURE_NAME(pyname, ret_type, cname, name, fn, ...) \ + SEMIWRAP_OVERRIDE_PURE_NAME(pyname, PYBIND11_TYPE(ret_type), PYBIND11_TYPE(cname), name, fn, __VA_ARGS__) diff --git a/src/semiwrap/makeplan.py b/src/semiwrap/makeplan.py new file mode 100644 index 00000000..aa2b1e71 --- /dev/null +++ b/src/semiwrap/makeplan.py @@ -0,0 +1,660 @@ +from __future__ import annotations + +import collections +import dataclasses +import os +import pathlib +import pprint +import sys +import sysconfig +import typing as T + +from .casters import PKGCONF_CASTER_EXT +from .config.autowrap_yml import AutowrapConfigYaml +from .config.pyproject_toml import ExtensionModuleConfig, TypeCasterConfig +from .pkgconf_cache import PkgconfCache +from .pyproject import PyProject +from .util import relpath_walk_up + +import toposort + + +@dataclasses.dataclass +class Entrypoint: + """ + Represents a python entrypoint that needs to be created when the wheel is installed + + .. seealso:: https://packaging.python.org/en/latest/specifications/entry-points/ + """ + + name: str + group: str + package: str + + +@dataclasses.dataclass(frozen=True) +class CppMacroValue: + """ + Represents the value of a macro defined by the C++ compiler + """ + + name: str + + +@dataclasses.dataclass(frozen=True) +class LocalDependency: + name: str + include_paths: T.Tuple[pathlib.Path, ...] + depends: T.Tuple[T.Union[LocalDependency, str], ...] + + +@dataclasses.dataclass(frozen=True) +class InputFile: + # TODO: what is this relative to? + path: pathlib.Path + + +@dataclasses.dataclass(frozen=True) +class OutputFile: + name: str + + +@dataclasses.dataclass(frozen=True) +class Depfile: + # this is needed anytime a command generates something based on something + # that is more than just the input file + name: str + + +@dataclasses.dataclass(frozen=True) +class BuildTargetOutput: + # references a specific build target output + target: BuildTarget + output_index: int + + +@dataclasses.dataclass(frozen=True) +class BuildTarget: + command: str + + args: T.Tuple[ + T.Union[ + str, + pathlib.Path, + InputFile, + OutputFile, + Depfile, + BuildTarget, + BuildTargetOutput, + CppMacroValue, + ], + ..., + ] + + # Install path is always relative to py.get_install_dir(pure: false) + install_path: T.Optional[pathlib.Path] + + +@dataclasses.dataclass(frozen=True) +class ExtensionModule: + + #: variable name/prefix in the build file + name: str + + #: full package name of installed extension + package_name: str + + sources: T.Tuple[BuildTarget, ...] + depends: T.Tuple[T.Union[LocalDependency, str], ...] + + # extra include directories that won't be found via depends + include_directories: T.Tuple[pathlib.Path, ...] + + # Install path is always relative to py.get_install_dir(pure: false) + install_path: pathlib.Path + + +class PlanError(Exception): + pass + + +def _split_ns(name: str) -> T.Tuple[str, str]: + ns = "" + idx = name.rfind("::") + if idx != -1: + ns = name[:idx] + name = name[idx + 2 :] + return ns, name + + +class _BuildPlanner: + def __init__(self, project_root: pathlib.Path, missing_yaml_ok: bool = False): + + self.project_root = project_root + self.missing_yaml_ok = missing_yaml_ok + + self.pyproject = PyProject(project_root / "pyproject.toml") + self.pkgcache = PkgconfCache() + self.pyproject_input = InputFile(project_root / "pyproject.toml") + + self.pyi_targets: T.List[BuildTarget] = [] + self.pyi_args = [] + + self.local_caster_targets: T.Dict[str, BuildTargetOutput] = {} + self.local_dependencies: T.Dict[str, LocalDependency] = {} + + sw_path = self.pkgcache.get("semiwrap").type_casters_path + assert sw_path is not None + self.semiwrap_type_caster_path = sw_path + + def generate(self): + + projectcfg = self.pyproject.project + + # + # Export type casters + # .. this probably should be its own hatchling plugin? + # + + for name, caster_cfg in projectcfg.export_type_casters.items(): + yield from self._process_export_type_caster(name, caster_cfg) + + # This is needed elsewhere + self._cpp_macro = CppMacroValue("__cplusplus") + yield self._cpp_macro + + # + # Generate extension modules + # + + for package_name, extension in self._sorted_extension_modules(): + try: + yield from self._process_extension_module(package_name, extension) + except Exception as e: + raise PlanError(f"{package_name} failed") from e + + # TODO: this conditional probably should be done in the build system instead + # cannot build pyi files when cross-compiling + if not ( + "_PYTHON_HOST_PLATFORM" in os.environ + or "PYTHON_CROSSENV" in os.environ + or os.environ.get("SEMIWRAP_SKIP_PYI") == "1" + ): + # Make a pyi for every module + # - they depend on every module because it needs a working environment + # and the user might import something + # - if there's a subpkg this fails, need to think about it + for pyi_target in self.pyi_targets: + yield BuildTarget( + command="make-pyi", + args=pyi_target.args + tuple(self.pyi_args), + install_path=pyi_target.install_path, + ) + + def _resolve_dep(self, dname: str): + return self.local_dependencies.get(dname, dname) + + def _process_export_type_caster(self, name: str, caster_cfg: TypeCasterConfig): + + # Need to generate the data file and the .pc file + caster_target = BuildTarget( + command="publish-casters", + args=( + self.pyproject_input, + name, + OutputFile(f"{name}{PKGCONF_CASTER_EXT}"), + OutputFile(f"{name}.pc"), + ), + install_path=pathlib.Path(*caster_cfg.pypackage.split(".")), + ) + yield caster_target + + # Need an entrypoint to point at the .pc file + yield Entrypoint(group="pkg_config", name=name, package=caster_cfg.pypackage) + + dep = self.pkgcache.add_local( + name=name, + includes=[self.project_root / inc for inc in caster_cfg.includedir], + requires=caster_cfg.requires, + ) + caster_dep = LocalDependency( + name=dep.name, + include_paths=tuple(dep.include_path), + depends=tuple([self._resolve_dep(cd) for cd in caster_cfg.requires]), + ) + self.local_dependencies[dep.name] = caster_dep + yield caster_dep + + # The .pc file cannot be used in the build, but the data file must be, so + # store it so it can be used elsewhere + self.local_caster_targets[name] = BuildTargetOutput(caster_target, 0) + + def _sorted_extension_modules( + self, + ) -> T.Generator[T.Tuple[str, ExtensionModuleConfig], None, None]: + # sort extension modules by dependencies, that way modules can depend on other modules + # also declared in pyproject.toml without needing to worry about ordering in the file + by_name = {} + to_sort: T.Dict[str, T.Set[str]] = {} + + for package_name, extension in self.pyproject.project.extension_modules.items(): + if extension.ignore: + continue + + name = extension.name or package_name.replace(".", "_") + by_name[name] = (package_name, extension) + + deps = to_sort.setdefault(name, set()) + for dep in extension.wraps: + deps.add(dep) + for dep in extension.depends: + deps.add(dep) + + for name in toposort.toposort_flatten(to_sort, sort=True): + data = by_name.get(name) + if data: + yield data + + def _process_extension_module( + self, package_name: str, extension: ExtensionModuleConfig + ): + package_path_elems = package_name.split(".") + parent_package = ".".join(package_path_elems[:-1]) + module_name = package_path_elems[-1] + package_path = pathlib.Path(*package_path_elems[:-1]) + varname = extension.name or package_name.replace(".", "_") + + # Detect the location of the package in the source tree + package_init_py = self.pyproject.package_root / package_path / "__init__.py" + self.pyi_args += [parent_package, package_init_py.as_posix()] + + depends = self.pyproject.get_extension_deps(extension) + depends.append("semiwrap") + + # Search path for wrapping is dictated by package_path and wraps + search_path, include_directories_uniq, caster_json_file, libinit_modules = ( + self._prepare_dependency_paths(depends, extension) + ) + + includes = [ + self.project_root / pathlib.PurePosixPath(inc) for inc in extension.includes + ] + search_path.extend(includes) + + # Search the package path last + search_path.append(self.pyproject.package_root / package_path) + + all_type_casters = BuildTarget( + command="resolve-casters", + args=( + OutputFile(f"{varname}.casters.pkl"), + Depfile(f"{varname}.casters.d"), + *caster_json_file, + ), + install_path=None, + ) + yield all_type_casters + + # + # Generate init.py for loading dependencies + # + + libinit_module = None + if libinit_modules: + libinit_py = extension.libinit or f"_init_{module_name}.py" + libinit_tgt = BuildTarget( + command="gen-libinit-py", + args=(OutputFile(libinit_py), *libinit_modules), + install_path=package_path, + ) + + libinit_module = f"{parent_package}.{libinit_py}"[:-3] + self.pyi_args += [libinit_module, libinit_tgt] + + yield libinit_tgt + + # + # Publish a .pc file for this module + # + + pc_args = [ + package_name, + varname, + self.pyproject_input, + OutputFile(f"{varname}.pc"), + ] + if libinit_module: + pc_args += ["--libinit-py", libinit_module] + + yield BuildTarget( + command="gen-pkgconf", + args=tuple(pc_args), + install_path=package_path, + ) + + yield Entrypoint(group="pkg_config", name=varname, package=parent_package) + + # + # Process the headers + # + + # Find and load the yaml + if extension.yaml_path is None: + yaml_path = pathlib.Path("semiwrap") + else: + yaml_path = pathlib.Path(pathlib.PurePosixPath(extension.yaml_path)) + + datfiles, module_sources, subpackages = yield from self._process_headers( + extension, + package_path, + yaml_path, + include_directories_uniq.keys(), + search_path, + all_type_casters, + ) + + modinit = BuildTarget( + command="gen-modinit-hpp", + args=( + module_name, + OutputFile(f"semiwrap_init.{package_name}.hpp"), + *datfiles, + ), + install_path=None, + ) + module_sources.append(modinit) + yield modinit + + # + # Emit the module + # + + # Use a local dependency to store everything so it can be referenced elsewhere + cached_dep = self.pkgcache.add_local( + name=varname, + includes=[*includes, self.pyproject.package_root / package_path], + requires=depends, + libinit_py=libinit_module, + ) + local_dep = LocalDependency( + name=cached_dep.name, + include_paths=tuple(cached_dep.include_path), + depends=tuple(self._resolve_dep(dep) for dep in depends), + ) + yield local_dep + self.local_dependencies[local_dep.name] = local_dep + + modobj = ExtensionModule( + name=varname, + package_name=package_name, + sources=tuple(module_sources), + depends=(local_dep,), + include_directories=tuple(), + install_path=package_path, + ) + yield modobj + + self.pyi_args += [package_name, modobj] + + # This is not yielded here because pyi targets need to depend on all modules + # via self.pyi_args. + # - The output .pyi files vary based on whether there are subpackages or not. If no + # subpackage, it's {module_name}.pyi. If there are subpackages, it becomes + # {module_name}/__init__.pyi and {module_name}/{subpackage}.pyi + # - As long as a user doesn't manually bind a subpackage our detection works here + # but if we need to allow that then will need to declare subpackages in pyproject.toml + # .. this breaks if there are sub-sub packages, don't do that please + + base_pyi_elems = package_name.split(".") + + if subpackages: + pyi_elems = base_pyi_elems + ["__init__.pyi"] + pyi_args = [ + pathlib.PurePath(*pyi_elems).as_posix(), + OutputFile("__init__.pyi"), + ] + for subpackage in subpackages: + pyi_elems = base_pyi_elems + [f"{subpackage}.pyi"] + pyi_args += [ + pathlib.PurePath(*pyi_elems).as_posix(), + OutputFile(f"{subpackage}.pyi"), + ] + + self.pyi_targets.append( + BuildTarget( + command="make-pyi", + args=(package_name, *pyi_args, "--"), + install_path=package_path / module_name, + ) + ) + + else: + base_pyi_elems[-1] = f"{base_pyi_elems[-1]}.pyi" + + self.pyi_targets.append( + BuildTarget( + command="make-pyi", + args=( + package_name, + pathlib.PurePath(*base_pyi_elems).as_posix(), + OutputFile(f"{module_name}.pyi"), + "--", + ), + install_path=package_path, + ) + ) + + def _locate_type_caster_json( + self, + depname: str, + caster_json_file: T.List[T.Union[BuildTargetOutput, pathlib.Path]], + ): + checked = set() + to_check = collections.deque([depname]) + while to_check: + name = to_check.popleft() + checked.add(name) + + entry = self.pkgcache.get(name) + + if name in self.local_caster_targets: + caster_json_file.append(self.local_caster_targets[name]) + else: + tc = entry.type_casters_path + if tc and tc not in caster_json_file: + caster_json_file.append(tc) + + for req in entry.requires: + if req not in checked: + to_check.append(req) + + def _prepare_dependency_paths( + self, depends: T.List[str], extension: ExtensionModuleConfig + ): + search_path: T.List[pathlib.Path] = [] + include_directories_uniq: T.Dict[pathlib.Path, bool] = {} + caster_json_file: T.List[T.Union[BuildTargetOutput, pathlib.Path]] = [] + libinit_modules: T.List[str] = [] + + # Add semiwrap default type casters + caster_json_file.append(self.semiwrap_type_caster_path) + + for dep in depends: + entry = self.pkgcache.get(dep) + include_directories_uniq.update( + dict.fromkeys(entry.full_include_path, True) + ) + + # extend the search path if the dependency is in 'wraps' + if dep in extension.wraps: + search_path.extend(entry.include_path) + + self._locate_type_caster_json(dep, caster_json_file) + + if entry.libinit_py: + libinit_modules.append(entry.libinit_py) + + return search_path, include_directories_uniq, caster_json_file, libinit_modules + + def _process_headers( + self, + extension: ExtensionModuleConfig, + package_path: pathlib.Path, + yaml_path: pathlib.Path, + include_directories_uniq: T.Iterable[pathlib.Path], + search_path: T.List[pathlib.Path], + all_type_casters: BuildTarget, + ): + datfiles: T.List[BuildTarget] = [] + module_sources: T.List[BuildTarget] = [] + subpackages: T.Set[str] = set() + + for yml, hdr in self.pyproject.get_extension_headers(extension): + yml_input = InputFile(yaml_path / f"{yml}.yml") + + try: + ayml = AutowrapConfigYaml.from_file(self.project_root / yml_input.path) + except FileNotFoundError: + if not self.missing_yaml_ok: + msg = f"{self.project_root / yml_input.path}: use `python3 -m semiwrap create-yaml --write` to generate" + raise FileNotFoundError(msg) from None + ayml = AutowrapConfigYaml() + + # find the source header + h_input, h_root = self._locate_header(hdr, search_path) + + header2dat_args = [] + for inc in include_directories_uniq: + header2dat_args += ["-I", inc] + + # https://github.com/pkgconf/pkgconf/issues/391 + header2dat_args += ["-I", sysconfig.get_path("include")] + + header2dat_args += ["--cpp", self._cpp_macro] + + header2dat_args.append(yml) + header2dat_args.append(yml_input) + header2dat_args.append(h_input) + header2dat_args.append(h_root) + header2dat_args.append(all_type_casters) + header2dat_args.append(OutputFile(f"{yml}.dat")) + header2dat_args.append(Depfile(f"{yml}.d")) + + datfile = BuildTarget( + command="header2dat", args=tuple(header2dat_args), install_path=None + ) + yield datfile + datfiles.append(datfile) + + # Every header has a .cpp file for binding + cppfile = BuildTarget( + command="dat2cpp", + args=(datfile, OutputFile(f"{yml}.cpp")), + install_path=None, + ) + module_sources.append(cppfile) + yield cppfile + + # Detect subpackages + for f in ayml.functions.values(): + if f.ignore: + continue + if f.subpackage: + subpackages.add(f.subpackage) + for f in f.overloads.values(): + if f.subpackage: + subpackages.add(f.subpackage) + + for e in ayml.enums.values(): + if e.ignore: + continue + if e.subpackage: + subpackages.add(e.subpackage) + + # Every class gets a trampoline file, but some just have #error in them + for name, ctx in ayml.classes.items(): + if ctx.ignore: + continue + + if ctx.subpackage: + subpackages.add(ctx.subpackage) + + cls_ns, cls_name = _split_ns(name) + cls_ns = cls_ns.replace(":", "_") + trampoline = BuildTarget( + command="dat2trampoline", + args=(datfile, name, OutputFile(f"{cls_ns}__{cls_name}.hpp")), + install_path=package_path / "trampolines", + ) + module_sources.append(trampoline) + yield trampoline + + # Even more files if there are templates + if ayml.templates: + + # Every template instantiation gets a cpp file to lessen compiler + # memory requirements + for i, (name, tctx) in enumerate(ayml.templates.items(), start=1): + if tctx.subpackage: + subpackages.add(tctx.subpackage) + + tmpl_cpp = BuildTarget( + command="dat2tmplcpp", + args=(datfile, name, OutputFile(f"{yml}_tmpl{i}.cpp")), + install_path=None, + ) + module_sources.append(tmpl_cpp) + yield tmpl_cpp + + # All of which use this hpp file + tmpl_hpp = BuildTarget( + command="dat2tmplhpp", + args=(datfile, OutputFile(f"{yml}_tmpl.hpp")), + install_path=None, + ) + module_sources.append(tmpl_hpp) + yield tmpl_hpp + + return datfiles, module_sources, subpackages + + def _locate_header(self, hdr: str, search_path: T.List[pathlib.Path]): + phdr = pathlib.PurePosixPath(hdr) + for p in search_path: + h_path = p / phdr + if h_path.exists(): + # We should return this as an InputFile, but inputs must be relative to the + # project root, which may not be the case on windows. Incremental build should + # still work, because the header is included in a depfile + return h_path, p + raise FileNotFoundError( + f"cannot locate {phdr} in {', '.join(map(str, search_path))}" + ) + + +def makeplan(project_root: pathlib.Path, missing_yaml_ok: bool = False) -> T.Generator[ + T.Union[BuildTarget, Entrypoint, LocalDependency, ExtensionModule, CppMacroValue], + None, +]: + """ + Given the pyproject.toml configuration for a semiwrap project, reads the + configuration and generates a series of commands that can be used to parse + the input headers and generate the needed source code from them. + """ + planner = _BuildPlanner(project_root, missing_yaml_ok) + yield from planner.generate() + + +def main(): + # this command only exists for debugging purposes + try: + _, project_root = sys.argv + except ValueError: + print(f"{sys.argv[0]} project_root", file=sys.stderr) + sys.exit(1) + + output = list(makeplan(pathlib.Path(project_root))) + pprint.pprint(output, indent=1) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/maven.py b/src/semiwrap/maven.py deleted file mode 100644 index ae208dd8..00000000 --- a/src/semiwrap/maven.py +++ /dev/null @@ -1,67 +0,0 @@ -import typing - -from .config.pyproject_toml import Download, MavenLibDownload - - -def _get_artifact_url(dlcfg: MavenLibDownload, classifier: str) -> str: - # TODO: support development against locally installed things? - repo_url = dlcfg.repo_url - grp = dlcfg.group_id.replace(".", "/") - art = dlcfg.artifact_id - ver = dlcfg.version - - return f"{repo_url}/{grp}/{art}/{ver}/{art}-{ver}-{classifier}.zip" - - -def convert_maven_to_downloads( - mcfg: MavenLibDownload, static: bool -) -> typing.List[Download]: - """ - Converts a MavenLibDownload object to a list of normal downloads - """ - - dl_lib = {} - dl_header = {} - dl_sources = {} - - if mcfg.use_sources: - if static: - raise ValueError("Cannot specify sources in static_lib section") - - # sources don't have libs, ignore them - dl_sources["url"] = _get_artifact_url(mcfg, mcfg.sources_classifier) - dl_sources["sources"] = mcfg.sources - dl_sources["patches"] = mcfg.patches - elif mcfg.sources is not None: - raise ValueError("sources must be None if use_sources is False!") - elif mcfg.patches is not None: - raise ValueError("patches must be None if use_sources is False!") - else: - # libs - - dl_lib["libs"] = mcfg.libs - if mcfg.libs is mcfg.dlopenlibs is None: - dl_lib["libs"] = [mcfg.artifact_id] - dl_lib["dlopenlibs"] = mcfg.dlopenlibs - dl_lib["libexts"] = mcfg.libexts - dl_lib["linkexts"] = mcfg.linkexts - - if static: - dl_lib["libdir"] = "{{ OS }}/{{ ARCH }}/static" - dl_lib["url"] = _get_artifact_url(mcfg, "{{ OS }}{{ ARCH }}static") - else: - dl_lib["libdir"] = "{{ OS }}/{{ ARCH }}/shared" - dl_lib["url"] = _get_artifact_url(mcfg, "{{ OS }}{{ ARCH }}") - - # headers - dl_header["incdir"] = "" - dl_header["url"] = _get_artifact_url(mcfg, "headers") - dl_header["header_patches"] = mcfg.header_patches - - # Construct downloads and return it - downloads = [] - for d in (dl_lib, dl_header, dl_sources): - if d: - downloads.append(Download(**d)) - - return downloads diff --git a/src/semiwrap/mkpc.py b/src/semiwrap/mkpc.py new file mode 100644 index 00000000..c6f6267b --- /dev/null +++ b/src/semiwrap/mkpc.py @@ -0,0 +1,51 @@ +import pathlib +import typing as T + +from .pkgconf_cache import INITPY_VARNAME +from .util import relpath_walk_up + + +def make_pc_file( + project_root: pathlib.Path, + pcfile: pathlib.Path, + pc_install_path: pathlib.Path, + name: str, + desc: str, + version: str, + includes: T.List[str], + depends: T.List[str], + libinit_py: T.Optional[str], + generator_name: str, +): + + pc_content = [ + f"# automatically generated by {generator_name}", + "prefix=${pcfiledir}", + ] + + if libinit_py: + pc_content.append(f"{INITPY_VARNAME}={libinit_py}") + + cflags = ["-I${prefix}"] + + for i, inc in enumerate(includes): + includedir = project_root / pathlib.PurePosixPath(inc) + rel = relpath_walk_up(includedir, pc_install_path) + pc_content.append(f"inc{i}=${{prefix}}/{rel.as_posix()}") + cflags.append(f"-I${{inc{i}}}") + + pc_content += [ + "", + f"Name: {name}", + f"Description: {desc}", + f"Version: {version}", + "Cflags: " + " ".join(cflags), + ] + + if depends: + requires = " ".join(depends) + pc_content.append(f"Requires: {requires}") + + pc_content.append("") + + pcfile.write_text("\n".join(pc_content), encoding="utf-8") diff --git a/src/semiwrap/overrides.py b/src/semiwrap/overrides.py deleted file mode 100644 index 7a92a5e8..00000000 --- a/src/semiwrap/overrides.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import Dict, List - - -def _merge(name, d, override_keys): - override = d.pop("override", None) - if override is not None: - if not isinstance(override, dict): - raise ValueError( - f"{name}.override: got {type(override)}, must be a dictionary" - ) - for k in override_keys: - v = override.get(k, None) - if v is not None: - if not isinstance(v, dict): - raise ValueError( - f"{name}.override.{k}: got {type(v)}, must be a dictionary" - ) - d.update(v) - - for k, v in d.items(): - if isinstance(v, dict): - _merge(f"{name}.{k}", v, override_keys) - elif isinstance(v, list): - for i in v: - if isinstance(i, dict): - _merge(f"{name}.{k}[{i}]", i, override_keys) - - -def apply_overrides(d: Dict, override_keys: List[str]): - """ - The idea is that any dictionary key can contain a key - called 'override', which will be processed as follows: - - - override contents must be a dictionary - - key is a potential 'override key' - - value must be a dictionary - - If a key matches an override key, then the contained - dictionary will be shallow updated with the contents - of the dictionary - - This will recurse all structures and implement overrides - wherever dictionaries are found - - Currently, if a list is overridden, the entire list is replaced. It may - make sense to provide - """ - _merge("", d, override_keys) - - -def _main(): - import argparse - import tomli - import tomli_w - - parser = argparse.ArgumentParser() - parser.add_argument("fname") - parser.add_argument("key") - args = parser.parse_args() - - with open(args.fname, "rb") as fp: - d = tomli.load(fp) - - apply_overrides(d, [args.key]) - print(tomli_w.dumps(d)) - - -if __name__ == "__main__": - _main() diff --git a/src/semiwrap/pkgcfg.py b/src/semiwrap/pkgcfg.py deleted file mode 100644 index eb2dd547..00000000 --- a/src/semiwrap/pkgcfg.py +++ /dev/null @@ -1,67 +0,0 @@ -# Used by robotpybuild entry point - -from os.path import abspath, join, dirname -from typing import Any, Dict, List, Optional - -_root = abspath(dirname(__file__)) - - -def get_include_dirs() -> Optional[List[str]]: - return [join(_root, "pybind11", "include"), join(_root, "include")] - - -def get_library_dirs() -> Optional[List[str]]: - pass - - -def get_type_casters_cfg(casters: Dict[str, Dict[str, Any]]) -> None: - casters.update( - { - # STL support - "std::vector": {"hdr": "pybind11/stl.h"}, - "std::deque": {"hdr": "pybind11/stl.h"}, - "std::list": {"hdr": "pybind11/stl.h"}, - "std::array": {"hdr": "pybind11/stl.h"}, - "std::valarray": {"hdr": "pybind11/stl.h"}, - "std::set": {"hdr": "pybind11/stl.h"}, - "std::map": {"hdr": "pybind11/stl.h"}, - "std::unordered_map": {"hdr": "pybind11/stl.h"}, - "std::optional": {"hdr": "pybind11/stl.h"}, - "std::nullopt_t": {"hdr": "pybind11/stl.h"}, - "std::variant": {"hdr": "pybind11/stl.h"}, - "std::function": {"hdr": "pybind11/functional.h"}, - "std::complex": {"hdr": "pybind11/complex.h"}, - "std::chrono::duration": {"hdr": "pybind11/chrono.h"}, - "std::chrono::time_point": {"hdr": "pybind11/chrono.h"}, - # Eigen support (requires numpy) - "Eigen::Block": {"hdr": "pybind11/eigen.h"}, - "Eigen::DiagonalMatrix": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixBase": {"hdr": "pybind11/eigen.h"}, - "Eigen::Matrix": {"hdr": "pybind11/eigen.h"}, - "Eigen::Matrix2d": {"hdr": "pybind11/eigen.h"}, - "Eigen::Matrix3d": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixXcd": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixXd": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixXdR": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixXi": {"hdr": "pybind11/eigen.h"}, - "Eigen::MatrixXf": {"hdr": "pybind11/eigen.h"}, - "Eigen::Ref": {"hdr": "pybind11/eigen.h"}, - "Eigen::Matrix4d": {"hdr": "pybind11/eigen.h"}, - "Eigen::RowVectorXf": {"hdr": "pybind11/eigen.h"}, - "Eigen::SparseMatrix": {"hdr": "pybind11/eigen.h"}, - "Eigen::SparseView": {"hdr": "pybind11/eigen.h"}, - "Eigen::Vector": {"hdr": "pybind11/eigen.h"}, - "Eigen::Vector2d": {"hdr": "pybind11/eigen.h"}, - "Eigen::Vector3d": {"hdr": "pybind11/eigen.h"}, - "Eigen::VectorXf": {"hdr": "pybind11/eigen.h"}, - "Eigen::VectorXcf": {"hdr": "pybind11/eigen.h"}, - } - ) - - -def get_type_casters(casters: Dict[str, str]) -> None: - t = {} - get_type_casters_cfg(t) - for k, v in t.items(): - if "hdr" in v: - casters[k] = v["hdr"] diff --git a/src/semiwrap/pkgcfg_provider.py b/src/semiwrap/pkgcfg_provider.py deleted file mode 100644 index 3435a084..00000000 --- a/src/semiwrap/pkgcfg_provider.py +++ /dev/null @@ -1,207 +0,0 @@ -import importlib.util -from importlib.metadata import entry_points, EntryPoint -from os.path import join, dirname -import sys -from typing import Dict, List, Optional, Set -import warnings - - -def _hacky_entrypoint_loader(module_name): - # load the root parent spec - pkgs = module_name.split(".") - spec = importlib.util.find_spec(pkgs[0]) - assert spec is not None and spec.origin is not None - - # even namespace packages are installed in the path, so just guess - # ... and maybe it works? - fname = join(dirname(spec.origin), *pkgs[1:]) + ".py" - spec = importlib.util.spec_from_file_location(module_name, fname) - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -class PkgCfg: - """ - Contains information about an installed package that uses robotpy-build - """ - - def __init__(self, entry_point: EntryPoint): - try: - self.module = entry_point.load() - except Exception as e: - try: - mod = ( - entry_point.module - if hasattr(entry_point, "module") - else entry_point.value - ) - self.module = _hacky_entrypoint_loader(mod) - except Exception: - raise e - - self.name = entry_point.name - - # could deduce this, but this is probably fine - self.libinit_import = getattr(self.module, "libinit_import", None) - self.depends = getattr(self.module, "depends", []) - self.pypi_package = getattr(self.module, "pypi_package", None) - self.package_name = getattr(self.module, "package_name", None) - self.static_lib = getattr(self.module, "static_lib", False) - - def get_include_dirs(self) -> Optional[List[str]]: - """ - Include directories provided by this module - """ - fn = getattr(self.module, "get_include_dirs", None) - if fn: - return fn() - return None - - def get_library_dirs(self) -> Optional[List[str]]: - """ - Directories where libraries reside - """ - fn = getattr(self.module, "get_library_dirs", None) - if fn: - return fn() - return None - - def get_library_dirs_rel(self) -> Optional[List[str]]: - """ - Directories where libraries reside, relative to package - """ - fn = getattr(self.module, "get_library_dirs_rel", None) - if fn: - return fn() - return None - - def get_library_names(self) -> Optional[List[str]]: - """ - Names of libraries provided (for linking) - """ - fn = getattr(self.module, "get_library_names", None) - if fn: - return fn() - return None - - def get_extra_objects(self) -> Optional[List[str]]: - """ - Names of extra objects to link in - """ - fn = getattr(self.module, "get_extra_objects", None) - if fn: - return fn() - return None - - def get_library_full_names(self) -> Optional[List[str]]: - """ - Full names of libraries provided (needed for OSX support) - """ - fn = getattr(self.module, "get_library_full_names", None) - if fn: - return fn() - return None - - def get_type_casters(self, casters: Dict[str, str]) -> None: - """ - Legacy type caster information - """ - t = {} - r = self.get_type_casters_cfg(t) - for k, v in t.items(): - if "hdr" in v: - casters[k] = v["hdr"] - return r - - def get_type_casters_cfg(self, casters: Dict[str, str]) -> None: - """ - Type caster headers provided - - key: type name - value: a dict with keys: - hdr: header file - darg: force default arg - - """ - fn = getattr(self.module, "get_type_casters_cfg", None) - if fn: - return fn(casters) - fn = getattr(self.module, "get_type_casters", None) - if fn: - t = {} - r = fn(t) - casters.update({k: {"hdr": v} for k, v in t.items()}) - return r - - -class PkgCfgProvider: - """ - Retrieves information about robotpy-build packages - - Warning: Not to be confused with 'pkg-config' - """ - - def __init__(self): - self.pkgs = {} - - def detect_pkgs(self) -> None: - """ - Detect and load packages under the robotpybuild entry point group. - Only loads packages that are dependencies. - """ - deps_names = set().union(*[pkg.depends for pkg in self.pkgs.values()]) - ep_ret = entry_points() - - # Python 3.8/3.9 - if isinstance(ep_ret, dict): - all_entry_points = ep_ret.get("robotpybuild", []) - else: - all_entry_points = [e for e in entry_points() if e.group == "robotpybuild"] - - # Only load the dependencies of the package we're building. - # If we load the [package being built], then the current build will fail. - # If we load a package that depends on the [package being built], - # then the [package being built] will be loaded and the current build will fail. - run_loop = True - while run_loop: - run_loop = False - for ep in all_entry_points: - if ep.name in self.pkgs: # Prevents loading the package being built - continue - if ep.name not in deps_names and ep.name != "robotpy-build": - continue - try: - pkg = PkgCfg(ep) - except Exception as e: - warnings.warn(f"Error loading entry point {ep.name}: {e}") - else: - self.add_pkg(pkg) - deps_names |= set(pkg.depends) - run_loop = True - - def add_pkg(self, pkg: PkgCfg) -> None: - self.pkgs[pkg.name] = pkg - - def get_pkg(self, name: str) -> PkgCfg: - try: - return self.pkgs[name] - except KeyError: - raise KeyError("robotpy-build package '%s' not installed" % name) - - def get_all_deps(self, name: str) -> Set[PkgCfg]: - deps: Set[PkgCfg] = set() - - def _get(name: str): - pkg = self.get_pkg(name) - if pkg in deps: - return pkg - deps.add(pkg) - for dep in pkg.depends: - _get(dep) - return pkg - - pkg = _get(name) - deps.remove(pkg) - return deps diff --git a/src/semiwrap/pkgconf_cache.py b/src/semiwrap/pkgconf_cache.py new file mode 100644 index 00000000..806607f0 --- /dev/null +++ b/src/semiwrap/pkgconf_cache.py @@ -0,0 +1,116 @@ +import pathlib +import shlex +import typing as T + +import pkgconf + + +INITPY_VARNAME = "pkgconf_pypi_initpy" + + +class CacheEntry: + + def __init__(self, name: str) -> None: + self.name = name + self.manual = False + + def _get_pkgconf_data(self, *args) -> str: + r = pkgconf.run_pkgconf(self.name, *args, capture_output=True) + if r.returncode != 0: + raise KeyError(f"Package '{self.name}' is not installed") + + return r.stdout.decode("utf-8").strip() + + @property + def requires(self) -> T.List[str]: + if not hasattr(self, "_requires"): + raw = self._get_pkgconf_data("--print-requires") + self._requires = [r for r in raw.split("\n") if r] + + return self._requires + + @property + def include_path(self) -> T.List[pathlib.Path]: + """Only the include path for this package""" + if not hasattr(self, "_include_path"): + include_path = [] + raw = self._get_pkgconf_data( + "--cflags-only-I", "--maximum-traverse-depth=1" + ) + for i in shlex.split(raw): + assert i.startswith("-I") + include_path.append(pathlib.Path(i[2:]).absolute()) + + self._include_path = include_path + + return self._include_path + + @property + def full_include_path(self) -> T.List[pathlib.Path]: + """Include path for this package and requirements""" + if not hasattr(self, "_full_include_path"): + full_include_path = [] + raw = self._get_pkgconf_data("--cflags-only-I") + for i in shlex.split(raw): + assert i.startswith("-I") + full_include_path.append(pathlib.Path(i[2:]).absolute()) + + self._full_include_path = full_include_path + + return self._full_include_path + + @property + def type_casters_path(self) -> T.Optional[pathlib.Path]: + if not hasattr(self, "_type_casters_path"): + raw = self._get_pkgconf_data("--path") + pc_path = pathlib.Path(raw) + type_caster_cfg = pc_path.with_suffix(".pybind11.json") + if type_caster_cfg.exists(): + self._type_casters_path = type_caster_cfg + else: + self._type_casters_path = None + + return self._type_casters_path + + @property + def libinit_py(self) -> T.Optional[str]: + if not hasattr(self, "_libinit_py"): + raw = self._get_pkgconf_data(f"--variable={INITPY_VARNAME}") + if raw: + self._libinit_py = raw + else: + self._libinit_py = None + + return self._libinit_py + + +class PkgconfCache: + def __init__(self) -> None: + self._cache: T.Dict[str, CacheEntry] = {} + + def add_local( + self, + name: str, + includes: T.List[pathlib.Path], + requires: T.List[str], + libinit_py: T.Optional[str] = None, + ) -> CacheEntry: + assert name not in self._cache + entry = CacheEntry(name) + entry.manual = True + entry._include_path = [inc.absolute() for inc in includes] + entry._full_include_path = entry._include_path[:] + entry._libinit_py = libinit_py + entry._type_casters_path = None + entry._requires = requires[:] + for req in requires: + dep = self.get(req) + entry._full_include_path.extend(dep.full_include_path) + self._cache[name] = entry + return entry + + def get(self, depname: str) -> CacheEntry: + entry = self._cache.get(depname) + if entry is None: + self._cache[depname] = entry = CacheEntry(depname) + return entry diff --git a/src/semiwrap/platforms.py b/src/semiwrap/platforms.py deleted file mode 100644 index 237b3585..00000000 --- a/src/semiwrap/platforms.py +++ /dev/null @@ -1,111 +0,0 @@ -from distutils.util import get_platform as _get_platform -from dataclasses import dataclass, field -from typing import List -import re -import typing - - -# wpilib platforms at https://github.com/wpilibsuite/native-utils/blob/master/src/main/java/edu/wpi/first/nativeutils/WPINativeUtilsExtension.java -@dataclass -class WPILibMavenPlatform: - arch: str - os: str = "linux" - libprefix: str = "lib" - - #: runtime linkage - libext: str = ".so" - - #: compile time linkage - linkext: str = None - - #: static linkage - staticext: str = ".a" - - defines: List[str] = field(default_factory=list) - - def __post_init__(self): - # linkext defaults to libext - if self.linkext is None: - self.linkext = self.libext - self.defines = [f"{d} 1" for d in self.defines] - - -X86_64 = "x86-64" - -# key is python platform, value is information about wpilib maven artifacts -_platforms = { - "linux-roborio": WPILibMavenPlatform("athena", defines=["__FRC_ROBORIO__"]), - "linux-raspbian": WPILibMavenPlatform("arm32", defines=["__RASPBIAN__"]), - "linux-armv7l": WPILibMavenPlatform("arm32"), - "linux-x86_64": WPILibMavenPlatform(X86_64), - "linux-aarch64": WPILibMavenPlatform("arm64"), - "win32": WPILibMavenPlatform("x86", "windows", "", ".dll", ".lib", ".lib"), - "win-amd64": WPILibMavenPlatform(X86_64, "windows", "", ".dll", ".lib", ".lib"), - "macos-universal": WPILibMavenPlatform("universal", "osx", libext=".dylib"), -} - - -def get_platform_names() -> typing.List[str]: - return list(_platforms.keys()) - - -def get_platform(name: typing.Optional[str] = None) -> WPILibMavenPlatform: - """ - Retrieve platform specific information - """ - - # TODO: _PYTHON_HOST_PLATFORM is used for cross builds, - # and is returned directly from get_platform. Might - # be useful to note for the future. - - if not name: - pyplatform = _get_platform() - - # Check for 64 bit x86 macOS (version agnostic) - # - See https://github.com/pypa/setuptools/issues/2520 for universal2 - # related questions? Sorta. - if ( - re.fullmatch(r"macosx-.*-x86_64", pyplatform) - or re.fullmatch(r"macosx-.*-arm64", pyplatform) - or re.fullmatch(r"macosx-.*-universal2", pyplatform) - ): - return _platforms["macos-universal"] - - if pyplatform == "linux-armv7l": - try: - import distro - - distro_id = distro.id() - - if distro_id == "raspbian": - pyplatform = "linux-raspbian" - - except Exception: - pass - - elif pyplatform == "linux-armv6": - try: - import distro - - distro_id = distro.id() - - if distro_id == "raspbian": - pyplatform = "linux-raspbian" - except Exception: - pass - - name = pyplatform - - try: - return _platforms[name] - except KeyError: - raise KeyError(f"platform {name} is not supported by semiwrap!") - - -def get_platform_override_keys(platform: WPILibMavenPlatform): - # Used in places where overrides exist - return [ - f"arch_{platform.arch}", - f"os_{platform.os}", - f"platform_{platform.os}_{platform.arch}", - ] diff --git a/src/semiwrap/pyproject.py b/src/semiwrap/pyproject.py new file mode 100644 index 00000000..77f76964 --- /dev/null +++ b/src/semiwrap/pyproject.py @@ -0,0 +1,73 @@ +import pathlib +import typing as T + +import tomli + +from .config.util import parse_input +from .config.pyproject_toml import ExtensionModuleConfig, SemiwrapToolConfig + + +class PyProject: + def __init__(self, pyproject_path: T.Optional[pathlib.Path] = None) -> None: + + if pyproject_path: + self.root = pyproject_path.parent.resolve() + else: + self.root = pathlib.Path().resolve() + + self._platform = None + self._project = None + self._package_root = None + + self._all_deps = None + + @property + def package_root(self) -> pathlib.Path: + if self._package_root is None: + # try to detect packages based on the extension modules + for package_name in self.project.extension_modules.keys(): + parent = pathlib.Path(*package_name.split(".")[:-1]) + if (self.root / parent / "__init__.py").exists(): + self._package_root = self.root + break + elif (self.root / "src" / parent / "__init__.py").exists(): + self._package_root = self.root / "src" + break + else: + raise ValueError("Cannot determine package root") + + return self._package_root + + @property + def project(self): + if self._project is None: + project_fname = self.root / "pyproject.toml" + + try: + with open(project_fname, "rb") as fp: + self.pyproject = tomli.load(fp) + except FileNotFoundError as e: + raise ValueError("current directory is not a semiwrap project") from e + + self.project_dict = self.pyproject.get("tool", {}).get("semiwrap", {}) + + try: + self._project = parse_input( + self.project_dict, SemiwrapToolConfig, project_fname + ) + except Exception as e: + raise ValueError( + f"semiwrap configuration in pyproject.toml is incorrect" + ) from e + return self._project + + def get_extension(self, module_package_name: str) -> ExtensionModuleConfig: + return self.project.extension_modules[module_package_name] + + def get_extension_deps(self, extension: ExtensionModuleConfig) -> T.List[str]: + deps = [] + for wrap in extension.wraps: + if wrap not in extension.depends: + deps.append(wrap) + deps.extend(extension.depends) + return deps diff --git a/src/semiwrap/relink_libs.py b/src/semiwrap/relink_libs.py deleted file mode 100644 index f16b6100..00000000 --- a/src/semiwrap/relink_libs.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -On OSX, the loader does not look at the current process to load -dylibs -- it insists on finding them itself, so we have to fixup -our binaries such that they resolve correctly. - -Two cases we need to deal with -- Local development/installation -- Building a wheel for pypi - -In development, we assume things are installed exactly where they will be -at runtime. - -> @loader_path/{relpath(final_location, dep_path)} - -For pypi wheels, we assume that installation is in site-packages, and -so are the libraries that this lib depends on. - -> @loader_path/{relpath(final_siterel, dep_siterel)} - -Notice these are the same IF you only build wheels in a virtualenv -that only has its dependencies installed in site-packages - - -.. warning:: This will only work for the environment it's compiled in! - This basically means don't compile wheels in your development - environment, use a clean environment instead - -""" - -from delocate.delocating import filter_system_libs -from delocate.tools import get_install_names, set_install_name as _set_install_name - -from os import path - -from typing import Dict, List, Optional, Tuple - -from .pkgcfg_provider import PkgCfg, PkgCfgProvider - - -def set_install_name(file: str, old_install_name: str, new_install_name: str): - """Change the install name for a library - - :param file: path to a executable/library file - :param old_install_name: current path to dependency - :param new_install_name: new path to dependency - """ - - # This function just calls delocate's set_install_name which uses install_name_tool. - # This function exists in case we want to change the implementation. - - _set_install_name(file, old_install_name, new_install_name) - print("Relink:", file, ":", old_install_name, "->", new_install_name) - - -# Common data structure used here -# - key is basename of library file -# - Value tuple has two pieces: -# - 0: Where the library file really is right now -# - 1: Where the library file will be when installed -LibsDict = Dict[str, Tuple[str, str]] - - -def _resolve_libs(libpaths: Optional[List[str]], libname_full: str, libs: LibsDict): - if not libpaths: - return - for libpath in libpaths: - p = path.join(libpath, libname_full) - if path.exists(p): - libs[libname_full] = (p, p) - return - - -def _resolve_libs_in_self(dep: PkgCfg, install_root: str, libs: LibsDict): - pkgroot = path.join(install_root, *dep.package_name.split(".")) - full_names = dep.get_library_full_names() - if not full_names: - return - for libname_full in full_names: - for ld, ldr in zip(dep.get_library_dirs(), dep.get_library_dirs_rel()): - p = path.join(ld, libname_full) - if path.exists(p): - # stores where it will exist - libs[libname_full] = (p, path.join(pkgroot, ldr, libname_full)) - break - - -def _resolve_dependencies( - install_root: str, pkg: PkgCfg, pkgcfg: PkgCfgProvider, libs: LibsDict -): - # first, gather all possible libraries by retrieving this package and - # it's dependents. We're not concerned about redirecting non-robotpy-build - # libraries, since we can't control where those are located - deps = pkgcfg.get_all_deps(pkg.name) - - pypi_package = pkg.pypi_package - - for dep in deps: - # dependencies are in their installed location - # .. except when they're in the same wheel - if pypi_package and dep.pypi_package == pypi_package: - _resolve_libs_in_self(dep, install_root, libs) - else: - libdirs = dep.get_library_dirs() - full_names = dep.get_library_full_names() - if full_names: - for libname_full in full_names: - _resolve_libs(libdirs, libname_full, libs) - - -def _fix_libs(to_fix: LibsDict, libs: LibsDict): - for current_libpath, install_libpath in to_fix.values(): - for lib in get_install_names(current_libpath): - libb = path.basename(lib) - libdata = libs.get(libb) - if libdata: - desired_path = path.relpath(libdata[1], path.dirname(install_libpath)) - desired_path = "@loader_path/" + desired_path - set_install_name(current_libpath, lib, desired_path) - elif filter_system_libs(lib): - raise ValueError( - "unresolved lib %s: maybe a dependency is missing?" % lib - ) - - -def relink_libs(install_root: str, pkg: PkgCfg, pkgcfg: PkgCfgProvider): - """ - Given a package, relink it's external libraries - - :param install_root: Where this package will be (is) installed - :param pkg: Object that implements pkgcfg for this wrapper - :param pkgcfg: robotpy-build pkgcfg resolver - """ - libs: LibsDict = {} - _resolve_dependencies(install_root, pkg, pkgcfg, libs) - to_fix: LibsDict = {} - _resolve_libs_in_self(pkg, install_root, to_fix) - libs.update(to_fix) - _fix_libs(to_fix, libs) - - -def relink_extension( - install_root: str, - extension_path: str, - extension_rel: str, - pkg: PkgCfg, - pkgcfg: PkgCfgProvider, -) -> LibsDict: - """ - Given an extension, relink it - - :param install_root: Where this package will be (is) installed - :param extension_path: full path to extension library - :param extension_rel: Relative path to library where it will be (is) installed - :param pkg: Object that implements pkgcfg for this wrapper - :param pkgcfg: robotpy-build pkgcfg resolver - """ - libs: LibsDict = {} - _resolve_dependencies(install_root, pkg, pkgcfg, libs) - _resolve_libs_in_self(pkg, install_root, libs) - - to_fix = { - path.basename(extension_path): ( - extension_path, - path.join(install_root, extension_rel), - ) - } - _fix_libs(to_fix, libs) - return libs - - -def resolve_libs( - install_root: str, - pkg: PkgCfg, - pkgcfg: PkgCfgProvider, -): - libs: LibsDict = {} - _resolve_dependencies(install_root, pkg, pkgcfg, libs) - _resolve_libs_in_self(pkg, install_root, libs) - return libs diff --git a/src/semiwrap/render_meson.py b/src/semiwrap/render_meson.py new file mode 100644 index 00000000..bd815a95 --- /dev/null +++ b/src/semiwrap/render_meson.py @@ -0,0 +1,454 @@ +from __future__ import annotations + +import codecs +import os +import pathlib +import re +import sys +import typing as T + +from .autowrap.buffer import RenderBuffer +from .makeplan import ( + Depfile, + Entrypoint, + InputFile, + OutputFile, + BuildTarget, + BuildTargetOutput, + ExtensionModule, + LocalDependency, + CppMacroValue, + makeplan, +) +from .util import maybe_write_file, relpath_walk_up + +# String escaping stolen from meson source code, Apache 2.0 license +# This is the regex for the supported escape sequences of a regular string +# literal, like 'abc\x00' +ESCAPE_SEQUENCE_SINGLE_RE = re.compile( + r""" + ( \\U[A-Fa-f0-9]{8} # 8-digit hex escapes + | \\u[A-Fa-f0-9]{4} # 4-digit hex escapes + | \\x[A-Fa-f0-9]{2} # 2-digit hex escapes + | \\[0-7]{1,3} # Octal escapes + | \\N\{[^}]+\} # Unicode characters by name + | \\[\\'abfnrtv] # Single-character escapes + )""", + re.UNICODE | re.VERBOSE, +) + + +def _decode_match(match: T.Match[str]) -> str: + return codecs.decode(match.group(0).encode(), "unicode_escape") + + +def _make_string(s: str): + s = ESCAPE_SEQUENCE_SINGLE_RE.sub(_decode_match, s) + return f"'{s}'" + + +VarTypes = T.Union[ + InputFile, OutputFile, BuildTarget, ExtensionModule, LocalDependency, CppMacroValue +] + + +class VarCache: + def __init__(self) -> None: + self.cache: T.Dict[VarTypes, str] = {} + + # this might get annoying to debug, but for now this is easier... + self.idx = 1 + + def getvar(self, item: VarTypes) -> str: + var = self.cache.get(item) + if var is None: + + # TODO: probably should name variables according to their target type + # to make it easier to debug + + if isinstance(item, InputFile): + # .. this probably isn't right either, what should this be relative to? + # .. TODO should use files()? but maybe only if this is actually a variable + return _make_string(item.path.resolve().as_posix()) + # var = f"sw_in_{self.idx}" + elif isinstance(item, OutputFile): + return _make_string(item.name) + # var = f"sw_out_{self.idx}" + elif isinstance(item, BuildTarget): + var = f"_sw_target_{self.idx}" + elif isinstance(item, ExtensionModule): + name = item.name.replace("-", "_") + var = f"{name}_module" + elif isinstance(item, LocalDependency): + name = item.name.replace("-", "_") + var = f"{name}_dep" + elif isinstance(item, CppMacroValue): + name = item.name + var = f"_sw_cpp_var_{name}" + else: + assert False + + self.idx += 1 + self.cache[item] = var + return var + + +def _render_build_target(r: RenderBuffer, vc: VarCache, bt: BuildTarget): + outvar = vc.getvar(bt) + + bt_cmd = bt.command.replace("-", "_") + cmd = [f"_sw_cmd_{bt_cmd}"] + tinput = [] + toutput = [] + depfile = None + + for arg in bt.args: + if isinstance(arg, str): + cmd.append(_make_string(arg)) + elif isinstance(arg, pathlib.Path): + cmd.append(_make_string(arg.resolve().as_posix())) + elif isinstance(arg, (BuildTarget, InputFile)): + cmd.append(f"'@INPUT{len(tinput)}@'") + tinput.append(vc.getvar(arg)) + elif isinstance(arg, BuildTargetOutput): + cmd.append(f"'@INPUT{len(tinput)}@'") + tinput.append(f"{vc.getvar(arg.target)}[{arg.output_index}]") + elif isinstance(arg, OutputFile): + cmd.append(f"'@OUTPUT{len(toutput)}@'") + toutput.append(vc.getvar(arg)) + elif isinstance(arg, Depfile): + assert depfile is None, bt + cmd.append("'@DEPFILE@'") + depfile = _make_string(arg.name) + elif isinstance(arg, ExtensionModule): + cmd.append(f"'@INPUT{len(tinput)}@'") + tinput.append(vc.getvar(arg)) + elif isinstance(arg, CppMacroValue): + cmd.append(vc.getvar(arg)) + else: + assert False, f"unexpected {arg!r} in {bt}" + + r.writeln(f"{outvar} = custom_target(") + with r.indent(2): + _render_meson_args(r, "command", cmd) + if tinput: + _render_meson_args(r, "input", tinput) + if toutput: + _render_meson_args(r, "output", toutput) + + if depfile: + r.writeln(f"depfile: {depfile},") + + if bt.install_path is not None: + install_path = _make_string(bt.install_path.as_posix()) + r.writeln( + f"install_dir: sw_py.get_install_dir(pure: false) / {install_path}," + ) + r.writeln("install: true,") + + r.writeln(")") + + +def _render_include_directories( + r: RenderBuffer, + incs: T.Sequence[pathlib.Path], + meson_build_path: T.Optional[pathlib.Path], +): + # meson wants these to be relative to meson.build + # - only can do that if we're writing an output file + if meson_build_path: + meson_build_parent = meson_build_path.parent + incs = [relpath_walk_up(p, meson_build_parent) for p in incs] + + _render_meson_args( + r, "include_directories", [_make_string(inc.as_posix()) for inc in incs] + ) + + +def _render_meson_args(r: RenderBuffer, name: str, args: T.List[str]): + r.writeln(f"{name}: [") + with r.indent(): + for arg in args: + r.writeln(f"{arg},") + r.writeln("],") + + +def _render_module_stage0( + r: RenderBuffer, + vc: VarCache, + m: ExtensionModule, + meson_build_path: T.Optional[pathlib.Path], +): + + # variables generated here should be deterministic so that users can add + # their own things to it, or use it directly + + r.writeln(f"# {m.package_name}") + r.writeln(f"{m.name}_sources = []") + r.writeln(f"{m.name}_deps = [declare_dependency(") + with r.indent(): + + if m.sources: + r.writeln("sources: [") + with r.indent(): + for src in m.sources: + r.writeln(f"{vc.getvar(src)},") + + r.writeln("],") + + if m.depends: + depnames = [] + for d in m.depends: + if isinstance(d, LocalDependency): + depnames.append(vc.getvar(d)) + else: + depnames.append(f"dependency({_make_string(d)})") + + deps = ", ".join(depnames) + r.writeln(f"dependencies: [{deps}],") + + if m.include_directories: + _render_include_directories(r, m.include_directories, meson_build_path) + + r.writeln(")]") + + +def _render_module_stage1( + r: RenderBuffer, + vc: VarCache, + m: ExtensionModule, + meson_build_path: T.Optional[pathlib.Path], +): + + # variables generated here should be deterministic so that users can + # use it directly if they wish + + subdir = _make_string(m.install_path.as_posix()) + module_name = _make_string(m.package_name.split(".")[-1]) + mvar = vc.getvar(m) + + r.writeln(f"# {m.package_name}") + r.writeln(f"{mvar} = sw_py.extension_module(") + with r.indent(): + r.write_trim( + f""" + {module_name}, + sources: [{m.name}_sources], + dependencies: [{m.name}_deps], + install: true, + subdir: {subdir}, + """ + ) + + if m.include_directories: + _render_include_directories(r, m.include_directories, meson_build_path) + + r.writeln(")") + r.writeln() + + +def render_meson( + project_root: pathlib.Path, + stage0_path: T.Optional[pathlib.Path], + stage1_path: T.Optional[pathlib.Path], + trampolines_path: T.Optional[pathlib.Path], +) -> T.Tuple[str, str, str, T.List[Entrypoint]]: + """ + Returns the contents of two meson.build files that build on each other, and + any entry points that need to be created + """ + + eps: T.List[Entrypoint] = [] + + r0 = RenderBuffer() + r1 = RenderBuffer() + t = RenderBuffer() + vc = VarCache() + + # standard boilerplate here + r0.write_trim( + """ + # This file is automatically generated, DO NOT EDIT + # + # The generator's stable API includes variables that do not start with + # an underscore. Any variables with an underscore may change in the future + # without warning + # + + sw_py = import('python').find_installation() + + # internal commands for the autowrap machinery + _sw_cmd_gen_libinit_py = [sw_py, '-m', 'semiwrap.cmd.gen_libinit'] + _sw_cmd_gen_pkgconf = [sw_py, '-m', 'semiwrap.cmd.gen_pkgconf'] + _sw_cmd_publish_casters = [sw_py, '-m', 'semiwrap.cmd.publish_casters'] + _sw_cmd_resolve_casters = [sw_py, '-m', 'semiwrap.cmd.resolve_casters'] + _sw_cmd_header2dat = [sw_py, '-m', 'semiwrap.cmd.header2dat'] + _sw_cmd_dat2cpp = [sw_py, '-m', 'semiwrap.cmd.dat2cpp'] + _sw_cmd_dat2trampoline = [sw_py, '-m', 'semiwrap.cmd.dat2trampoline'] + _sw_cmd_dat2tmplcpp = [sw_py, '-m', 'semiwrap.cmd.dat2tmplcpp'] + _sw_cmd_dat2tmplhpp = [sw_py, '-m', 'semiwrap.cmd.dat2tmplhpp'] + _sw_cmd_gen_modinit_hpp = [sw_py, '-m', 'semiwrap.cmd.gen_modinit_hpp'] + _sw_cmd_make_pyi = [sw_py, '-m', 'semiwrap.cmd.make_pyi'] + + # + # internal custom targets for generating wrappers + # + """ + ) + r0.writeln() + + # ... so, we could write out proper loops and stuff to make the output + # meson file easier to read, but I think expanded custom targets + # is simpler to generate + + plan = makeplan(pathlib.Path(project_root)) + macros: T.List[CppMacroValue] = [] + build_targets: T.List[BuildTarget] = [] + modules: T.List[ExtensionModule] = [] + pyi_targets: T.List[BuildTarget] = [] + local_deps: T.List[LocalDependency] = [] + trampoline_targets: T.List[BuildTarget] = [] + + for item in plan: + if isinstance(item, BuildTarget): + if item.command == "make-pyi": + # defer these to the end + pyi_targets.append(item) + elif item.command == "dat2trampoline": + trampoline_targets.append(item) + else: + build_targets.append(item) + elif isinstance(item, ExtensionModule): + # defer these to the end + modules.append(item) + elif isinstance(item, Entrypoint): + eps.append(item) + elif isinstance(item, LocalDependency): + local_deps.append(item) + elif isinstance(item, CppMacroValue): + macros.append(item) + else: + assert False + + if macros: + for macro in macros: + macro_name = _make_string(macro.name) + r0.writeln( + f"{vc.getvar(macro)} = meson.get_compiler('cpp').get_define({macro_name})" + ) + r0.writeln() + + for target in build_targets: + _render_build_target(r0, vc, target) + + if local_deps: + r0.writeln() + r0.write_trim( + """ + # + # Local dependencies + # + """ + ) + for ld in local_deps: + r0.writeln(f"{vc.getvar(ld)} = declare_dependency(") + with r0.indent(): + if ld.depends: + deps = [] + for dep in ld.depends: + if isinstance(dep, LocalDependency): + deps.append(vc.getvar(dep)) + else: + deps.append(f"dependency({_make_string(dep)})") + + depstrs = ", ".join(deps) + r0.writeln(f"dependencies: [{depstrs}],") + + if ld.include_paths: + _render_include_directories(r0, ld.include_paths, stage0_path) + + r0.writeln(")") + + if trampoline_targets: + t.writeln("# This file is automatically generated, DO NOT EDIT\n\n") + for target in trampoline_targets: + _render_build_target(t, vc, target) + + r0.writeln() + r0.writeln("subdir('trampolines')") + + if modules: + r0.writeln() + r0.write_trim( + """ + # + # Module configurations + # + """ + ) + r0.writeln() + + r1.writeln("# This file is automatically generated, DO NOT EDIT\n\n") + + for module in modules: + _render_module_stage0(r0, vc, module, stage0_path) + _render_module_stage1(r1, vc, module, stage1_path) + + # TODO: this conditional probably should be done in meson instead + # cannot build pyi files when cross-compiling + if not ( + "_PYTHON_HOST_PLATFORM" in os.environ + or "PYTHON_CROSSENV" in os.environ + or os.environ.get("SEMIWRAP_SKIP_PYI") == "1" + ): + for target in pyi_targets: + _render_build_target(r1, vc, target) + + return r0.getvalue(), r1.getvalue(), t.getvalue(), eps + + +def render_meson_to_file( + project_root: pathlib.Path, + stage0: pathlib.Path, + stage1: pathlib.Path, + trampolines: pathlib.Path, +) -> T.List[Entrypoint]: + + # because of https://github.com/mesonbuild/meson/issues/2320 + assert trampolines.parent.parent == stage0.parent + + s0_content, s1_content, t_content, eps = render_meson( + pathlib.Path(project_root), stage0, stage1, trampolines + ) + + maybe_write_file(stage0, s0_content, encoding="utf-8") + maybe_write_file(stage1, s1_content, encoding="utf-8") + maybe_write_file(trampolines, t_content, encoding="utf-8") + + return eps + + +def main(): + def _usage() -> T.NoReturn: + print(f"{sys.argv[0]} project_root stage0 [stage1]", file=sys.stderr) + sys.exit(1) + + # this entrypoint only for debugging + try: + _, project_root = sys.argv + except ValueError: + _usage() + + s0_content, s1_content, t_content, _ = render_meson( + pathlib.Path(project_root), None, None, None + ) + + print(s0_content) + print("\n---\n") + print(s1_content) + print("\n---\n") + print(t_content) + + +if __name__ == "__main__": + main() diff --git a/src/semiwrap/semiwrap-pybind11.pc b/src/semiwrap/semiwrap-pybind11.pc new file mode 100644 index 00000000..3ed3bcc1 --- /dev/null +++ b/src/semiwrap/semiwrap-pybind11.pc @@ -0,0 +1,8 @@ +prefix=${pcfiledir} +includedir=${prefix}/pybind11/include + +Name: semiwrap-pybind11 +Description: Semiwrap specific version of pybind11 +Version: 3.0.0.dev1 +Cflags: -I${includedir} +Libs: diff --git a/src/semiwrap/semiwrap.pc b/src/semiwrap/semiwrap.pc new file mode 100644 index 00000000..cb7ca174 --- /dev/null +++ b/src/semiwrap/semiwrap.pc @@ -0,0 +1,9 @@ +prefix=${pcfiledir} +includedir=${prefix}/include + +Name: semiwrap +Description: Semiwrap compile flags and include directories +Version: 2025 +Requires: semiwrap-pybind11 +Cflags: -I${includedir} +Libs: diff --git a/src/semiwrap/semiwrap.pybind11.json b/src/semiwrap/semiwrap.pybind11.json new file mode 100644 index 00000000..f61c9ad5 --- /dev/null +++ b/src/semiwrap/semiwrap.pybind11.json @@ -0,0 +1,66 @@ +{ + "headers": [ + { + "header": "pybind11/stl.h", + "types": [ + "std::vector", + "std::deque", + "std::list", + "std::array", + "std::valarray", + "std::set", + "std::map", + "std::unordered_map", + "std::optional", + "std::nullopt_t", + "std::variant" + ] + }, + { + "header": "pybind11/functional.h", + "types": [ + "std::function" + ] + }, + { + "header": "pybind11/complex.h", + "types": [ + "std::complex" + ] + }, + { + "header": "pybind11/chrono.h", + "types": [ + "std::chrono::duration", + "std::chrono::time_point" + ] + }, + { + "header": "pybind11/eigen.h", + "types": [ + "Eigen::Block", + "Eigen::DiagonalMatrix", + "Eigen::MatrixBase", + "Eigen::Matrix", + "Eigen::Matrix2d", + "Eigen::Matrix3d", + "Eigen::MatrixXcd", + "Eigen::MatrixXd", + "Eigen::MatrixXdR", + "Eigen::MatrixXi", + "Eigen::MatrixXf", + "Eigen::Ref", + "Eigen::Matrix4d", + "Eigen::RowVectorXf", + "Eigen::SparseMatrix", + "Eigen::SparseView", + "Eigen::Vector", + "Eigen::Vector2d", + "Eigen::Vector3d", + "Eigen::VectorXd", + "Eigen::VectorXf", + "Eigen::VectorXcf" + ] + } + ] +} \ No newline at end of file diff --git a/src/semiwrap/setup.py b/src/semiwrap/setup.py deleted file mode 100644 index 3f44498d..00000000 --- a/src/semiwrap/setup.py +++ /dev/null @@ -1,226 +0,0 @@ -import os -from os.path import abspath, exists, join -from setuptools import find_packages, setup as _setup -from setuptools_scm import get_version -import tomli - -try: - from wheel.bdist_wheel import bdist_wheel as _bdist_wheel - - class bdist_wheel(_bdist_wheel): - def finalize_options(self): - _bdist_wheel.finalize_options(self) - self.root_is_pure = False - -except ImportError: - bdist_wheel = None # type: ignore - -from .autowrap.writer import WrapperWriter - -from .command.build_py import BuildPy -from .command.build_dl import BuildDl -from .command.build_gen import BuildGen -from .command.build_ext import BuildExt -from .command.build_pyi import BuildPyi -from .command.develop import Develop -from .command.update_init import UpdateInit - -try: - from .command.editable_wheel import EditableWheel -except ImportError: - EditableWheel = None # type: ignore - -from .config.util import parse_input -from .config.pyproject_toml import RobotpyBuildConfig - -from .maven import convert_maven_to_downloads -from .overrides import apply_overrides -from .pkgcfg_provider import PkgCfgProvider -from .platforms import get_platform, get_platform_override_keys -from .static_libs import StaticLib -from .wrapper import Wrapper - - -class Setup: - """ - Hacky wrapper around setuptools because it's easier than copy/pasting - this stuff to a million setup.py files - """ - - def __init__(self): - self.root = abspath(os.getcwd()) - self.wrappers = [] - self.static_libs = [] - - self.platform = get_platform() - - project_fname = join(self.root, "pyproject.toml") - - try: - with open(project_fname, "rb") as fp: - self.pyproject = tomli.load(fp) - except FileNotFoundError as e: - raise ValueError("current directory is not a robotpy-build project") from e - - self.project_dict = self.pyproject.get("tool", {}).get("robotpy-build", {}) - - # Overrides are applied before processing, so that we can easily - # override anything without needing to make the dataclasses messy - # with needless details - override_keys = get_platform_override_keys(self.platform) - apply_overrides(self.project_dict, override_keys) - - try: - self.project = parse_input( - self.project_dict, RobotpyBuildConfig, project_fname - ) - except Exception as e: - raise ValueError( - f"robotpy-build configuration in pyproject.toml is incorrect" - ) from e - - # Shared wrapper writer instance - self.wwriter = WrapperWriter() - - @property - def base_package(self): - return self.project.base_package - - @property - def base_package_path(self): - return join(self.root, *self.base_package.split(".")) - - @property - def git_dir(self): - return join(self.root, ".git") - - @property - def pypi_package(self) -> str: - return self.setup_kwargs["name"] - - def prepare(self): - self.setup_kwargs = self.project_dict.get("metadata", {}) - self.setup_kwargs["zip_safe"] = False - self.setup_kwargs["include_package_data"] = True - self.setup_kwargs["python_requires"] = ">=3.8" - - self._generate_long_description() - - # get_version expects the directory to exist - base_package_path = self.base_package_path - os.makedirs(base_package_path, exist_ok=True) - this_version = get_version( - write_to=join(base_package_path, "version.py"), - fallback_version="master", - search_parent_directories=True, - ) - self.setup_kwargs["version"] = this_version - - # Support ==THIS_VERSION - install_requires = self.setup_kwargs.get("install_requires") - if install_requires: - - def _xform(v: str): - if v.endswith("==THIS_VERSION"): - v = f"{v[:-14]}=={this_version}" - return v - - self.setup_kwargs["install_requires"] = list(map(_xform, install_requires)) - - self.pkgcfg = PkgCfgProvider() - - self._collect_static_libs() - self._collect_wrappers() - - self.pkgcfg.detect_pkgs() - - self.setup_kwargs["cmdclass"] = { - "build_py": BuildPy, - "build_dl": BuildDl, - "build_gen": BuildGen, - "build_ext": BuildExt, - "build_pyi": BuildPyi, - "develop": Develop, - "update_init": UpdateInit, - } - if EditableWheel: - self.setup_kwargs["cmdclass"]["editable_wheel"] = EditableWheel - if bdist_wheel: - self.setup_kwargs["cmdclass"]["bdist_wheel"] = bdist_wheel - for cls in self.setup_kwargs["cmdclass"].values(): - cls.wrappers = self.wrappers - cls.static_libs = self.static_libs - cls.rpybuild_pkgcfg = self.pkgcfg - BuildPyi.base_package = self.base_package - UpdateInit.update_list = self.project.update_init - - # We already know some of our packages, so collect those in addition - # to using find_packages() - packages = {w.package_name for w in self.wrappers} - packages.update(find_packages()) - self.setup_kwargs["packages"] = list(packages) - - def _generate_long_description(self): - readme_rst = join(self.root, "README.rst") - readme_md = join(self.root, "README.md") - if exists(readme_rst): - self.setup_kwargs["long_description_content_type"] = "text/x-rst" - with open(readme_rst) as fp: - self.setup_kwargs["long_description"] = fp.read() - - elif exists(readme_md): - self.setup_kwargs["long_description_content_type"] = "text/markdown" - with open(readme_md) as fp: - self.setup_kwargs["long_description"] = fp.read() - - def _collect_wrappers(self): - ext_modules = [] - - for package_name, cfg in self.project.wrappers.items(): - if cfg.ignore: - continue - self._fix_downloads(cfg, False) - w = Wrapper(package_name, cfg, self, self.wwriter) - self.wrappers.append(w) - self.pkgcfg.add_pkg(w) - - if w.extension: - ext_modules.append(w.extension) - - if ext_modules: - self.setup_kwargs["ext_modules"] = ext_modules - - def _collect_static_libs(self): - for name, cfg in self.project.static_libs.items(): - if cfg.ignore: - continue - self._fix_downloads(cfg, True) - if not cfg.download: - raise ValueError(f"static_lib {name} must specify downloads") - s = StaticLib(name, cfg, self) - self.static_libs.append(s) - self.pkgcfg.add_pkg(s) - - def _fix_downloads(self, cfg, static: bool): - # maven is just a special case of a download - if cfg.maven_lib_download: - downloads = convert_maven_to_downloads(cfg.maven_lib_download, static) - cfg.maven_lib_download = None - if cfg.download: - cfg.download.append(downloads) - else: - cfg.download = downloads - - if cfg.download: - for dl in cfg.download: - dl._update_with_platform(self.platform) - - def run(self): - # assemble all the pieces and make it work - _setup(**self.setup_kwargs) - - -def setup(): - s = Setup() - s.prepare() - s.run() diff --git a/src/semiwrap/static_libs.py b/src/semiwrap/static_libs.py deleted file mode 100644 index 0f0680bc..00000000 --- a/src/semiwrap/static_libs.py +++ /dev/null @@ -1,114 +0,0 @@ -import os -from os.path import join -import posixpath -import shutil -from typing import Any, Dict, List, Optional - -from .download import download_and_extract_zip -from .config.pyproject_toml import Download, StaticLibConfig - - -class StaticLib: - # implements pkgcfg - - def __init__(self, name: str, cfg: StaticLibConfig, setup): - self.package_name = name - self.name = name - self.cfg = cfg - self.static_lib = True - self.libinit_import = None - self.pypi_package = None - # TODO - self.depends = [] - - self.platform = setup.platform - - self.root: Optional[os.PathLike] = None - self.incdir: Optional[str] = None - self.libdir: Optional[str] = None - - def set_root(self, root: os.PathLike) -> None: - self.root = root - self.libdir = join(self.root, self.name, "lib") - self.incdir = join(self.root, self.name, "include") - - def get_include_dirs(self) -> Optional[List[str]]: - if self.incdir is None: - return - - includes = [self.incdir] - if self.cfg.download: - for dl in self.cfg.download: - if dl.extra_includes: - includes += [join(self.incdir, inc) for inc in dl.extra_includes] - return includes - - def get_library_dirs(self) -> Optional[List[str]]: - if self.libdir: - return [self.libdir] - - def get_library_dirs_rel(self) -> Optional[List[str]]: - pass - - def get_library_names(self) -> Optional[List[str]]: - # don't do this except on Windows - if self.platform.os != "windows": - return - - return self._get_libnames(useext=False) - - def get_library_full_names(self) -> Optional[List[str]]: - pass - - def get_extra_objects(self) -> Optional[List[str]]: - if self.platform.os == "windows": - return - - if self.libdir: - return [join(self.libdir, lib) for lib in self._get_libnames()] - - def get_type_casters_cfg(self, casters: Dict[str, Dict[str, Any]]) -> None: - pass - - def _get_dl_libnames(self, dl: Download, useext=True): - ext = "" - if useext: - ext = self.platform.staticext - return [f"{self.platform.libprefix}{lib}{ext}" for lib in dl.libs] - - def _get_libnames(self, useext=True): - libs = [] - for dl in self.cfg.download: - if dl.libs: - libs += self._get_dl_libnames(dl, useext) - return libs - - def on_build_dl(self, cache: str, libdir: str): - self.set_root(libdir) - - shutil.rmtree(self.libdir, ignore_errors=True) - shutil.rmtree(self.incdir, ignore_errors=True) - - os.makedirs(self.libdir) - - for dl in self.cfg.download: - if dl.sources is not None: - raise ValueError(f"{dl.url}: cannot specify sources in static lib") - - if dl.libs is None: - if dl.incdir is None: - raise ValueError(f"{dl.url}: must specify libs in static lib") - to = {} - else: - to = { - posixpath.join(dl.libdir, libname): join(self.libdir, libname) - for libname in self._get_dl_libnames(dl) - } - - if dl.incdir is not None: - to[dl.incdir] = self.incdir - - if dl.dlopenlibs is not None: - raise ValueError(f"{dl.url}: cannot specify dlopenlibs in static lib") - - download_and_extract_zip(dl.url, to, cache) diff --git a/src/semiwrap/util.py b/src/semiwrap/util.py new file mode 100644 index 00000000..fa3920c6 --- /dev/null +++ b/src/semiwrap/util.py @@ -0,0 +1,27 @@ +import os.path +import pathlib +import typing as T + + +def maybe_write_file( + path: pathlib.Path, content: str, *, encoding: T.Optional[str] = None +) -> bool: + # returns True if new content written + if path.exists(): + with open(path, encoding=encoding) as fp: + oldcontent = fp.read() + if oldcontent == content: + return False + elif not path.parent.exists(): + path.parent.mkdir(parents=True) + + with open(path, "w", encoding=encoding) as fp: + fp.write(content) + + return True + + +def relpath_walk_up(p: pathlib.Path, other: pathlib.Path) -> pathlib.Path: + # walk_up=True was introduced in Python 3.12 so can't use that + # p.relative_to(other, walk_up=True) + return pathlib.Path(os.path.relpath(p, other)) diff --git a/src/semiwrap/wrapper.py b/src/semiwrap/wrapper.py deleted file mode 100644 index 2176d9e6..00000000 --- a/src/semiwrap/wrapper.py +++ /dev/null @@ -1,829 +0,0 @@ -import glob -import json -import inspect -import os -from os.path import ( - abspath, - basename, - dirname, - exists, - isdir, - join, - normpath, - relpath, - sep, - splitext, -) -import pathlib -import posixpath -import shutil -import sysconfig -import toposort -from typing import Any, Dict, List, Optional, Set, Tuple - -from urllib.error import HTTPError -import dataclasses - -from setuptools import Extension - -from cxxheaderparser.options import ParserOptions -from cxxheaderparser import preprocessor - - -from .download import download_and_extract_zip -from .config.pyproject_toml import PatchInfo, WrapperConfig, Download - -from .autowrap.cxxparser import parse_header -from .autowrap.generator_data import GeneratorData, MissingReporter -from .autowrap.writer import WrapperWriter - -from .config.autowrap_yml import AutowrapConfigYaml -from .config.dev_yml import get_dev_config -from .config.pyproject_toml import WrapperConfig, Download - -# TODO: eventually provide native preprocessor by default and allow it -# to be enabled/disabled per-file just in case -if os.getenv("RPYBUILD_PP_GCC") == "1": - # GCC preprocessor can be 10x faster than pcpp for very complex files - def make_preprocessor(*args, **kwargs): - return preprocessor.make_gcc_preprocessor(print_cmd=False, *args, **kwargs) - -else: - make_preprocessor = preprocessor.make_pcpp_preprocessor - - -class Wrapper: - """ - Wraps downloading bindings and generating them - """ - - # Used during preprocessing - # -> should we change this based on what flags the compiler supports? - _cpp_version = "__cplusplus 201703L" - - def __init__(self, package_name, cfg: WrapperConfig, setup, wwriter: WrapperWriter): - self.package_name = package_name - self.cfg = cfg - self.wwriter = wwriter - - self.setup_root = setup.root - self.pypi_package = setup.pypi_package - self.root = join(setup.root, *package_name.split(".")) - - # must match PkgCfg.name - self.name = cfg.name - self.static_lib = False - - # Compute the extension name, even if we don't create one - extname = cfg.extension - if not extname: - extname = f"_{cfg.name}" - - # must match PkgCfg.libinit_import - if cfg.libinit: - libinit_py = cfg.libinit - if libinit_py == "__init__.py": - self.libinit_import = package_name - else: - pkg = splitext(libinit_py)[0] - self.libinit_import = f"{package_name}.{pkg}" - else: - libinit_py = f"_init{extname}.py" - self.libinit_import = f"{package_name}._init{extname}" - - self.libinit_import_py = join(self.root, libinit_py) - - self.platform = setup.platform - self.pkgcfg = setup.pkgcfg - - # Used by pkgcfg - self.depends = self.cfg.depends - - # Files that are generated AND need to be in the final wheel. Used by build_py - self.additional_data_files: List[str] = [] - - self._all_deps = None - - self._gen_includes = [] - - self.extension = None - if self.cfg.sources or self.cfg.autogen_headers: - define_macros = [("RPYBUILD_MODULE_NAME", extname)] + [ - tuple(d.split(" ")) for d in self.platform.defines - ] - define_macros += [tuple(m.split(" ", 1)) for m in self.cfg.pp_defines] - - # extensions just hold data about what to actually build, we can - # actually modify extensions all the way up until the build - # really happens - extname_full = f"{self.package_name}.{extname}" - self.extension = Extension( - extname_full, - self.cfg.sources, - define_macros=define_macros, - language="c++", - ) - - # Add self to extension so that build_ext can query it on OSX - self.extension.rpybuild_wrapper = self - - # Used if the maven download fails - self.supported_platforms = setup.project.supported_platforms - - if self.cfg.autogen_headers and not self.cfg.generation_data: - raise ValueError( - "generation_data must be specified when autogen_headers/generate is specified" - ) - - # Setup an entry point (written during build_clib) - entry_point = f"{self.cfg.name} = {self.package_name}.pkgcfg" - - setup_kwargs = setup.setup_kwargs - ep = setup_kwargs.setdefault("entry_points", {}) - ep.setdefault("robotpybuild", []).append(entry_point) - - self.incdir = join(self.root, "include") - self.rpy_incdir = join(self.root, "rpy-include") - - self.dev_config = get_dev_config(self.name) - - self._update_addl_data_files() - - def _extract_zip_to(self, dl: Download, dst, cache): - try: - download_and_extract_zip(dl.url, dst, cache) - except HTTPError as e: - # Check for a 404 error and raise an error if the platform isn't supported. - if e.code != 404: - raise e - else: - platform_dict = dataclasses.asdict(self.platform) - - os = platform_dict["os"] - arch = platform_dict["arch"] - - is_os_supported = False - is_arch_supported = False - - for supp_plat in self.supported_platforms: - if supp_plat.os is None or supp_plat.os == os: - is_os_supported = True - if supp_plat.arch is None or supp_plat.arch == arch: - is_arch_supported = True - - if not (is_os_supported and is_arch_supported): - if arch == "x86": - arch = "32-bit" - elif arch == "x86-64": - arch = "64-bit" - - if os == "osx": - os = "macOS" - - if not is_os_supported: - arch = "" - - msg_plat = "{}{}{}".format(arch, " " if arch != "" else "", os) - - err_msg = "{} is not supported on {}!".format( - self.pypi_package, msg_plat - ) - - raise OSError(err_msg) - raise e - - def _add_addl_data_file(self, fullpath): - if not isdir(fullpath): - self.additional_data_files.append(relpath(fullpath, self.root)) - - # pkgcfg interface - def get_include_dirs(self) -> List[str]: - includes = [self.incdir, self.rpy_incdir] - if self.cfg.download: - for dl in self.cfg.download: - if dl.extra_includes: - includes += [join(self.incdir, inc) for inc in dl.extra_includes] - for h in self.cfg.extra_includes: - includes.append(join(self.setup_root, normpath(h))) - return includes - - def get_library_dirs(self) -> Optional[List[str]]: - if self.get_library_full_names(): - return [join(self.root, "lib")] - return [] - - def get_library_dirs_rel(self) -> Optional[List[str]]: - if self.get_library_full_names(): - return ["lib"] - return [] - - def get_library_names(self) -> Optional[List[str]]: - libs = [] - if self.cfg.download: - for dl in self.cfg.download: - if dl.libs: - libs += dl.libs - return libs - - def get_library_full_names(self) -> Optional[List[str]]: - if not self.cfg.download: - return [] - - dlopen_libnames = self.get_dlopen_library_names() - - libnames_full = [] - - for dl in self.cfg.download: - libext = dl.libexts.get(self.platform.libext, self.platform.libext) - if dl.libs: - for lib in dl.libs: - if lib not in dlopen_libnames: - libnames_full.append(f"{self.platform.libprefix}{lib}{libext}") - if dl.dlopenlibs: - libnames_full += [ - f"{self.platform.libprefix}{lib}{libext}" for lib in dl.dlopenlibs - ] - - return libnames_full - - def get_dlopen_library_names(self) -> Optional[List[str]]: - libs = [] - if self.cfg.download: - for dl in self.cfg.download: - if dl.dlopenlibs: - libs += dl.dlopenlibs - return libs - - def get_extra_objects(self) -> Optional[List[str]]: - pass - - def get_type_casters_cfg(self, casters: Dict[str, Dict[str, Any]]) -> None: - for ccfg in self.cfg.type_casters: - cfg = {"hdr": ccfg.header} - if ccfg.default_arg_cast: - cfg["darg"] = True - - for typ in ccfg.types: - casters[typ] = cfg - - def _update_addl_data_files(self) -> List[str]: - headers = set() - for ccfg in self.cfg.type_casters: - headers.add(ccfg.header) - - if headers: - includes = self.get_include_dirs() - if includes: - for hdr in headers: - for p in includes: - fpath = join(p, hdr) - if exists(fpath): - self._add_addl_data_file(fpath) - - def all_deps(self): - if self._all_deps is None: - self._all_deps = self.pkgcfg.get_all_deps(self.name) - return self._all_deps - - def _all_includes(self, include_rpyb): - includes = self.get_include_dirs() - for dep in self.all_deps(): - dep_inc = dep.get_include_dirs() - if dep_inc: - includes.extend(dep_inc) - if include_rpyb: - includes.extend(self.pkgcfg.get_pkg("robotpy-build").get_include_dirs()) - return includes - - def _generation_search_path(self): - return [self.root] + self._all_includes(False) - - def _all_library_dirs(self): - libs = self.get_library_dirs() - for dep in self.cfg.depends: - libdirs = self.pkgcfg.get_pkg(dep).get_library_dirs() - if libdirs: - libs.extend(libdirs) - return libs - - def _all_library_names(self): - libs = list( - set(self.get_library_names()) | set(self.get_dlopen_library_names()) - ) - for dep in self.cfg.depends: - pkg = self.pkgcfg.get_pkg(dep) - libnames = pkg.get_library_names() - if libnames: - libs.extend(libnames) - return list(reversed(libs)) - - def _all_extra_objects(self): - libs = [] - for dep in self.cfg.depends: - pkg = self.pkgcfg.get_pkg(dep) - libnames = pkg.get_extra_objects() - if libnames: - libs.extend(libnames) - return list(reversed(libs)) - - def _all_casters(self): - casters = {} - for dep in self.all_deps(): - dep.get_type_casters_cfg(casters) - self.pkgcfg.get_pkg("robotpy-build").get_type_casters_cfg(casters) - self.get_type_casters_cfg(casters) - - # make each configuration unique - for k, v in list(casters.items()): - v = v.copy() - v["typename"] = k - casters[k] = v - - # add non-namespaced versions of all casters - # -> in theory this could lead to a conflict, but - # let's see how it works in practice? - for k, v in list(casters.items()): - k = k.split("::")[-1] - casters[k] = v - return casters - - def on_build_dl(self, cache: str, srcdir: str): - pkgcfgpy = join(self.root, "pkgcfg.py") - srcdir = join(srcdir, self.name) - - try: - os.unlink(self.libinit_import_py) - except OSError: - pass - - try: - os.unlink(pkgcfgpy) - except OSError: - pass - - libnames_full = [] - all_libs = [] - downloads = self.cfg.download - if downloads: - libnames_full, all_libs = self._clean_and_download(downloads, cache, srcdir) - - self._write_libinit_py(libnames_full) - self._write_pkgcfg_py(pkgcfgpy, libnames_full) - - return all_libs - - def _apply_patches(self, patches: List[PatchInfo], root: str): - import patch - - for p in patches: - patch_path = join(self.setup_root, normpath(p.patch)) - ps = patch.PatchSet() - with open(patch_path, "rb") as fp: - if not ps.parse(fp): - raise ValueError(f"Error parsing patch '{patch_path}'") - - if not ps.apply(strip=p.strip, root=root): - raise ValueError(f"Error applying patch '{patch_path}' to '{root}'") - - def _clean_and_download( - self, downloads: List[Download], cache: str, srcdir: str - ) -> Tuple[List[str], List[str]]: - libdir = join(self.root, "lib") - incdir = join(self.root, "include") - - add_libdir = False - add_incdir = False - - # Remove downloaded/generated artifacts first - shutil.rmtree(libdir, ignore_errors=True) - shutil.rmtree(incdir, ignore_errors=True) - shutil.rmtree(srcdir, ignore_errors=True) - - dlopen_libnames = self.get_dlopen_library_names() - libnames_full = [] - all_libs = [] - - for dl in downloads: - # extract the whole thing into a directory when using for sources - if dl.sources is not None: - download_and_extract_zip(dl.url, srcdir, cache) - sources = [join(srcdir, normpath(s)) for s in dl.sources] - self.extension.sources.extend(sources) - if dl.patches: - self._apply_patches(dl.patches, srcdir) - elif dl.sources is not None: - raise ValueError("sources must be None if use_sources is False!") - elif dl.patches is not None: - raise ValueError("patches must be None if use_sources is False!") - - if dl.libs or dl.dlopenlibs: - add_libdir = True - extract_names = [] - os.makedirs(libdir) - - libext = dl.libexts.get(self.platform.libext, self.platform.libext) - linkext = dl.linkexts.get(self.platform.linkext, self.platform.linkext) - if dl.libs: - for lib in dl.libs: - if lib not in dlopen_libnames: - name = f"{self.platform.libprefix}{lib}{libext}" - libnames_full.append(name) - extract_names.append(name) - if libext != linkext: - extract_names.append( - f"{self.platform.libprefix}{lib}{linkext}" - ) - - if dl.dlopenlibs: - dlopen_libnames_full = [ - f"{self.platform.libprefix}{lib}{libext}" - for lib in dl.dlopenlibs - ] - libnames_full += dlopen_libnames_full - extract_names += dlopen_libnames_full - - to = { - posixpath.join(dl.libdir, libname): join(libdir, libname) - for libname in extract_names - } - all_libs.extend(to.values()) - else: - to = {} - - if dl.incdir is not None: - to[dl.incdir] = self.incdir - add_incdir = True - - download_and_extract_zip(dl.url, to, cache) - - if dl.header_patches: - self._apply_patches(dl.header_patches, incdir) - - if add_incdir: - for f in glob.glob(join(glob.escape(incdir), "**"), recursive=True): - self._add_addl_data_file(f) - - if add_libdir: - for f in glob.glob(join(glob.escape(libdir), "**"), recursive=True): - self._add_addl_data_file(f) - - return libnames_full, all_libs - - def _write_libinit_py(self, libnames): - # This file exists to ensure that any shared library dependencies - # are loaded for the compiled extension - - init = inspect.cleandoc( - """ - - # This file is automatically generated, DO NOT EDIT - # fmt: off - - from os.path import abspath, join, dirname, exists - _root = abspath(dirname(__file__)) - - ##IMPORTS## - - """ - ) - - init += "\n" - - if libnames: - if self.platform.os == "osx": - init += "from ctypes import CDLL, RTLD_GLOBAL\n\n" - else: - init += "from ctypes import cdll\n\n" - - for libname in libnames: - init += "try:\n" - if self.platform.os == "osx": - init += f' _lib = CDLL(join(_root, "lib", "{libname}"), mode=RTLD_GLOBAL)\n' - else: - init += f' _lib = cdll.LoadLibrary(join(_root, "lib", "{libname}"))\n' - - init += "except FileNotFoundError:\n" - init += f' if not exists(join(_root, "lib", "{libname}")):\n' - init += f' raise FileNotFoundError("{libname} was not found on your system. Is this package correctly installed?")\n' - if self.platform.os == "windows": - init += f' raise Exception("{libname} could not be loaded. Do you have Visual Studio C++ Redistributible 2019 installed?")\n\n' - else: - init += f' raise FileNotFoundError("{libname} could not be loaded. There is a missing dependency.")\n\n' - imports = [] - for dep in self.cfg.depends: - pkg = self.pkgcfg.get_pkg(dep) - if pkg.libinit_import: - imports.append(pkg.libinit_import) - - if imports: - imports = "# runtime dependencies\nimport " + "\nimport ".join(imports) - else: - imports = "" - - init = init.replace("##IMPORTS##", imports) - - with open(self.libinit_import_py, "w") as fp: - fp.write(init) - - self._add_addl_data_file(self.libinit_import_py) - - def _write_pkgcfg_py(self, fname, libnames_full): - library_dirs = "[]" - library_dirs_rel = [] - library_names = self.get_library_names() - if library_names: - library_dirs = '[join(_root, "lib")]' - library_dirs_rel = ["lib"] - - deps = [] - for dep in self.cfg.depends: - pkg = self.pkgcfg.get_pkg(dep) - if not pkg.static_lib: - deps.append(dep) - - # write pkgcfg.py - pkgcfg = inspect.cleandoc( - f""" - # fmt: off - # This file is automatically generated, DO NOT EDIT - - from os.path import abspath, join, dirname - _root = abspath(dirname(__file__)) - - libinit_import = "{self.libinit_import}" - depends = {repr(deps)} - pypi_package = {repr(self.pypi_package)} - - def get_include_dirs(): - return [join(_root, "include"), join(_root, "rpy-include")##EXTRAINCLUDES##] - - def get_library_dirs(): - return {library_dirs} - - def get_library_dirs_rel(): - return {repr(library_dirs_rel)} - - def get_library_names(): - return {repr(library_names)} - - def get_library_full_names(): - return {repr(libnames_full)} - """ - ) - - extraincludes = "" - if self.cfg.extra_includes: - # these are relative to the root of the project, need - # to resolve the path relative to the package - pth = join(*self.package_name.split(".")) - - for h in self.cfg.extra_includes: - h = '", "'.join(relpath(normpath(h), pth).split(sep)) - extraincludes += f', join(_root, "{h}")' - - pkgcfg = pkgcfg.replace("##EXTRAINCLUDES##", extraincludes) - - type_casters = {} - self.get_type_casters_cfg(type_casters) - if type_casters: - pkgcfg += "\n\n" - pkgcfg += inspect.cleandoc( - f""" - - def get_type_casters_cfg(casters): - casters.update({repr(type_casters)}) - - def get_type_casters(casters): - t = {{}} - get_type_casters_cfg(t) - for k, v in t.items(): - if "hdr" in v: - casters[k] = v["hdr"] - """ - ) - - with open(fname, "w") as fp: - fp.write(pkgcfg) - - self._add_addl_data_file(fname) - - def on_build_gen( - self, cxx_gen_dir, missing_reporter: Optional[MissingReporter] = None - ): - if not self.cfg.autogen_headers: - return - - cxx_gen_dir = join(cxx_gen_dir, self.name) - - if missing_reporter: - report_only = True - else: - report_only = False - missing_reporter = MissingReporter() - - hppoutdir = join(self.rpy_incdir, "rpygen") - - pp_includes = self._all_includes(True) + [sysconfig.get_path("include")] - - # TODO: only regenerate files if the generated files - # have changed - if not report_only: - if self.dev_config.only_generate is None: - shutil.rmtree(cxx_gen_dir, ignore_errors=True) - shutil.rmtree(hppoutdir, ignore_errors=True) - - os.makedirs(cxx_gen_dir, exist_ok=True) - os.makedirs(hppoutdir, exist_ok=True) - - per_header = False - data_fname = self.cfg.generation_data - if self.cfg.generation_data: - datapath = join(self.setup_root, normpath(self.cfg.generation_data)) - per_header = isdir(datapath) - if not per_header: - data = AutowrapConfigYaml.from_file(datapath) - else: - data = AutowrapConfigYaml() - - pp_defines = [self._cpp_version] + self.platform.defines + self.cfg.pp_defines - casters = self._all_casters() - - # These are written to file to make it easier for dev mode to work - classdeps = {} - - if self.dev_config.only_generate is not None: - only_generate = {n: True for n in self.dev_config.only_generate} - else: - only_generate = None - - generation_search_path = self._generation_search_path() - - for name, header in self.cfg.autogen_headers.items(): - header = normpath(header) - for path in generation_search_path: - header_path = join(path, header) - if exists(header_path): - header_root = pathlib.Path(path) - break - else: - import pprint - - pprint.pprint(generation_search_path) - raise ValueError("could not find " + header) - - if not report_only: - classdeps_dst = join(cxx_gen_dir, f"{name}.json") - classdeps[name] = classdeps_dst - - if per_header: - data_fname = join(datapath, name + ".yml") - if not exists(data_fname): - print("WARNING: could not find", data_fname) - data = AutowrapConfigYaml() - else: - data = AutowrapConfigYaml.from_file(data_fname) - - if only_generate is not None and not only_generate.pop(name, False): - continue - - popts = ParserOptions( - preprocessor=make_preprocessor( - defines=pp_defines, - include_paths=pp_includes, - encoding=data.encoding, - ) - ) - - gendata = GeneratorData(data, data_fname) - - try: - hctx = parse_header( - name, - pathlib.Path(header_path), - header_root, - gendata, - popts, - casters, - report_only, - ) - - if not report_only: - generated_sources = self.wwriter.write_files( - hctx, name, cxx_gen_dir, hppoutdir, classdeps_dst - ) - self.extension.sources.extend( - [relpath(src, self.setup_root) for src in generated_sources] - ) - except Exception as e: - raise ValueError(f"processing {header}") from e - - gendata.report_missing(data_fname, missing_reporter) - - if only_generate: - unused = ", ".join(sorted(only_generate)) - # raise ValueError(f"only_generate specified unused headers! {unused}") - # TODO: make this a warning - - if not report_only: - for name, contents in missing_reporter.as_yaml(): - print("WARNING: some items not in generation yaml for", basename(name)) - print(contents) - - # generate an inline file that can be included + called - if not report_only: - self._write_wrapper_hpp(cxx_gen_dir, classdeps) - gen_includes = [cxx_gen_dir] - else: - gen_includes = [] - - self._gen_includes = gen_includes - - for f in glob.glob(join(glob.escape(hppoutdir), "*.hpp")): - self._add_addl_data_file(f) - - def finalize_extension(self): - if self.extension is None: - return - - # Add the root to the includes (but only privately) - root_includes = [self.root] - - # update the build extension so that build_ext works - # use normpath to get rid of .. otherwise gcc is weird - self.extension.include_dirs = [ - normpath(p) - for p in (self._all_includes(True) + self._gen_includes + root_includes) - ] - self.extension.library_dirs = self._all_library_dirs() - self.extension.libraries = self._all_library_names() - self.extension.extra_objects = self._all_extra_objects() - - def _write_wrapper_hpp(self, outdir, classdeps): - decls = [] - begin_calls = [] - finish_calls = [] - - # Need to ensure that wrapper initialization is called in base order - # so we have to toposort it here. The data is written at gen time - # to JSON files - types2name = {} - types2deps = {} - ordering = [] - - for name, jsonfile in classdeps.items(): - with open(jsonfile) as fp: - dep = json.load(fp) - - # make sure objects without classes are also included! - if not dep: - ordering.append(name) - - for clsname, bases in dep.items(): - if clsname in types2name: - raise ValueError(f"{name} ({jsonfile}): duplicate class {clsname}") - types2name[clsname] = name - types2deps[clsname] = bases[:] - - to_sort: Dict[str, Set[str]] = {} - for clsname, bases in types2deps.items(): - clsname = types2name[clsname] - deps = to_sort.setdefault(clsname, set()) - for base in bases: - base = types2name.get(base) - if base and base != clsname: - deps.add(base) - - ordering.extend(toposort.toposort_flatten(to_sort, sort=True)) - - for name in ordering: - decls.append(f"void begin_init_{name}(py::module &m);") - decls.append(f"void finish_init_{name}();") - begin_calls.append(f" begin_init_{name}(m);") - finish_calls.append(f" finish_init_{name}();") - - content = ( - inspect.cleandoc( - """ - - // This file is autogenerated, DO NOT EDIT - #pragma once - #include - - // forward declarations - ##DECLS## - - static void initWrapper(py::module &m) { - ##BEGIN_CALLS## - - ##FINISH_CALLS## - } - - """ - ) - .replace("##DECLS##", "\n".join(decls)) - .replace("##BEGIN_CALLS##", "\n".join(begin_calls)) - .replace("##FINISH_CALLS##", "\n".join(finish_calls)) - ) - - with open(join(outdir, "rpygen_wrapper.hpp"), "w") as fp: - fp.write(content) From 451e8fb61999c1ba8153d4b049c947423f6a831b Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 6 May 2025 01:35:16 -0400 Subject: [PATCH 06/11] Fix semiwrap tools --- src/semiwrap/tool/__init__.py | 1 + src/semiwrap/tool/__main__.py | 17 +-- src/semiwrap/tool/build_dep.py | 10 +- src/semiwrap/tool/create_gen.py | 49 -------- src/semiwrap/tool/create_imports.py | 50 +++++++- src/semiwrap/tool/create_yaml.py | 103 +++++++++++++++++ src/semiwrap/tool/parse_maven.py | 172 ---------------------------- src/semiwrap/tool/platform_info.py | 29 ----- src/semiwrap/tool/scan_headers.py | 165 ++++++++++++++++---------- src/semiwrap/tool/show_override.py | 33 ------ src/semiwrap/tool/util.py | 7 -- 11 files changed, 267 insertions(+), 369 deletions(-) delete mode 100644 src/semiwrap/tool/create_gen.py create mode 100644 src/semiwrap/tool/create_yaml.py delete mode 100644 src/semiwrap/tool/parse_maven.py delete mode 100644 src/semiwrap/tool/platform_info.py delete mode 100644 src/semiwrap/tool/show_override.py delete mode 100644 src/semiwrap/tool/util.py diff --git a/src/semiwrap/tool/__init__.py b/src/semiwrap/tool/__init__.py index e69de29b..afdccbee 100644 --- a/src/semiwrap/tool/__init__.py +++ b/src/semiwrap/tool/__init__.py @@ -0,0 +1 @@ +from .__main__ import main diff --git a/src/semiwrap/tool/__main__.py b/src/semiwrap/tool/__main__.py index 8085b5e7..53e72f9d 100644 --- a/src/semiwrap/tool/__main__.py +++ b/src/semiwrap/tool/__main__.py @@ -2,16 +2,13 @@ import sys from .build_dep import BuildDep -from .create_gen import GenCreator -from .create_imports import ImportCreator -from .parse_maven import MavenParser -from .platform_info import PlatformInfo -from .show_override import ShowOverrides +from .create_yaml import GenCreator +from .create_imports import ImportCreator, UpdateInit from .scan_headers import HeaderScanner def main(): - parser = argparse.ArgumentParser(prog="robotpy-build") + parser = argparse.ArgumentParser(prog="semiwrap") parent_parser = argparse.ArgumentParser(add_help=False) subparsers = parser.add_subparsers(dest="cmd") subparsers.required = True @@ -21,9 +18,7 @@ def main(): GenCreator, HeaderScanner, ImportCreator, - PlatformInfo, - ShowOverrides, - MavenParser, + UpdateInit, ): cls.add_subparser(parent_parser, subparsers).set_defaults(cls=cls) @@ -40,8 +35,8 @@ def main(): else: retval = 0 - return retval + sys.exit(retval) if __name__ == "__main__": - sys.exit(main()) + main() diff --git a/src/semiwrap/tool/build_dep.py b/src/semiwrap/tool/build_dep.py index dc5fd561..6c16ba57 100644 --- a/src/semiwrap/tool/build_dep.py +++ b/src/semiwrap/tool/build_dep.py @@ -1,7 +1,6 @@ import subprocess import sys - -from .util import get_setup +import tomli class BuildDep: @@ -21,10 +20,9 @@ def add_subparser(cls, parent_parser, subparsers): return parser def run(self, args): - s = get_setup() - requirements = s.pyproject.get("build-system", {}).get("requires", []) - requirements.extend(s.setup_kwargs.get("install_requires", "")) - requirements.append("wheel") + with open("pyproject.toml", "rb") as fp: + pyproject = tomli.load(fp) + requirements = pyproject.get("build-system", {}).get("requires", []) pipargs = [ sys.executable, diff --git a/src/semiwrap/tool/create_gen.py b/src/semiwrap/tool/create_gen.py deleted file mode 100644 index 42b8167a..00000000 --- a/src/semiwrap/tool/create_gen.py +++ /dev/null @@ -1,49 +0,0 @@ -from os.path import exists - -from .util import get_setup -from ..autowrap.generator_data import MissingReporter - - -class GenCreator: - @classmethod - def add_subparser(cls, parent_parser, subparsers): - parser = subparsers.add_parser( - "create-gen", - help="Create YAML files from parsed header files", - parents=[parent_parser], - ) - parser.add_argument( - "--write", help="Write to files if they don't exist", action="store_true" - ) - parser.add_argument("--strip-prefixes", action="append") - - return parser - - def run(self, args): - pfx = "" - if args.strip_prefixes: - pfx = "strip_prefixes:\n- " + "\n- ".join(args.strip_prefixes) + "\n\n" - - s = get_setup() - for wrapper in s.wrappers: - reporter = MissingReporter() - wrapper.on_build_gen("", reporter) - - nada = True - for name, report in reporter.as_yaml(): - report = f"---\n\n{pfx}{report}" - - nada = False - if args.write: - if not exists(name): - print("Writing", name) - with open(name, "w") as fp: - fp.write(report) - else: - print(name, "already exists!") - - print("===", name, "===") - print(report) - - if nada: - print("Nothing to do!") diff --git a/src/semiwrap/tool/create_imports.py b/src/semiwrap/tool/create_imports.py index dfebd0b1..47e158d0 100644 --- a/src/semiwrap/tool/create_imports.py +++ b/src/semiwrap/tool/create_imports.py @@ -1,16 +1,19 @@ import inspect import posixpath import subprocess +import sys import typing import types +from ..pyproject import PyProject + class ImportCreator: @classmethod def add_subparser(cls, parent_parser, subparsers): parser = subparsers.add_parser( "create-imports", - help="Generate suitable imports for a module", + help="Generate suitable imports for a module. Prefer using update-init", parents=[parent_parser], ) parser.add_argument("base", help="Ex: wpiutil") @@ -35,11 +38,17 @@ def create(self, base: str, compiled: typing.Optional[str], write: bool): try: import black except: - print("Error, The following module is required to run this tool: black") - exit(1) + print( + "Error, The following module is required to run this tool: black", + file=sys.stderr, + ) + return 1 if not compiled: compiled = f"{base}._{base.split('.')[-1]}" + print(f"CHK base={base} compiled={compiled} (auto)") + else: + print(f"CHK base={base} compiled={compiled}") # TODO: could probably generate this from parsed code, but seems hard ctx = {} @@ -51,7 +60,8 @@ def create(self, base: str, compiled: typing.Optional[str], write: bool): relimport = self._rel(base, compiled) stmt_compiled = "" if not compiled else f" {compiled}" - begin_stmt = f"# autogenerated by 'robotpy-build create-imports {base}" + begin_stmt = f"# autogenerated by 'semiwrap create-imports {base}" + old_begin_stmt = f"# autogenerated by 'robotpy-build create-imports {base}" stmt = inspect.cleandoc( f""" @@ -77,6 +87,9 @@ def create(self, base: str, compiled: typing.Optional[str], write: bool): # Find the beginning statement idx = startidx = fcontent.find(begin_stmt) + if startidx == -1: + idx = startidx = fcontent.find(old_begin_stmt) + if startidx != -1: for to_find in ("from", "__all__", "[", "]", "\n"): idx = fcontent.find(to_find, idx) @@ -99,3 +112,32 @@ def create(self, base: str, compiled: typing.Optional[str], write: bool): else: print(content) + + +class UpdateInit: + @classmethod + def add_subparser(cls, parent_parser, subparsers): + parser = subparsers.add_parser( + "update-init", + help="Updates __init__.py files using settings from tool.semiwrap.update_init", + parents=[parent_parser], + ) + return parser + + def run(self, args): + project = PyProject().project + + if project.update_init is None: + print("[tool.semiwrap].update_init not set", file=sys.stderr) + return 1 + + ic = ImportCreator() + + for to_update in project.update_init: + if " " in to_update: + base, compiled = to_update.split(" ", 1) + else: + base = to_update + compiled = None + + ic.create(base, compiled, True) diff --git a/src/semiwrap/tool/create_yaml.py b/src/semiwrap/tool/create_yaml.py new file mode 100644 index 00000000..1687949b --- /dev/null +++ b/src/semiwrap/tool/create_yaml.py @@ -0,0 +1,103 @@ +import os +import pathlib +import typing as T + +from ..autowrap.generator_data import MissingReporter +from ..cmd.header2dat import make_argparser, generate_wrapper +from ..makeplan import InputFile, makeplan, BuildTarget + + +class GenCreator: + @classmethod + def add_subparser(cls, parent_parser, subparsers): + parser = subparsers.add_parser( + "create-yaml", + help="Create YAML files from parsed header files", + parents=[parent_parser], + ) + parser.add_argument( + "--write", help="Write to files if they don't exist", action="store_true" + ) + + return parser + + def run(self, args): + project_root = pathlib.Path.cwd() + + # Problem: if another hatchling plugin sets PKG_CONFIG_PATH to include a .pc + # file, makeplan() will fail to find it, which prevents a semiwrap program + # from consuming those .pc files. + # + # We search for .pc files in the project root by default and add anything found + # to the PKG_CONFIG_PATH to allow that to work. Probably won't hurt anything? + + pcpaths: T.Set[str] = set() + for pcfile in project_root.glob("**/*.pc"): + pcpaths.add(str(pcfile.parent)) + + if pcpaths: + # Add to PKG_CONFIG_PATH so that it can be resolved by other hatchling + # plugins if desired + pkg_config_path = os.environ.get("PKG_CONFIG_PATH") + if pkg_config_path is not None: + os.environ["PKG_CONFIG_PATH"] = os.pathsep.join( + (pkg_config_path, *pcpaths) + ) + else: + os.environ["PKG_CONFIG_PATH"] = os.pathsep.join(pcpaths) + + plan = makeplan(project_root, missing_yaml_ok=True) + + for item in plan: + if not isinstance(item, BuildTarget) or item.command != "header2dat": + continue + + # convert args to string so we can parse it + # .. this is weird, but less annoying than other alternatives + # that I can think of? + argv = [] + for arg in item.args: + if isinstance(arg, str): + argv.append(arg) + elif isinstance(arg, InputFile): + argv.append(str(arg.path.absolute())) + elif isinstance(arg, pathlib.Path): + argv.append(str(arg.absolute())) + else: + # anything else shouldn't matter + argv.append("ignored") + + sparser = make_argparser() + sargs = sparser.parse_args(argv) + + reporter = MissingReporter() + + generate_wrapper( + name=sargs.name, + src_yml=sargs.src_yml, + src_h=sargs.src_h, + src_h_root=sargs.src_h_root, + dst_dat=None, + dst_depfile=None, + include_paths=sargs.include_paths, + casters={}, + pp_defines=sargs.pp_defines, + missing_reporter=reporter, + report_only=True, + ) + + if reporter: + for name, report in reporter.as_yaml(): + report = f"---\n\n{report}" + + if args.write: + if not name.exists(): + name.parent.mkdir(parents=True, exist_ok=True) + print("Writing", name) + with open(name, "w") as fp: + fp.write(report) + else: + print(name, "already exists!") + + print("===", name, "===") + print(report) diff --git a/src/semiwrap/tool/parse_maven.py b/src/semiwrap/tool/parse_maven.py deleted file mode 100644 index 589e14d4..00000000 --- a/src/semiwrap/tool/parse_maven.py +++ /dev/null @@ -1,172 +0,0 @@ -from collections import defaultdict -from contextlib import suppress -import re -from urllib.request import Request, urlopen -from urllib.error import HTTPError - -import tomli - -from .. import platforms - - -class MavenParser: - after_archs = [ - "static", - "debug", - "staticdebug", - ] - - @classmethod - def add_subparser(cls, parent_parser, subparsers): - parser = subparsers.add_parser( - "parse-maven", - help="Find supported platforms from a pyproject.toml", - parents=[parent_parser], - ) - parser.add_argument( - "toml_link", - help="Ex: pyproject.toml", - ) - parser.add_argument( - "-b", - "--brute_force", - action="store_true", - help="Try known os and arch combinations instead of parsing html (needed for rev)", - ) - return parser - - def check_url_exists(self, file_url): - with suppress(Exception): - if urlopen(Request(file_url)).code == 200: - return True - return False - - def run(self, args): - self.os_names = set() - self.arch_names = set() - for plat in platforms._platforms.values(): - self.os_names.add(plat.os) - self.arch_names.add(plat.arch) - - with open(args.toml_link, "rb") as fp: - config = tomli.load(fp)["tool"]["robotpy-build"] - - wrappers = {} - if "wrappers" in config: - wrappers = { - k: v - for (k, v) in config["wrappers"].items() - if "maven_lib_download" in v - } - - static_libs = {} - if "static_libs" in config: - static_libs = { - k: v - for (k, v) in config["static_libs"].items() - if "maven_lib_download" in v - } - - if wrappers == {} and static_libs == {}: - print("No maven_lib_downloads in pyproject.toml") - exit() - - for w_name, wrapper in {**wrappers, **static_libs}.items(): - if "maven_lib_download" not in wrapper: - continue - - mvl = wrapper["maven_lib_download"] - - repo_url = mvl["repo_url"] - grp = mvl["group_id"].replace(".", "/") - art = mvl["artifact_id"] - ver = mvl["version"] - - dir_url = f"{repo_url}/{grp}/{art}/{ver}/" - - plats = defaultdict(set) - - found_source = False - source_name = None - - if args.brute_force: - for os in self.os_names: - for arch in self.arch_names: - for after_arch in self.after_archs + [""]: - classifier = os + arch + after_arch - file_url = f"{dir_url}{art}-{ver}-{classifier}.zip" - - if self.check_url_exists(file_url): - plats[os].add(arch) - - if self.check_url_exists(f"{dir_url}{art}-{ver}-source.zip"): - found_source = True - source_name = "source" - - if self.check_url_exists(f"{dir_url}{art}-{ver}-sources.zip"): - found_source = True - source_name = "sources" - - else: - try: - html = str(urlopen(Request(dir_url)).read()) - except HTTPError as e: - if e.code != 404: - raise e - else: - print( - "The repo url returned a 404 error. Try using the brute_force flag." - ) - exit() - - found_source = False - source_name = None - - if "source.zip" in html: - found_source = True - source_name = "source" - - if "sources.zip" in html: - found_source = True - source_name = "sources" - - # matches = re.findall('\.zip">(.*?)\.zip', html, re.I) # match on text - matches = re.findall('href="(.*?)">', html, re.I) # match on links - matches = [ - m[:-4] for m in matches if m[-4:] == ".zip" - ] # match on zip files and remove extension - - for m in matches: - for os in self.os_names: - idx = m.find(os) - - if idx != -1: - arch = m[idx + len(os) :] - - for after_arch in self.after_archs: - arch = arch.replace(after_arch, "") - - plats[os].add(arch) - break - - # Formatting - print() - print(f"Wrapper / static_lib :: {w_name}") - print(f"Artifact ID :: {art}") - print(f"URL :: {dir_url}") - print() - - if found_source: - print("This repo appears to provide sources.") - print("The name of the source file is:", source_name) - print() - - plat_str = "supported_platforms = [\n" - for os in plats: - for arch in plats[os]: - plat_str += ' {{ os = "{}", arch = "{}" }},\n'.format( - os, arch - ) - plat_str += "]" - - print(plat_str) diff --git a/src/semiwrap/tool/platform_info.py b/src/semiwrap/tool/platform_info.py deleted file mode 100644 index 87c9c117..00000000 --- a/src/semiwrap/tool/platform_info.py +++ /dev/null @@ -1,29 +0,0 @@ -import pprint - -from .. import platforms - - -class PlatformInfo: - @classmethod - def add_subparser(cls, parent_parser, subparsers): - parser = subparsers.add_parser( - "platform-info", - help="Displays platform-specific information", - parents=[parent_parser], - ) - parser.add_argument("--list", action="store_true", default=False) - parser.add_argument("platform", nargs="?", default=None) - return parser - - def run(self, args): - if args.list: - for name in platforms.get_platform_names(): - print(name) - else: - p = platforms.get_platform(args.platform) - print("platform:") - pprint.pprint(p) - print() - - print("override keys:") - pprint.pprint(platforms.get_platform_override_keys(p)) diff --git a/src/semiwrap/tool/scan_headers.py b/src/semiwrap/tool/scan_headers.py index 21f1da3b..71167ba7 100644 --- a/src/semiwrap/tool/scan_headers.py +++ b/src/semiwrap/tool/scan_headers.py @@ -2,10 +2,11 @@ import glob from itertools import chain from os.path import join, relpath -from pathlib import Path +from pathlib import Path, PurePosixPath +import typing as T - -from .util import get_setup +from ..pkgconf_cache import PkgconfCache +from ..pyproject import PyProject class HeaderScanner: @@ -17,12 +18,50 @@ def add_subparser(cls, parent_parser, subparsers): parents=[parent_parser], ) parser.add_argument("--all", default=False, action="store_true") + parser.add_argument( + "--as-ignore", + default=False, + action="store_true", + help="Emit scan_headers_ignore instead", + ) return parser + def _make_search_paths(self, pyproject: PyProject) -> T.Dict[str, T.List[Path]]: + # This logic is copied from makepath, probably should put it somewhere + # reusable instead? + + # extension: paths + cache = PkgconfCache() + search_paths: T.Dict[str, T.List[Path]] = {} + + for name, cfg in pyproject.project.extension_modules.items(): + paths: T.Dict[Path, bool] = {} + + for dep in cfg.wraps: + entry = cache.get(dep) + for incpath in entry.include_path: + paths[incpath] = True + + for inc in cfg.includes: + paths[pyproject.root / PurePosixPath(inc)] = True + + path_elems = name.split(".") + package_path = Path(*path_elems[:-1]) + + paths[pyproject.package_root / package_path] = True + + search_paths[name] = list(paths.keys()) + + return search_paths + def run(self, args): - s = get_setup() + pyproject = PyProject() + project = pyproject.project + + # Get the search path for each extension module + search_paths = self._make_search_paths(pyproject) - to_ignore = s.project.scan_headers_ignore + to_ignore = ["*/trampolines/*", "trampolines/*"] + project.scan_headers_ignore def _should_ignore(f): for pat in to_ignore: @@ -30,64 +69,74 @@ def _should_ignore(f): return True return False - already_present = {} + all_present = set() + if not args.all: - for i, wrapper in enumerate(s.project.wrappers.values()): - files = set() - if wrapper.autogen_headers: - files |= {Path(f) for f in wrapper.autogen_headers.values()} - if wrapper.type_casters: - files |= {Path(tc.header) for tc in wrapper.type_casters} - if not files: + for ccfg in project.export_type_casters.values(): + incs = [pyproject.root / inc for inc in ccfg.includedir] + for h in ccfg.headers: + for inc in incs: + if (inc / h.header).exists(): + all_present.add(inc / h.header) + + for name, ext in project.extension_modules.items(): + if not ext.headers: continue - for incdir in s.wrappers[i]._generation_search_path(): - ifiles = already_present.setdefault(incdir, set()) + files = [Path(f) for f in ext.headers.values()] + for incdir in search_paths[name]: incdir = Path(incdir) for f in files: if (incdir / f).exists(): - ifiles.add(f) - - for wrapper in s.wrappers: - printed = False - - # This uses the direct include directories instead of the generation - # search path as we only want to output a file once - for incdir in wrapper.get_include_dirs(): - wpresent = already_present.get(incdir, set()) - - files = list( - sorted( - Path(relpath(f, incdir)) - for f in chain( - glob.glob(join(incdir, "**", "*.h"), recursive=True), - glob.glob(join(incdir, "**", "*.hpp"), recursive=True), - ) - if "rpygen" not in f and not _should_ignore(relpath(f, incdir)) - ) - ) - - files = [f for f in files if f not in wpresent] - if not files: + all_present.add(incdir / f) + + all_search_paths = set() + for ps in search_paths.values(): + for p in ps: + all_search_paths.add(p) + + for incdir in sorted(all_search_paths, key=lambda pth: -len(pth.parts)): + files: T.List[Path] = [] + + for f in chain( + glob.glob(join(incdir, "**", "*.h"), recursive=True), + glob.glob(join(incdir, "**", "*.hpp"), recursive=True), + ): + rf = relpath(f, incdir) + + if _should_ignore(rf): + all_present.add(incdir / rf) + continue + + if (incdir / rf) in all_present: continue - if not printed: - print( - f'[tool.robotpy-build.wrappers."{wrapper.package_name}".autogen_headers]' - ) - printed = True - - lastdir = None - for f in files: - thisdir = f.parent - if lastdir is None: - if thisdir: - print("#", thisdir) - elif lastdir != thisdir: - print() - if thisdir: - print("#", thisdir) - lastdir = thisdir - - base = f.stem + files.append(Path(rf)) + + if not files: + continue + + files.sort() + + if args.as_ignore: + comment = " #" + else: + comment = "#" + + lastdir = None + for f in files: + thisdir = f.parent + if lastdir is None: + if thisdir: + print(comment, thisdir) + elif lastdir != thisdir: + print() + if thisdir: + print(comment, thisdir) + lastdir = thisdir + + base = f.stem + if args.as_ignore: + print(f' "{f.as_posix()}",') + else: print(f'{base} = "{f.as_posix()}"') - print() + print() diff --git a/src/semiwrap/tool/show_override.py b/src/semiwrap/tool/show_override.py deleted file mode 100644 index e1447cb3..00000000 --- a/src/semiwrap/tool/show_override.py +++ /dev/null @@ -1,33 +0,0 @@ -import tomli -import tomli_w - -from .. import overrides -from .. import platforms - - -class ShowOverrides: - @classmethod - def add_subparser(cls, parent_parser, subparsers): - parser = subparsers.add_parser( - "show-override", - help="Displays pyproject.toml after override processing", - parents=[parent_parser], - ) - parser.add_argument("toml", nargs="?", default="pyproject.toml") - parser.add_argument( - "-p", - "--platform", - default=None, - help="Use robotpy-build platform-info --list for available platforms", - ) - return parser - - def run(self, args): - p = platforms.get_platform(args.platform) - override_keys = platforms.get_platform_override_keys(p) - - with open(args.toml, "rb") as fp: - d = tomli.load(fp) - - overrides.apply_overrides(d, override_keys) - print(tomli_w.dumps(d)) diff --git a/src/semiwrap/tool/util.py b/src/semiwrap/tool/util.py deleted file mode 100644 index 5f0204f5..00000000 --- a/src/semiwrap/tool/util.py +++ /dev/null @@ -1,7 +0,0 @@ -from ..setup import Setup - - -def get_setup() -> Setup: - s = Setup() - s.prepare() - return s From 0e027d2a942515c4a75532a9d62d6c806f761347 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 6 May 2025 01:35:36 -0400 Subject: [PATCH 07/11] Migrate example to semiwrap --- examples/demo/.gitignore | 4 +- examples/demo/gen/.gitkeep | 0 examples/demo/meson.build | 23 +++++ examples/demo/pyproject.toml | 83 ++++++++++--------- examples/demo/rpydemo/src/main.cpp | 3 - examples/demo/semiwrap/.gitignore | 3 + examples/demo/{gen => semiwrap}/demo.yml | 2 +- examples/demo/setup.py | 4 - examples/demo/{rpydemo => swdemo}/__init__.py | 2 +- .../demo/{rpydemo => swdemo}/include/demo.h | 4 + .../demo/{rpydemo => swdemo}/src/demo.cpp | 6 +- examples/demo/swdemo/src/main.cpp | 11 +++ 12 files changed, 94 insertions(+), 51 deletions(-) delete mode 100644 examples/demo/gen/.gitkeep create mode 100644 examples/demo/meson.build delete mode 100644 examples/demo/rpydemo/src/main.cpp create mode 100644 examples/demo/semiwrap/.gitignore rename examples/demo/{gen => semiwrap}/demo.yml (78%) delete mode 100755 examples/demo/setup.py rename examples/demo/{rpydemo => swdemo}/__init__.py (73%) rename examples/demo/{rpydemo => swdemo}/include/demo.h (89%) rename examples/demo/{rpydemo => swdemo}/src/demo.cpp (78%) create mode 100644 examples/demo/swdemo/src/main.cpp diff --git a/examples/demo/.gitignore b/examples/demo/.gitignore index 61da11e0..c49eb750 100644 --- a/examples/demo/.gitignore +++ b/examples/demo/.gitignore @@ -9,6 +9,6 @@ py.typed /dist _init*.py -pkgcfg.py +*.pc version.py -rpy-include \ No newline at end of file +trampolines \ No newline at end of file diff --git a/examples/demo/gen/.gitkeep b/examples/demo/gen/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/demo/meson.build b/examples/demo/meson.build new file mode 100644 index 00000000..65542832 --- /dev/null +++ b/examples/demo/meson.build @@ -0,0 +1,23 @@ +project('demo', ['cpp'], + default_options: ['warning_level=1', 'cpp_std=c++20', + 'b_colorout=auto', 'optimization=2', 'b_pie=true']) + +# Include autogenerated semiwrap/meson.build +subdir('semiwrap') + +# Add additional source files to predefined variable in semiwrap/meson.build +demo_sources += files( + 'swdemo/src/demo.cpp', + 'swdemo/src/main.cpp', +) + +# You can add extra compilation arguments by adding a dependency to predefined +# variable +demo_deps += [ + declare_dependency(include_directories: ['swdemo/include']) +] + +# Include autogenerated semiwrap/modules/meson.build +# - Builds the extension modules +# - Generates the pyi file for the extension modules +subdir('semiwrap/modules') diff --git a/examples/demo/pyproject.toml b/examples/demo/pyproject.toml index 6bdf7fc3..d15b46ef 100644 --- a/examples/demo/pyproject.toml +++ b/examples/demo/pyproject.toml @@ -1,41 +1,46 @@ -# This section tells pip to install robotpy-build before starting a build [build-system] -requires = ["robotpy-build"] - -# Tells robotpy-build where to place autogenerated metadata -[tool.robotpy-build] -base_package = "rpydemo" - -# This section configures the 'rpydemo' python package. Multiple -# sections are possible to build multiple packages -[tool.robotpy-build.wrappers."rpydemo"] -name = "rpydemo" - -# C++ source files to compile, path is relative to the root of the project -sources = [ - "rpydemo/src/demo.cpp", - "rpydemo/src/main.cpp" -] - -# This is a directory that can be used to customize the autogenerated -# C++ code -# -> autogenerate those files via `robotpy-build create-gen` -generation_data = "gen" - -# This tells robotpy-build to parse include/demo.h and autogenerate pybind11 -# wrappers for the contents of the header. -# -> autogenerate this via `robotpy-build scan-headers` -[tool.robotpy-build.wrappers."rpydemo".autogen_headers] -demo = "demo.h" - - -# Standard python package metadata -[tool.robotpy-build.metadata] -name = "robotpy-build-demo" -description = "robotpy-build demo program" -author = "RobotPy Development Team" -author_email = "robotpy@googlegroups.com" -url = "https://github.com/robotpy/robotpy-build" -license = "BSD-3-Clause" -install_requires = [] \ No newline at end of file +# semiwrap is a hatchling plugin and is launched by the hatchling build backend +build-backend = "hatchling.build" +# compilation is handled by meson +requires = ["semiwrap", "hatch-meson", "hatchling"] + +# Standard python packaging metadata +[project] +name = "swdemo" +description = "Demo program" +version = "0.0.1" + +# +# Configure hatchling build hooks here +# - semiwrap is just a hatchling build hook and can be used along with +# other hatchling build hooks +# + +# semiwrap build hook will autogenerate meson.build files in semiwrap directory +[tool.hatch.build.hooks.semiwrap] + +# meson build hook will build python extension modules as generated by semiwrap +[tool.hatch.build.hooks.meson] + + +# +# semiwrap code generation configuration +# + +[tool.semiwrap] +# This tells the `semiwrap update-init` command which files to update +# .. the first item is the python package to be updated, and the second +# item is the name of the compiled python extension module +update_init = ["swdemo swdemo._demo"] + +# This tells semiwrap to generate a python extension module `swdemo._demo` +[tool.semiwrap.extension_modules."swdemo._demo"] +name = "demo" + +# semiwrap will parse any header files mentioned here and autogenerate pybind11 +# wrappers around the contents of the header +# - You can use `python -m semiwrap scan-headers` to autogenerate a list of +# headers that are available to be wrapped, and paste the list here +[tool.semiwrap.extension_modules."swdemo._demo".headers] +demo = "include/demo.h" diff --git a/examples/demo/rpydemo/src/main.cpp b/examples/demo/rpydemo/src/main.cpp deleted file mode 100644 index c7e6095e..00000000 --- a/examples/demo/rpydemo/src/main.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include - -RPYBUILD_PYBIND11_MODULE(m) { initWrapper(m); } \ No newline at end of file diff --git a/examples/demo/semiwrap/.gitignore b/examples/demo/semiwrap/.gitignore new file mode 100644 index 00000000..6cdce102 --- /dev/null +++ b/examples/demo/semiwrap/.gitignore @@ -0,0 +1,3 @@ +/meson.build +/modules/meson.build +/trampolines/meson.build diff --git a/examples/demo/gen/demo.yml b/examples/demo/semiwrap/demo.yml similarity index 78% rename from examples/demo/gen/demo.yml rename to examples/demo/semiwrap/demo.yml index a1b6811f..1a632401 100644 --- a/examples/demo/gen/demo.yml +++ b/examples/demo/semiwrap/demo.yml @@ -3,7 +3,7 @@ functions: add2: classes: - DemoClass: + demo::DemoClass: methods: setX: getX: diff --git a/examples/demo/setup.py b/examples/demo/setup.py deleted file mode 100755 index ea1ae2b4..00000000 --- a/examples/demo/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python3 -from robotpy_build.setup import setup - -setup() diff --git a/examples/demo/rpydemo/__init__.py b/examples/demo/swdemo/__init__.py similarity index 73% rename from examples/demo/rpydemo/__init__.py rename to examples/demo/swdemo/__init__.py index 0641bb9a..671b581f 100644 --- a/examples/demo/rpydemo/__init__.py +++ b/examples/demo/swdemo/__init__.py @@ -1,4 +1,4 @@ # autogenerated by 'robotpy-build create-imports rpydemo rpydemo._rpydemo' -from ._rpydemo import DemoClass, add2 +from ._demo import DemoClass, add2 __all__ = ["DemoClass", "add2"] diff --git a/examples/demo/rpydemo/include/demo.h b/examples/demo/swdemo/include/demo.h similarity index 89% rename from examples/demo/rpydemo/include/demo.h rename to examples/demo/swdemo/include/demo.h index 0f351c50..29968add 100644 --- a/examples/demo/rpydemo/include/demo.h +++ b/examples/demo/swdemo/include/demo.h @@ -4,6 +4,8 @@ /** Adds 2 to the first parameter and returns it */ int add2(int x); +namespace demo { + /** Doxygen documentation is automatically added to your python objects when the bindings are autogenerated. @@ -20,3 +22,5 @@ class DemoClass { private: int m_x = 0; }; + +} // namespace demo diff --git a/examples/demo/rpydemo/src/demo.cpp b/examples/demo/swdemo/src/demo.cpp similarity index 78% rename from examples/demo/rpydemo/src/demo.cpp rename to examples/demo/swdemo/src/demo.cpp index 6b4279a5..fd670f31 100644 --- a/examples/demo/rpydemo/src/demo.cpp +++ b/examples/demo/swdemo/src/demo.cpp @@ -5,10 +5,14 @@ int add2(int x) { return x + 2; } +namespace demo { + void DemoClass::setX(int x) { m_x = x; } int DemoClass::getX() const { return m_x; -} \ No newline at end of file +} + +} // namespace demo diff --git a/examples/demo/swdemo/src/main.cpp b/examples/demo/swdemo/src/main.cpp new file mode 100644 index 00000000..0eb4f06e --- /dev/null +++ b/examples/demo/swdemo/src/main.cpp @@ -0,0 +1,11 @@ + +// This header file is automatically generated by semiwrap and provides the +// `initWrapper` function which is used to instantiate all the wrapper code +// autogenerated by semiwrap +// +// The name of the header will be `semiwrap_init.PACKAGE.NAME.hpp` +#include + +SEMIWRAP_PYBIND11_MODULE(m) { + initWrapper(m); +} \ No newline at end of file From 43086bc383a24a5e65632d58fa32f15383c9ccf4 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 6 May 2025 01:36:01 -0400 Subject: [PATCH 08/11] Migrate tests to semiwrap and add enhancements - Builds multiple projects that use things from each other --- tests/cpp/.gitignore | 29 ++----- tests/cpp/dl/downloaded.cpp | 6 -- tests/cpp/dl/downloaded.h | 4 - tests/cpp/gen/dl/downloaded.yml | 4 - tests/cpp/rpytest/dl/__init__.py | 6 -- tests/cpp/rpytest/dl/dl.cpp | 3 - tests/cpp/rpytest/dl/dl.patch | 10 --- tests/cpp/rpytest/ft/src/ft.cpp | 7 -- tests/cpp/rpytest/ft/subpkg.py | 4 - tests/cpp/rpytest/srconly/__init__.py | 4 - tests/cpp/rpytest/srconly/srconly.cpp | 10 --- tests/cpp/run_install.py | 87 +++++-------------- tests/cpp/setup.py | 5 -- tests/cpp/sw-caster-consumer/meson.build | 11 +++ tests/cpp/sw-caster-consumer/pyproject.toml | 29 +++++++ .../sw-caster-consumer/semiwrap/.gitignore | 1 + .../semiwrap/modules/.gitignore | 1 + .../cpp/sw-caster-consumer/semiwrap/more.yml | 4 + .../sw-caster-consumer/semiwrap/ns_more.yml | 4 + .../semiwrap/trampolines/.gitignore | 1 + .../src/sw_caster_consumer/__init__.py | 1 + .../src/sw_caster_consumer/cpp/main.cpp | 5 ++ .../src/sw_caster_consumer/cpp/more.h | 8 ++ .../src/sw_caster_consumer/cpp/ns_more.h | 12 +++ tests/cpp/sw-test-base/.gitignore | 3 + tests/cpp/sw-test-base/meson.build | 19 ++++ tests/cpp/sw-test-base/pyproject.toml | 52 +++++++++++ tests/cpp/sw-test-base/semiwrap/.gitignore | 1 + .../cpp/sw-test-base/semiwrap/base_class.yml | 6 ++ tests/cpp/sw-test-base/semiwrap/fn.yml | 4 + tests/cpp/sw-test-base/semiwrap/fn2.yml | 4 + .../sw-test-base/semiwrap/modules/.gitignore | 1 + .../semiwrap/trampolines/.gitignore | 1 + .../sw-test-base/src/swtest_base/__init__.py | 4 + .../src/swtest_base/cpp/baseclass.h | 14 +++ .../cpp/sw-test-base/src/swtest_base/cpp/fn.h | 8 ++ .../sw-test-base/src/swtest_base/cpp/fn2.h | 5 ++ .../sw-test-base/src/swtest_base/cpp/inty.h | 3 + .../sw-test-base/src/swtest_base/cpp/main.cpp | 5 ++ .../src/swtest_base/cpp/main2.cpp | 5 ++ .../src/swtest_base/cpp/main3.cpp | 5 ++ .../src/swtest_base/cpp/ns_inty.h | 9 ++ .../src/swtest_base/cpp/remote_class.h | 4 + .../swtest_base/cpp/type_casters/inty_cast.h | 51 +++++++++++ .../cpp/type_casters/ns_inty_cast.h | 51 +++++++++++ tests/cpp/sw-test/meson.build | 13 +++ .../pyproject.toml} | 80 ++++++----------- tests/cpp/sw-test/semiwrap/.gitignore | 1 + .../{gen => sw-test/semiwrap}/ft/IBase.yml | 0 .../{gen => sw-test/semiwrap}/ft/IChild.yml | 0 .../{gen => sw-test/semiwrap}/ft/IGChild.yml | 0 .../{gen => sw-test/semiwrap}/ft/IMChild.yml | 0 .../semiwrap}/ft/Overloaded.yml | 0 .../{gen => sw-test/semiwrap}/ft/PBase.yml | 0 .../{gen => sw-test/semiwrap}/ft/PChild.yml | 0 .../{gen => sw-test/semiwrap}/ft/PGChild.yml | 0 .../{gen => sw-test/semiwrap}/ft/abstract.yml | 0 .../semiwrap}/ft/base_qualname.yml | 0 .../semiwrap}/ft/base_qualname_hidden.yml | 0 .../{gen => sw-test/semiwrap}/ft/buffers.yml | 0 .../semiwrap}/ft/custom_type_caster.yml | 0 .../{gen => sw-test/semiwrap}/ft/defaults.yml | 0 .../semiwrap}/ft/docstrings.yml | 0 .../semiwrap}/ft/docstrings_append.yml | 0 .../{gen => sw-test/semiwrap}/ft/enums.yml | 0 .../{gen => sw-test/semiwrap}/ft/factory.yml | 0 .../{gen => sw-test/semiwrap}/ft/fields.yml | 0 .../semiwrap}/ft/gilsafe_container.yml | 0 .../{gen => sw-test/semiwrap}/ft/ignore.yml | 0 .../semiwrap}/ft/ignored_by_default.yml | 0 .../semiwrap}/ft/inline_code.yml | 0 .../semiwrap}/ft/keepalive.yml | 0 .../{gen => sw-test/semiwrap}/ft/lifetime.yml | 0 .../cpp/{gen => sw-test/semiwrap}/ft/mvi.yml | 0 .../{gen => sw-test/semiwrap}/ft/nested.yml | 0 .../{gen => sw-test/semiwrap}/ft/ns_class.yml | 0 .../semiwrap}/ft/ns_hidden.yml | 0 .../semiwrap}/ft/operators.yml | 0 .../semiwrap}/ft/overloads.yml | 0 .../semiwrap}/ft/parameters.yml | 0 .../{gen => sw-test/semiwrap}/ft/refqual.yml | 0 .../cpp/sw-test/semiwrap/ft/remote_class.yml | 4 + .../sw-test/semiwrap/ft/remote_trampoline.yml | 6 ++ .../{gen => sw-test/semiwrap}/ft/rename.yml | 0 .../{gen => sw-test/semiwrap}/ft/retval.yml | 0 .../semiwrap}/ft/static_only.yml | 0 .../{gen => sw-test/semiwrap}/ft/subpkg.yml | 0 .../{gen => sw-test/semiwrap}/ft/tbase.yml | 0 .../{gen => sw-test/semiwrap}/ft/tbasic.yml | 0 .../semiwrap}/ft/tconcrete.yml | 0 .../{gen => sw-test/semiwrap}/ft/tcrtp.yml | 0 .../{gen => sw-test/semiwrap}/ft/tcrtpfwd.yml | 0 .../semiwrap}/ft/tdependent_base.yml | 0 .../semiwrap}/ft/tdependent_param.yml | 0 .../semiwrap}/ft/tdependent_using.yml | 0 .../semiwrap}/ft/tdependent_using2.yml | 0 .../cpp/{gen => sw-test/semiwrap}/ft/tfn.yml | 0 .../{gen => sw-test/semiwrap}/ft/tnested.yml | 0 .../{gen => sw-test/semiwrap}/ft/tnumeric.yml | 0 .../semiwrap}/ft/trampoline.yml | 0 .../{gen => sw-test/semiwrap}/ft/tvbase.yml | 0 .../{gen => sw-test/semiwrap}/ft/tvchild.yml | 0 .../semiwrap}/ft/type_caster.yml | 0 .../semiwrap}/ft/type_caster_nested.yml | 0 .../{gen => sw-test/semiwrap}/ft/using.yml | 0 .../{gen => sw-test/semiwrap}/ft/using2.yml | 0 .../semiwrap}/ft/usingparent.yml | 0 .../semiwrap}/ft/virtual_comma.yml | 0 .../semiwrap}/ft/virtual_xform.yml | 0 tests/cpp/sw-test/semiwrap/modules/.gitignore | 1 + .../sw-test/semiwrap/trampolines/.gitignore | 1 + .../src/swtest}/__init__.py | 0 .../src/swtest}/ft/__init__.py | 6 +- .../src/swtest}/ft/include/abstract.h | 0 .../src/swtest}/ft/include/base_qualname.h | 0 .../swtest}/ft/include/base_qualname_hidden.h | 0 .../src/swtest}/ft/include/buffers.h | 0 .../swtest}/ft/include/custom_type_caster.h | 0 .../src/swtest}/ft/include/defaults.h | 0 .../src/swtest}/ft/include/docstrings.h | 0 .../swtest}/ft/include/docstrings_append.h | 0 .../src/swtest}/ft/include/enums.h | 0 .../src/swtest}/ft/include/factory.h | 0 .../src/swtest}/ft/include/fields.h | 0 .../swtest}/ft/include/gilsafe_container.h | 4 +- .../src/swtest}/ft/include/ignore.h | 0 .../swtest}/ft/include/ignored_by_default.h | 0 .../swtest}/ft/include/inheritance/ibase.h | 0 .../swtest}/ft/include/inheritance/ichild.h | 0 .../swtest}/ft/include/inheritance/igchild.h | 0 .../swtest}/ft/include/inheritance/imchild.h | 0 .../src/swtest}/ft/include/inheritance/mvi.h | 0 .../ft/include/inheritance/overloaded.h | 0 .../ft/include/inheritance/usingparent.h | 0 .../src/swtest}/ft/include/inline_code.h | 0 .../src/swtest}/ft/include/keepalive.h | 2 +- .../src/swtest}/ft/include/lifetime.h | 0 .../src/swtest}/ft/include/nested.h | 0 .../src/swtest}/ft/include/ns_class.h | 0 .../src/swtest}/ft/include/ns_hidden.h | 0 .../src/swtest}/ft/include/operators.h | 0 .../src/swtest}/ft/include/overloads.h | 0 .../src/swtest}/ft/include/parameters.h | 0 .../src/swtest}/ft/include/protection/pbase.h | 0 .../swtest}/ft/include/protection/pchild.h | 0 .../swtest}/ft/include/protection/pgchild.h | 0 .../src/swtest}/ft/include/refqual.h | 0 .../src/swtest/ft/include/remote_trampoline.h | 11 +++ .../src/swtest}/ft/include/rename.h | 0 .../src/swtest}/ft/include/retval.h | 0 .../src/swtest}/ft/include/static_only.h | 0 .../src/swtest}/ft/include/subpkg.h | 0 .../src/swtest}/ft/include/templates/basic.h | 0 .../ft/include/templates/dependent_base.h | 0 .../ft/include/templates/dependent_param.h | 0 .../ft/include/templates/dependent_using.h | 0 .../ft/include/templates/dependent_using2.h | 0 .../src/swtest}/ft/include/templates/fn.h | 0 .../src/swtest}/ft/include/templates/nested.h | 0 .../swtest}/ft/include/templates/numeric.h | 0 .../src/swtest}/ft/include/templates/tbase.h | 0 .../swtest}/ft/include/templates/tconcrete.h | 0 .../src/swtest}/ft/include/templates/tcrtp.h | 0 .../swtest}/ft/include/templates/tcrtpfwd.h | 0 .../src/swtest}/ft/include/templates/tvbase.h | 0 .../swtest}/ft/include/templates/tvchild.h | 0 .../src/swtest}/ft/include/trampoline.h | 0 .../src/swtest}/ft/include/type_caster.h | 0 .../swtest}/ft/include/type_caster_nested.h | 0 .../src/swtest}/ft/include/using.h | 0 .../src/swtest}/ft/include/using2.h | 0 .../src/swtest}/ft/include/using_companion.h | 0 .../src/swtest}/ft/include/virtual_comma.h | 0 .../src/swtest}/ft/include/virtual_xform.h | 0 .../src/swtest}/ft/src/fields.cpp | 0 tests/cpp/sw-test/src/swtest/ft/src/ft.cpp | 7 ++ .../src/swtest}/ft/src/using2.cpp | 0 tests/cpp/sw-test/src/swtest/ft/subpkg.py | 4 + .../src/swtest}/tc/include/rpyint.h | 0 .../swtest}/tc/include/rpyint_type_caster.h | 2 +- tests/requirements.txt | 4 +- tests/test_caster_module.py | 10 +++ tests/test_downloaded.py | 10 --- tests/test_ft_defaults.py | 2 +- tests/test_ft_docs.py | 10 +-- tests/test_ft_enums.py | 6 +- tests/test_ft_fields.py | 2 +- tests/test_ft_ignore.py | 26 +++--- tests/test_ft_inheritance.py | 2 +- tests/test_ft_keepalive.py | 4 +- tests/test_ft_lifetime.py | 2 +- tests/test_ft_misc.py | 6 +- tests/test_ft_overloaded.py | 2 +- tests/test_ft_overloads.py | 2 +- tests/test_ft_parameters.py | 2 +- tests/test_ft_protection.py | 2 +- tests/test_ft_refquals.py | 2 +- tests/test_ft_rename.py | 8 +- tests/test_ft_templates.py | 2 +- tests/test_ft_trampoline.py | 13 ++- tests/test_ft_typecaster.py | 2 +- tests/test_multiple_virtual_inheritance.py | 2 +- tests/test_srconly.py | 6 -- 203 files changed, 508 insertions(+), 272 deletions(-) delete mode 100644 tests/cpp/dl/downloaded.cpp delete mode 100644 tests/cpp/dl/downloaded.h delete mode 100644 tests/cpp/gen/dl/downloaded.yml delete mode 100644 tests/cpp/rpytest/dl/__init__.py delete mode 100644 tests/cpp/rpytest/dl/dl.cpp delete mode 100644 tests/cpp/rpytest/dl/dl.patch delete mode 100644 tests/cpp/rpytest/ft/src/ft.cpp delete mode 100644 tests/cpp/rpytest/ft/subpkg.py delete mode 100644 tests/cpp/rpytest/srconly/__init__.py delete mode 100644 tests/cpp/rpytest/srconly/srconly.cpp delete mode 100755 tests/cpp/setup.py create mode 100644 tests/cpp/sw-caster-consumer/meson.build create mode 100644 tests/cpp/sw-caster-consumer/pyproject.toml create mode 100644 tests/cpp/sw-caster-consumer/semiwrap/.gitignore create mode 100644 tests/cpp/sw-caster-consumer/semiwrap/modules/.gitignore create mode 100644 tests/cpp/sw-caster-consumer/semiwrap/more.yml create mode 100644 tests/cpp/sw-caster-consumer/semiwrap/ns_more.yml create mode 100644 tests/cpp/sw-caster-consumer/semiwrap/trampolines/.gitignore create mode 100644 tests/cpp/sw-caster-consumer/src/sw_caster_consumer/__init__.py create mode 100644 tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/main.cpp create mode 100644 tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/more.h create mode 100644 tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/ns_more.h create mode 100644 tests/cpp/sw-test-base/.gitignore create mode 100644 tests/cpp/sw-test-base/meson.build create mode 100644 tests/cpp/sw-test-base/pyproject.toml create mode 100644 tests/cpp/sw-test-base/semiwrap/.gitignore create mode 100644 tests/cpp/sw-test-base/semiwrap/base_class.yml create mode 100644 tests/cpp/sw-test-base/semiwrap/fn.yml create mode 100644 tests/cpp/sw-test-base/semiwrap/fn2.yml create mode 100644 tests/cpp/sw-test-base/semiwrap/modules/.gitignore create mode 100644 tests/cpp/sw-test-base/semiwrap/trampolines/.gitignore create mode 100644 tests/cpp/sw-test-base/src/swtest_base/__init__.py create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/baseclass.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/fn.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/fn2.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/inty.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/main.cpp create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/main2.cpp create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/main3.cpp create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/ns_inty.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/remote_class.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/inty_cast.h create mode 100644 tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/ns_inty_cast.h create mode 100644 tests/cpp/sw-test/meson.build rename tests/cpp/{pyproject.toml.tmpl => sw-test/pyproject.toml} (57%) create mode 100644 tests/cpp/sw-test/semiwrap/.gitignore rename tests/cpp/{gen => sw-test/semiwrap}/ft/IBase.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/IChild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/IGChild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/IMChild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/Overloaded.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/PBase.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/PChild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/PGChild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/abstract.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/base_qualname.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/base_qualname_hidden.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/buffers.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/custom_type_caster.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/defaults.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/docstrings.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/docstrings_append.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/enums.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/factory.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/fields.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/gilsafe_container.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/ignore.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/ignored_by_default.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/inline_code.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/keepalive.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/lifetime.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/mvi.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/nested.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/ns_class.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/ns_hidden.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/operators.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/overloads.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/parameters.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/refqual.yml (100%) create mode 100644 tests/cpp/sw-test/semiwrap/ft/remote_class.yml create mode 100644 tests/cpp/sw-test/semiwrap/ft/remote_trampoline.yml rename tests/cpp/{gen => sw-test/semiwrap}/ft/rename.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/retval.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/static_only.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/subpkg.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tbase.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tbasic.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tconcrete.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tcrtp.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tcrtpfwd.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tdependent_base.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tdependent_param.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tdependent_using.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tdependent_using2.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tfn.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tnested.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tnumeric.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/trampoline.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tvbase.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/tvchild.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/type_caster.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/type_caster_nested.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/using.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/using2.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/usingparent.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/virtual_comma.yml (100%) rename tests/cpp/{gen => sw-test/semiwrap}/ft/virtual_xform.yml (100%) create mode 100644 tests/cpp/sw-test/semiwrap/modules/.gitignore create mode 100644 tests/cpp/sw-test/semiwrap/trampolines/.gitignore rename tests/cpp/{rpytest => sw-test/src/swtest}/__init__.py (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/__init__.py (96%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/abstract.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/base_qualname.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/base_qualname_hidden.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/buffers.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/custom_type_caster.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/defaults.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/docstrings.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/docstrings_append.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/enums.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/factory.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/fields.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/gilsafe_container.h (82%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/ignore.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/ignored_by_default.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/ibase.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/ichild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/igchild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/imchild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/mvi.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/overloaded.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inheritance/usingparent.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/inline_code.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/keepalive.h (76%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/lifetime.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/nested.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/ns_class.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/ns_hidden.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/operators.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/overloads.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/parameters.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/protection/pbase.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/protection/pchild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/protection/pgchild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/refqual.h (100%) create mode 100644 tests/cpp/sw-test/src/swtest/ft/include/remote_trampoline.h rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/rename.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/retval.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/static_only.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/subpkg.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/basic.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/dependent_base.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/dependent_param.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/dependent_using.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/dependent_using2.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/fn.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/nested.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/numeric.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tbase.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tconcrete.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tcrtp.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tcrtpfwd.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tvbase.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/templates/tvchild.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/trampoline.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/type_caster.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/type_caster_nested.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/using.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/using2.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/using_companion.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/virtual_comma.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/include/virtual_xform.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/src/fields.cpp (100%) create mode 100644 tests/cpp/sw-test/src/swtest/ft/src/ft.cpp rename tests/cpp/{rpytest => sw-test/src/swtest}/ft/src/using2.cpp (100%) create mode 100644 tests/cpp/sw-test/src/swtest/ft/subpkg.py rename tests/cpp/{rpytest => sw-test/src/swtest}/tc/include/rpyint.h (100%) rename tests/cpp/{rpytest => sw-test/src/swtest}/tc/include/rpyint_type_caster.h (97%) create mode 100644 tests/test_caster_module.py delete mode 100644 tests/test_downloaded.py delete mode 100644 tests/test_srconly.py diff --git a/tests/cpp/.gitignore b/tests/cpp/.gitignore index 9dc347fd..a65f7f1a 100644 --- a/tests/cpp/.gitignore +++ b/tests/cpp/.gitignore @@ -1,6 +1,8 @@ *.py[ciod] +*.pc +*.pybind11.json *.so *.dll *.pyd @@ -8,28 +10,9 @@ py.typed -/build -/dist -/pip-wheel-metadata +build +dist +trampolines -# autogenerated from template -/pyproject.toml +version.py -/rpytest/version.py - -/rpytest/dl/_init_rpytest_dl.py -/rpytest/dl/pkgcfg.py -/rpytest/dl/include -/rpytest/dl/rpy-include - -/rpytest/ft/_init_rpytest_ft.py -/rpytest/ft/pkgcfg.py -/rpytest/ft/rpy-include - -/rpytest/srconly/_init_rpytest_srconly.py -/rpytest/srconly/pkgcfg.py -/rpytest/srconly/include -/rpytest/srconly/rpy-include - -/rpytest/tc/_init_rpytest_tc.py -/rpytest/tc/pkgcfg.py \ No newline at end of file diff --git a/tests/cpp/dl/downloaded.cpp b/tests/cpp/dl/downloaded.cpp deleted file mode 100644 index 42af69a5..00000000 --- a/tests/cpp/dl/downloaded.cpp +++ /dev/null @@ -1,6 +0,0 @@ - -#include "downloaded.h" - -int downloaded_fn(int val) { - return 0x42 + val; -} diff --git a/tests/cpp/dl/downloaded.h b/tests/cpp/dl/downloaded.h deleted file mode 100644 index a7a7cdd1..00000000 --- a/tests/cpp/dl/downloaded.h +++ /dev/null @@ -1,4 +0,0 @@ - -#pragma once - -int downloaded_fn(int val); diff --git a/tests/cpp/gen/dl/downloaded.yml b/tests/cpp/gen/dl/downloaded.yml deleted file mode 100644 index b0ba34ae..00000000 --- a/tests/cpp/gen/dl/downloaded.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- - -functions: - downloaded_fn: diff --git a/tests/cpp/rpytest/dl/__init__.py b/tests/cpp/rpytest/dl/__init__.py deleted file mode 100644 index acced46d..00000000 --- a/tests/cpp/rpytest/dl/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from . import _init_rpytest_dl - -# autogenerated by 'robotpy-build create-imports rpytest.dl rpytest.dl._rpytest_dl' -from ._rpytest_dl import downloaded_fn, extra_content - -__all__ = ["downloaded_fn", "extra_content"] diff --git a/tests/cpp/rpytest/dl/dl.cpp b/tests/cpp/rpytest/dl/dl.cpp deleted file mode 100644 index c7e6095e..00000000 --- a/tests/cpp/rpytest/dl/dl.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#include - -RPYBUILD_PYBIND11_MODULE(m) { initWrapper(m); } \ No newline at end of file diff --git a/tests/cpp/rpytest/dl/dl.patch b/tests/cpp/rpytest/dl/dl.patch deleted file mode 100644 index ce7fabed..00000000 --- a/tests/cpp/rpytest/dl/dl.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- downloaded.h.old 2022-02-23 23:09:51.053000000 -0500 -+++ downloaded.h 2022-02-23 23:10:13.861277275 -0500 -@@ -1,4 +1,6 @@ - --#pragma once -+#pragma once -+ -+inline bool extra_content() { return true; } - - int downloaded_fn(int val); diff --git a/tests/cpp/rpytest/ft/src/ft.cpp b/tests/cpp/rpytest/ft/src/ft.cpp deleted file mode 100644 index c16827fb..00000000 --- a/tests/cpp/rpytest/ft/src/ft.cpp +++ /dev/null @@ -1,7 +0,0 @@ - -#include - -RPYBUILD_PYBIND11_MODULE(m) -{ - initWrapper(m); -} \ No newline at end of file diff --git a/tests/cpp/rpytest/ft/subpkg.py b/tests/cpp/rpytest/ft/subpkg.py deleted file mode 100644 index 09251e93..00000000 --- a/tests/cpp/rpytest/ft/subpkg.py +++ /dev/null @@ -1,4 +0,0 @@ -# autogenerated by 'robotpy-build create-imports rpytest.ft rpytest.ft._rpytest_ft.subpkg' -from ._rpytest_ft.subpkg import SPClass, sp_func - -__all__ = ["SPClass", "sp_func"] diff --git a/tests/cpp/rpytest/srconly/__init__.py b/tests/cpp/rpytest/srconly/__init__.py deleted file mode 100644 index 91e56528..00000000 --- a/tests/cpp/rpytest/srconly/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# autogenerated by 'robotpy-build create-imports rpytest.srconly rpytest.srconly._rpytest_srconly' -from ._rpytest_srconly import srconly_fn - -__all__ = ["srconly_fn"] diff --git a/tests/cpp/rpytest/srconly/srconly.cpp b/tests/cpp/rpytest/srconly/srconly.cpp deleted file mode 100644 index 0a101eae..00000000 --- a/tests/cpp/rpytest/srconly/srconly.cpp +++ /dev/null @@ -1,10 +0,0 @@ - -#include - -int srconly_fn(int val) { - return val - 0x42; -} - -RPYBUILD_PYBIND11_MODULE(m) { - m.def("srconly_fn", &srconly_fn); -} \ No newline at end of file diff --git a/tests/cpp/run_install.py b/tests/cpp/run_install.py index 2b45723c..717f9395 100755 --- a/tests/cpp/run_install.py +++ b/tests/cpp/run_install.py @@ -1,76 +1,41 @@ #!/usr/bin/env python3 -import http.server import os from os.path import abspath, dirname, join import pathlib import shutil import sys import subprocess -import threading import tempfile import zipfile -def create_artifact_path(path, group_id, artifact_id, version, classifier): - components = group_id.split(".") + [artifact_id, version] - path = join(path, *components) - os.makedirs(path, exist_ok=True) - fname = f"{artifact_id}-{version}-{classifier}.zip" - return join(path, fname) - - -def http_server(): - httpd = http.server.HTTPServer( - ("127.0.0.1", 0), http.server.SimpleHTTPRequestHandler - ) - - t = threading.Thread(target=httpd.serve_forever, daemon=True) - t.start() - - return httpd, httpd.socket.getsockname()[1] +def subprocess_must_run(*args, **kwargs): + """Run a subprocess verbosely and exit if there is an error""" + try: + print("+", *args[0]) + subprocess.run(check=True, *args, **kwargs) + except subprocess.CalledProcessError as cbe: + print(cbe, file=sys.stderr) + sys.exit(cbe.returncode) if __name__ == "__main__": root = abspath(dirname(__file__)) os.chdir(root) - # delete build/cache directory - shutil.rmtree(join(root, "build", "cache"), ignore_errors=True) - - # create tempdir with maven directory structure for pkg - with tempfile.TemporaryDirectory() as d: - # create headers and sources zip files - hname = create_artifact_path(d, "fake.dl", "dl", "1.2.3", "headers") - with zipfile.ZipFile(hname, "w") as z: - z.write(join(root, "dl", "downloaded.h"), "downloaded.h") - - sname = create_artifact_path(d, "fake.dl", "dl", "1.2.3", "sources") - with zipfile.ZipFile(sname, "w") as z: - z.write(join(root, "dl", "downloaded.cpp"), "downloaded.cpp") - - # http.server prior to 3.7 could only serve the current directory - os.chdir(d) + to_install = ["sw-test-base", "sw-caster-consumer", "sw-test"] - # start http server on random port - httpd, port = http_server() - - with open(join(root, "pyproject.toml.tmpl")) as rfp: - content = rfp.read().replace("RANDOM_PORT", str(port)) - with open(join(root, "pyproject.toml"), "w") as wfp: - wfp.write(content) - - cwd = None + # First, uninstall packages + subprocess_must_run( + [sys.executable, "-m", "pip", "--disable-pip-version-check", "uninstall", "-y"] + + to_install + ) - if len(sys.argv) == 2 and sys.argv[1] == "wheel": - cmd_args = [sys.executable, "-m", "build", "--wheel", "--no-isolation"] - cwd = root - elif len(sys.argv) == 2 and sys.argv[1] == "develop": - cmd_args = [sys.executable, "setup.py", "develop", "-N"] - cwd = root - else: - # run pip install - cmd_args = [ + # Now install them + for pkg in to_install: + subprocess_must_run( + [ sys.executable, "-m", "pip", @@ -78,17 +43,7 @@ def http_server(): "--disable-pip-version-check", "install", "--no-build-isolation", + os.path.abspath(pkg), ] - - if len(sys.argv) == 2 and sys.argv[1] == "-e": - cmd_args.append("-e") - - cmd_args.append(root) - - env = os.environ.copy() - env["SETUPTOOLS_SCM_PRETEND_VERSION"] = "0.0.1" - - subprocess.check_call(cmd_args, cwd=cwd, env=env) - - # Windows fails if you try to delete the directory you're currently in - os.chdir(root) + + sys.argv[1:] + ) diff --git a/tests/cpp/setup.py b/tests/cpp/setup.py deleted file mode 100755 index 3542d0c9..00000000 --- a/tests/cpp/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 - -from robotpy_build.setup import setup - -setup() diff --git a/tests/cpp/sw-caster-consumer/meson.build b/tests/cpp/sw-caster-consumer/meson.build new file mode 100644 index 00000000..160243e2 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/meson.build @@ -0,0 +1,11 @@ +project('sw-caster-consumer', ['cpp'], + default_options: ['warning_level=1', 'cpp_std=c++20', + 'b_colorout=auto', 'optimization=2', 'b_pie=true']) + +subdir('semiwrap') + +sw_caster_consumer__module_sources += files( + 'src/sw_caster_consumer/cpp/main.cpp', +) + +subdir('semiwrap/modules') diff --git a/tests/cpp/sw-caster-consumer/pyproject.toml b/tests/cpp/sw-caster-consumer/pyproject.toml new file mode 100644 index 00000000..9d9afcd7 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/pyproject.toml @@ -0,0 +1,29 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["semiwrap", "hatch-meson", "hatchling"] + +[project] +description = "Test program" +name = "sw-caster-consumer" +version = "0.0.1" + +[tool.hatch.build.targets.wheel] +packages = ['src/sw_caster_consumer'] + +[tool.hatch.build.hooks.semiwrap] + +[tool.hatch.build.hooks.meson] + +# +# Semiwrap configuration +# + +[tool.semiwrap] +[tool.semiwrap.extension_modules."sw_caster_consumer._module"] +depends = ["swtest_base__module"] + +[tool.semiwrap.extension_modules."sw_caster_consumer._module".headers] +more = "cpp/more.h" + +# checks that caster is included automatically when in a namespace +ns_more = "cpp/ns_more.h" \ No newline at end of file diff --git a/tests/cpp/sw-caster-consumer/semiwrap/.gitignore b/tests/cpp/sw-caster-consumer/semiwrap/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/semiwrap/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-caster-consumer/semiwrap/modules/.gitignore b/tests/cpp/sw-caster-consumer/semiwrap/modules/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/semiwrap/modules/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-caster-consumer/semiwrap/more.yml b/tests/cpp/sw-caster-consumer/semiwrap/more.yml new file mode 100644 index 00000000..f9dafd81 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/semiwrap/more.yml @@ -0,0 +1,4 @@ +--- + +functions: + add_more_to_inty: diff --git a/tests/cpp/sw-caster-consumer/semiwrap/ns_more.yml b/tests/cpp/sw-caster-consumer/semiwrap/ns_more.yml new file mode 100644 index 00000000..a9b89c3f --- /dev/null +++ b/tests/cpp/sw-caster-consumer/semiwrap/ns_more.yml @@ -0,0 +1,4 @@ +--- + +functions: + add_more_to_inty2: diff --git a/tests/cpp/sw-caster-consumer/semiwrap/trampolines/.gitignore b/tests/cpp/sw-caster-consumer/semiwrap/trampolines/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/semiwrap/trampolines/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/__init__.py b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/__init__.py new file mode 100644 index 00000000..7d849c08 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/__init__.py @@ -0,0 +1 @@ +from ._module import add_more_to_inty diff --git a/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/main.cpp b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/main.cpp new file mode 100644 index 00000000..ac260f64 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/main.cpp @@ -0,0 +1,5 @@ +#include + +SEMIWRAP_PYBIND11_MODULE(m) { + initWrapper(m); +} diff --git a/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/more.h b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/more.h new file mode 100644 index 00000000..e83312ed --- /dev/null +++ b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/more.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +inline inty add_more_to_inty(inty v, long value) { + v.long_value += value; + return v; +} diff --git a/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/ns_more.h b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/ns_more.h new file mode 100644 index 00000000..39417ee6 --- /dev/null +++ b/tests/cpp/sw-caster-consumer/src/sw_caster_consumer/cpp/ns_more.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace ns { + +inline inty2 add_more_to_inty2(inty2 v, long value) { + v.long_value += value; + return v; +} + +} diff --git a/tests/cpp/sw-test-base/.gitignore b/tests/cpp/sw-test-base/.gitignore new file mode 100644 index 00000000..3b0e1720 --- /dev/null +++ b/tests/cpp/sw-test-base/.gitignore @@ -0,0 +1,3 @@ +/src/swtest_base/sw-caster-test-casters.pc +/src/swtest_base/sw-caster-test-casters.pybind11.json +/src/swtest_base/trampolines \ No newline at end of file diff --git a/tests/cpp/sw-test-base/meson.build b/tests/cpp/sw-test-base/meson.build new file mode 100644 index 00000000..b33d5409 --- /dev/null +++ b/tests/cpp/sw-test-base/meson.build @@ -0,0 +1,19 @@ +project('swtest-base', ['cpp'], + default_options: ['warning_level=1', 'cpp_std=c++20', + 'b_colorout=auto', 'optimization=2', 'b_pie=true']) + +subdir('semiwrap') + +swtest_base__module_sources += files( + 'src/swtest_base/cpp/main.cpp', +) + +swtest_base__module2_sources += files( + 'src/swtest_base/cpp/main2.cpp', +) + +swtest_base__module3_sources += files( + 'src/swtest_base/cpp/main3.cpp', +) + +subdir('semiwrap/modules') diff --git a/tests/cpp/sw-test-base/pyproject.toml b/tests/cpp/sw-test-base/pyproject.toml new file mode 100644 index 00000000..35bbb60b --- /dev/null +++ b/tests/cpp/sw-test-base/pyproject.toml @@ -0,0 +1,52 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["semiwrap", "hatch-meson", "hatchling"] + +[project] +description = "Test program" +name = "sw-test-base" +version = "0.0.1" + +[tool.hatch.build.targets.wheel] +packages = ['src/swtest_base'] + +[tool.hatch.build.hooks.semiwrap] + +[tool.hatch.build.hooks.meson] + +# +# Semiwrap configuration +# + +[tool.semiwrap] +[tool.semiwrap.extension_modules."swtest_base._module"] +depends = ["sw-test-base-casters"] +includes = ["src/swtest_base/cpp"] + +[tool.semiwrap.extension_modules."swtest_base._module".headers] +fn = "cpp/fn.h" +base_class = "cpp/baseclass.h" + + +[tool.semiwrap.extension_modules."swtest_base._module2"] +depends = ["swtest_base__module", "swtest_base__module3"] + +[tool.semiwrap.extension_modules."swtest_base._module2".headers] +fn2 = "cpp/fn2.h" + + +[tool.semiwrap.extension_modules."swtest_base._module3"] +# empty module to test dependency on module declared after a module + + +[tool.semiwrap.export_type_casters.sw-test-base-casters] +pypackage = "swtest_base" +includedir = ["src/swtest_base/cpp/type_casters"] + +[[tool.semiwrap.export_type_casters.sw-test-base-casters.headers]] +header = "inty_cast.h" +types = ["inty"] + +[[tool.semiwrap.export_type_casters.sw-test-base-casters.headers]] +header = "ns_inty_cast.h" +types = ["ns::inty2"] diff --git a/tests/cpp/sw-test-base/semiwrap/.gitignore b/tests/cpp/sw-test-base/semiwrap/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-test-base/semiwrap/base_class.yml b/tests/cpp/sw-test-base/semiwrap/base_class.yml new file mode 100644 index 00000000..345ab863 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/base_class.yml @@ -0,0 +1,6 @@ +--- + +classes: + abaseclass: + methods: + fn: diff --git a/tests/cpp/sw-test-base/semiwrap/fn.yml b/tests/cpp/sw-test-base/semiwrap/fn.yml new file mode 100644 index 00000000..34aac061 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/fn.yml @@ -0,0 +1,4 @@ +--- + +functions: + add_to_inty: diff --git a/tests/cpp/sw-test-base/semiwrap/fn2.yml b/tests/cpp/sw-test-base/semiwrap/fn2.yml new file mode 100644 index 00000000..11ec4519 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/fn2.yml @@ -0,0 +1,4 @@ +--- + +functions: + something: diff --git a/tests/cpp/sw-test-base/semiwrap/modules/.gitignore b/tests/cpp/sw-test-base/semiwrap/modules/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/modules/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-test-base/semiwrap/trampolines/.gitignore b/tests/cpp/sw-test-base/semiwrap/trampolines/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test-base/semiwrap/trampolines/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-test-base/src/swtest_base/__init__.py b/tests/cpp/sw-test-base/src/swtest_base/__init__.py new file mode 100644 index 00000000..2e3f4cc1 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/__init__.py @@ -0,0 +1,4 @@ +# for type caster +inty = int + +from ._module import add_to_inty diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/baseclass.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/baseclass.h new file mode 100644 index 00000000..1b8c67c8 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/baseclass.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +// tests trampoline across packages +class abaseclass { +public: + virtual ~abaseclass() = default; + + inline virtual std::string fn() { + return "abaseclass"; + } + +}; \ No newline at end of file diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/fn.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/fn.h new file mode 100644 index 00000000..6d526314 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/fn.h @@ -0,0 +1,8 @@ +#pragma once + +#include "inty.h" + +inline inty add_to_inty(inty v, long value) { + v.long_value += value; + return v; +} diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/fn2.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/fn2.h new file mode 100644 index 00000000..871b9ab2 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/fn2.h @@ -0,0 +1,5 @@ +#pragma once + +int something() { + return 1; +} \ No newline at end of file diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/inty.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/inty.h new file mode 100644 index 00000000..756b30f9 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/inty.h @@ -0,0 +1,3 @@ +#pragma once + +struct inty { long long_value; }; diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/main.cpp b/tests/cpp/sw-test-base/src/swtest_base/cpp/main.cpp new file mode 100644 index 00000000..a06c0a6d --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/main.cpp @@ -0,0 +1,5 @@ +#include + +SEMIWRAP_PYBIND11_MODULE(m) { + initWrapper(m); +} diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/main2.cpp b/tests/cpp/sw-test-base/src/swtest_base/cpp/main2.cpp new file mode 100644 index 00000000..13bcf96f --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/main2.cpp @@ -0,0 +1,5 @@ +#include + +SEMIWRAP_PYBIND11_MODULE(m) { + initWrapper(m); +} diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/main3.cpp b/tests/cpp/sw-test-base/src/swtest_base/cpp/main3.cpp new file mode 100644 index 00000000..1ff8e014 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/main3.cpp @@ -0,0 +1,5 @@ +#include + +SEMIWRAP_PYBIND11_MODULE(m) { + initWrapper(m); +} diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/ns_inty.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/ns_inty.h new file mode 100644 index 00000000..2348f14e --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/ns_inty.h @@ -0,0 +1,9 @@ +#pragma once + +namespace ns { + +struct inty2 { long long_value; }; + +} + + diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/remote_class.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/remote_class.h new file mode 100644 index 00000000..d4e7a9c3 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/remote_class.h @@ -0,0 +1,4 @@ +#pragma once + +// Make sure we can wrap a class from a different package +struct RemoteClass {}; diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/inty_cast.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/inty_cast.h new file mode 100644 index 00000000..67796590 --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/inty_cast.h @@ -0,0 +1,51 @@ +#pragma once + +// +// From pybind11 documentation +// + +#include + +#include "../inty.h" + +namespace pybind11 { namespace detail { + template <> struct type_caster { + public: + /** + * This macro establishes the name 'inty' in + * function signatures and declares a local variable + * 'value' of type inty + */ + PYBIND11_TYPE_CASTER(inty, const_name("swtest_base.inty")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a inty + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool) { + /* Extract PyObject from handle */ + PyObject *source = src.ptr(); + /* Try converting into a Python integer value */ + PyObject *tmp = PyNumber_Long(source); + if (!tmp) + return false; + /* Now try to convert into a C++ int */ + value.long_value = PyLong_AsLong(tmp); + Py_DECREF(tmp); + /* Ensure return code was OK (to avoid out-of-range errors etc) */ + return !(value.long_value == -1 && !PyErr_Occurred()); + } + + /** + * Conversion part 2 (C++ -> Python): convert an inty instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(inty src, return_value_policy /* policy */, handle /* parent */) { + return PyLong_FromLong(src.long_value); + } + }; +}} // namespace PYBIND11_NAMESPACE::detail diff --git a/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/ns_inty_cast.h b/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/ns_inty_cast.h new file mode 100644 index 00000000..bdb6691f --- /dev/null +++ b/tests/cpp/sw-test-base/src/swtest_base/cpp/type_casters/ns_inty_cast.h @@ -0,0 +1,51 @@ +#pragma once + +// +// From pybind11 documentation +// + +#include + +#include "../ns_inty.h" + +namespace pybind11 { namespace detail { + template <> struct type_caster { + public: + /** + * This macro establishes the name 'inty' in + * function signatures and declares a local variable + * 'value' of type inty + */ + PYBIND11_TYPE_CASTER(ns::inty2, const_name("swtest_base.inty")); + + /** + * Conversion part 1 (Python->C++): convert a PyObject into a inty + * instance or return false upon failure. The second argument + * indicates whether implicit conversions should be applied. + */ + bool load(handle src, bool) { + /* Extract PyObject from handle */ + PyObject *source = src.ptr(); + /* Try converting into a Python integer value */ + PyObject *tmp = PyNumber_Long(source); + if (!tmp) + return false; + /* Now try to convert into a C++ int */ + value.long_value = PyLong_AsLong(tmp); + Py_DECREF(tmp); + /* Ensure return code was OK (to avoid out-of-range errors etc) */ + return !(value.long_value == -1 && !PyErr_Occurred()); + } + + /** + * Conversion part 2 (C++ -> Python): convert an inty instance into + * a Python object. The second and third arguments are used to + * indicate the return value policy and parent object (for + * ``return_value_policy::reference_internal``) and are generally + * ignored by implicit casters. + */ + static handle cast(ns::inty2 src, return_value_policy /* policy */, handle /* parent */) { + return PyLong_FromLong(src.long_value); + } + }; +}} // namespace PYBIND11_NAMESPACE::detail diff --git a/tests/cpp/sw-test/meson.build b/tests/cpp/sw-test/meson.build new file mode 100644 index 00000000..6e4f06f0 --- /dev/null +++ b/tests/cpp/sw-test/meson.build @@ -0,0 +1,13 @@ +project('sw-test', ['cpp'], + default_options: ['warning_level=1', 'cpp_std=c++20', + 'b_colorout=auto', 'optimization=2', 'b_pie=true']) + +subdir('semiwrap') + +swtest_ft__ft_sources += files( + 'src/swtest/ft/src/fields.cpp', + 'src/swtest/ft/src/ft.cpp', + 'src/swtest/ft/src/using2.cpp', +) + +subdir('semiwrap/modules') diff --git a/tests/cpp/pyproject.toml.tmpl b/tests/cpp/sw-test/pyproject.toml similarity index 57% rename from tests/cpp/pyproject.toml.tmpl rename to tests/cpp/sw-test/pyproject.toml index ccf16e87..1f2d403f 100644 --- a/tests/cpp/pyproject.toml.tmpl +++ b/tests/cpp/sw-test/pyproject.toml @@ -1,48 +1,32 @@ [build-system] -requires = ["robotpy-build"] +build-backend = "hatchling.build" +requires = ["semiwrap", "hatch-meson", "hatchling"] -[tool.robotpy-build] -base_package = "rpytest" +[project] +name = "sw-test" +description = "semiwrap test program" +version = "0.0.1" -[tool.robotpy-build.wrappers."rpytest.dl".maven_lib_download] -artifact_id = "dl" -group_id = "fake.dl" -repo_url = "http://127.0.0.1:RANDOM_PORT" -version = "1.2.3" -use_sources = true -sources = [ - "downloaded.cpp" -] +[tool.hatch.build.hooks.semiwrap] -[[tool.robotpy-build.wrappers."rpytest.dl".maven_lib_download.header_patches]] -patch = "rpytest/dl/dl.patch" -strip = 0 +[tool.hatch.build.hooks.meson] -[tool.robotpy-build.wrappers."rpytest.dl"] -name = "rpytest_dl" +[tool.hatch.build.targets.wheel] +packages = ["src/swtest"] -sources = [ - "rpytest/dl/dl.cpp" -] +# +# Semiwrap configuration +# -generation_data = "gen/dl" +[tool.semiwrap] -[tool.robotpy-build.wrappers."rpytest.dl".autogen_headers] -downloaded = "downloaded.h" +[tool.semiwrap.extension_modules."swtest.ft._ft"] +depends = ["sw-test-casters"] +wraps = ["swtest_base__module"] +yaml_path = "semiwrap/ft" +includes = ["src/swtest/ft/include"] -[tool.robotpy-build.wrappers."rpytest.ft"] -name = "rpytest_ft" -depends = ["rpytest_tc"] - -sources = [ - "rpytest/ft/src/fields.cpp", - "rpytest/ft/src/ft.cpp", - "rpytest/ft/src/using2.cpp", -] - -generation_data = "gen/ft" - -[tool.robotpy-build.wrappers."rpytest.ft".autogen_headers] +[tool.semiwrap.extension_modules."swtest.ft._ft".headers] abstract = "abstract.h" base_qualname = "base_qualname.h" base_qualname_hidden = "base_qualname_hidden.h" @@ -67,6 +51,8 @@ operators = "operators.h" overloads = "overloads.h" parameters = "parameters.h" refqual = "refqual.h" +remote_class = "remote_class.h" +remote_trampoline = "remote_trampoline.h" rename = "rename.h" retval = "retval.h" subpkg = "subpkg.h" @@ -112,30 +98,18 @@ tnumeric = "templates/numeric.h" tnested = "templates/nested.h" -[tool.robotpy-build.wrappers."rpytest.srconly"] -name = "rpytest_srconly" -sources = [ - "rpytest/srconly/srconly.cpp" -] -[tool.robotpy-build.wrappers."rpytest.tc"] -name = "rpytest_tc" +[tool.semiwrap.export_type_casters.sw-test-casters] +pypackage = "swtest.tc" +includedir = ["src/swtest/tc/include"] -[[tool.robotpy-build.wrappers."rpytest.tc".type_casters]] +[[tool.semiwrap.export_type_casters.sw-test-casters.headers]] header = "rpyint_type_caster.h" types = ["rpy::rpyint"] default_arg_cast = true -[[tool.robotpy-build.wrappers."rpytest.tc".type_casters]] +[[tool.semiwrap.export_type_casters.sw-test-casters.headers]] header = "rpyint_type_caster.h" types = ["rpyint_plus_5"] -[tool.robotpy-build.metadata] -name = "robotpy-build-test" -description = "robotpy-build test program" -author = "RobotPy Development Team" -author_email = "robotpy@googlegroups.com" -url = "https://github.com/robotpy/robotpy-build" -license = "BSD-3-Clause" -install_requires = [] \ No newline at end of file diff --git a/tests/cpp/sw-test/semiwrap/.gitignore b/tests/cpp/sw-test/semiwrap/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/gen/ft/IBase.yml b/tests/cpp/sw-test/semiwrap/ft/IBase.yml similarity index 100% rename from tests/cpp/gen/ft/IBase.yml rename to tests/cpp/sw-test/semiwrap/ft/IBase.yml diff --git a/tests/cpp/gen/ft/IChild.yml b/tests/cpp/sw-test/semiwrap/ft/IChild.yml similarity index 100% rename from tests/cpp/gen/ft/IChild.yml rename to tests/cpp/sw-test/semiwrap/ft/IChild.yml diff --git a/tests/cpp/gen/ft/IGChild.yml b/tests/cpp/sw-test/semiwrap/ft/IGChild.yml similarity index 100% rename from tests/cpp/gen/ft/IGChild.yml rename to tests/cpp/sw-test/semiwrap/ft/IGChild.yml diff --git a/tests/cpp/gen/ft/IMChild.yml b/tests/cpp/sw-test/semiwrap/ft/IMChild.yml similarity index 100% rename from tests/cpp/gen/ft/IMChild.yml rename to tests/cpp/sw-test/semiwrap/ft/IMChild.yml diff --git a/tests/cpp/gen/ft/Overloaded.yml b/tests/cpp/sw-test/semiwrap/ft/Overloaded.yml similarity index 100% rename from tests/cpp/gen/ft/Overloaded.yml rename to tests/cpp/sw-test/semiwrap/ft/Overloaded.yml diff --git a/tests/cpp/gen/ft/PBase.yml b/tests/cpp/sw-test/semiwrap/ft/PBase.yml similarity index 100% rename from tests/cpp/gen/ft/PBase.yml rename to tests/cpp/sw-test/semiwrap/ft/PBase.yml diff --git a/tests/cpp/gen/ft/PChild.yml b/tests/cpp/sw-test/semiwrap/ft/PChild.yml similarity index 100% rename from tests/cpp/gen/ft/PChild.yml rename to tests/cpp/sw-test/semiwrap/ft/PChild.yml diff --git a/tests/cpp/gen/ft/PGChild.yml b/tests/cpp/sw-test/semiwrap/ft/PGChild.yml similarity index 100% rename from tests/cpp/gen/ft/PGChild.yml rename to tests/cpp/sw-test/semiwrap/ft/PGChild.yml diff --git a/tests/cpp/gen/ft/abstract.yml b/tests/cpp/sw-test/semiwrap/ft/abstract.yml similarity index 100% rename from tests/cpp/gen/ft/abstract.yml rename to tests/cpp/sw-test/semiwrap/ft/abstract.yml diff --git a/tests/cpp/gen/ft/base_qualname.yml b/tests/cpp/sw-test/semiwrap/ft/base_qualname.yml similarity index 100% rename from tests/cpp/gen/ft/base_qualname.yml rename to tests/cpp/sw-test/semiwrap/ft/base_qualname.yml diff --git a/tests/cpp/gen/ft/base_qualname_hidden.yml b/tests/cpp/sw-test/semiwrap/ft/base_qualname_hidden.yml similarity index 100% rename from tests/cpp/gen/ft/base_qualname_hidden.yml rename to tests/cpp/sw-test/semiwrap/ft/base_qualname_hidden.yml diff --git a/tests/cpp/gen/ft/buffers.yml b/tests/cpp/sw-test/semiwrap/ft/buffers.yml similarity index 100% rename from tests/cpp/gen/ft/buffers.yml rename to tests/cpp/sw-test/semiwrap/ft/buffers.yml diff --git a/tests/cpp/gen/ft/custom_type_caster.yml b/tests/cpp/sw-test/semiwrap/ft/custom_type_caster.yml similarity index 100% rename from tests/cpp/gen/ft/custom_type_caster.yml rename to tests/cpp/sw-test/semiwrap/ft/custom_type_caster.yml diff --git a/tests/cpp/gen/ft/defaults.yml b/tests/cpp/sw-test/semiwrap/ft/defaults.yml similarity index 100% rename from tests/cpp/gen/ft/defaults.yml rename to tests/cpp/sw-test/semiwrap/ft/defaults.yml diff --git a/tests/cpp/gen/ft/docstrings.yml b/tests/cpp/sw-test/semiwrap/ft/docstrings.yml similarity index 100% rename from tests/cpp/gen/ft/docstrings.yml rename to tests/cpp/sw-test/semiwrap/ft/docstrings.yml diff --git a/tests/cpp/gen/ft/docstrings_append.yml b/tests/cpp/sw-test/semiwrap/ft/docstrings_append.yml similarity index 100% rename from tests/cpp/gen/ft/docstrings_append.yml rename to tests/cpp/sw-test/semiwrap/ft/docstrings_append.yml diff --git a/tests/cpp/gen/ft/enums.yml b/tests/cpp/sw-test/semiwrap/ft/enums.yml similarity index 100% rename from tests/cpp/gen/ft/enums.yml rename to tests/cpp/sw-test/semiwrap/ft/enums.yml diff --git a/tests/cpp/gen/ft/factory.yml b/tests/cpp/sw-test/semiwrap/ft/factory.yml similarity index 100% rename from tests/cpp/gen/ft/factory.yml rename to tests/cpp/sw-test/semiwrap/ft/factory.yml diff --git a/tests/cpp/gen/ft/fields.yml b/tests/cpp/sw-test/semiwrap/ft/fields.yml similarity index 100% rename from tests/cpp/gen/ft/fields.yml rename to tests/cpp/sw-test/semiwrap/ft/fields.yml diff --git a/tests/cpp/gen/ft/gilsafe_container.yml b/tests/cpp/sw-test/semiwrap/ft/gilsafe_container.yml similarity index 100% rename from tests/cpp/gen/ft/gilsafe_container.yml rename to tests/cpp/sw-test/semiwrap/ft/gilsafe_container.yml diff --git a/tests/cpp/gen/ft/ignore.yml b/tests/cpp/sw-test/semiwrap/ft/ignore.yml similarity index 100% rename from tests/cpp/gen/ft/ignore.yml rename to tests/cpp/sw-test/semiwrap/ft/ignore.yml diff --git a/tests/cpp/gen/ft/ignored_by_default.yml b/tests/cpp/sw-test/semiwrap/ft/ignored_by_default.yml similarity index 100% rename from tests/cpp/gen/ft/ignored_by_default.yml rename to tests/cpp/sw-test/semiwrap/ft/ignored_by_default.yml diff --git a/tests/cpp/gen/ft/inline_code.yml b/tests/cpp/sw-test/semiwrap/ft/inline_code.yml similarity index 100% rename from tests/cpp/gen/ft/inline_code.yml rename to tests/cpp/sw-test/semiwrap/ft/inline_code.yml diff --git a/tests/cpp/gen/ft/keepalive.yml b/tests/cpp/sw-test/semiwrap/ft/keepalive.yml similarity index 100% rename from tests/cpp/gen/ft/keepalive.yml rename to tests/cpp/sw-test/semiwrap/ft/keepalive.yml diff --git a/tests/cpp/gen/ft/lifetime.yml b/tests/cpp/sw-test/semiwrap/ft/lifetime.yml similarity index 100% rename from tests/cpp/gen/ft/lifetime.yml rename to tests/cpp/sw-test/semiwrap/ft/lifetime.yml diff --git a/tests/cpp/gen/ft/mvi.yml b/tests/cpp/sw-test/semiwrap/ft/mvi.yml similarity index 100% rename from tests/cpp/gen/ft/mvi.yml rename to tests/cpp/sw-test/semiwrap/ft/mvi.yml diff --git a/tests/cpp/gen/ft/nested.yml b/tests/cpp/sw-test/semiwrap/ft/nested.yml similarity index 100% rename from tests/cpp/gen/ft/nested.yml rename to tests/cpp/sw-test/semiwrap/ft/nested.yml diff --git a/tests/cpp/gen/ft/ns_class.yml b/tests/cpp/sw-test/semiwrap/ft/ns_class.yml similarity index 100% rename from tests/cpp/gen/ft/ns_class.yml rename to tests/cpp/sw-test/semiwrap/ft/ns_class.yml diff --git a/tests/cpp/gen/ft/ns_hidden.yml b/tests/cpp/sw-test/semiwrap/ft/ns_hidden.yml similarity index 100% rename from tests/cpp/gen/ft/ns_hidden.yml rename to tests/cpp/sw-test/semiwrap/ft/ns_hidden.yml diff --git a/tests/cpp/gen/ft/operators.yml b/tests/cpp/sw-test/semiwrap/ft/operators.yml similarity index 100% rename from tests/cpp/gen/ft/operators.yml rename to tests/cpp/sw-test/semiwrap/ft/operators.yml diff --git a/tests/cpp/gen/ft/overloads.yml b/tests/cpp/sw-test/semiwrap/ft/overloads.yml similarity index 100% rename from tests/cpp/gen/ft/overloads.yml rename to tests/cpp/sw-test/semiwrap/ft/overloads.yml diff --git a/tests/cpp/gen/ft/parameters.yml b/tests/cpp/sw-test/semiwrap/ft/parameters.yml similarity index 100% rename from tests/cpp/gen/ft/parameters.yml rename to tests/cpp/sw-test/semiwrap/ft/parameters.yml diff --git a/tests/cpp/gen/ft/refqual.yml b/tests/cpp/sw-test/semiwrap/ft/refqual.yml similarity index 100% rename from tests/cpp/gen/ft/refqual.yml rename to tests/cpp/sw-test/semiwrap/ft/refqual.yml diff --git a/tests/cpp/sw-test/semiwrap/ft/remote_class.yml b/tests/cpp/sw-test/semiwrap/ft/remote_class.yml new file mode 100644 index 00000000..72a4a2a7 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/ft/remote_class.yml @@ -0,0 +1,4 @@ +--- + +classes: + RemoteClass: diff --git a/tests/cpp/sw-test/semiwrap/ft/remote_trampoline.yml b/tests/cpp/sw-test/semiwrap/ft/remote_trampoline.yml new file mode 100644 index 00000000..c74ec9ad --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/ft/remote_trampoline.yml @@ -0,0 +1,6 @@ +--- + +classes: + RemoteTrampoline: + methods: + fn: diff --git a/tests/cpp/gen/ft/rename.yml b/tests/cpp/sw-test/semiwrap/ft/rename.yml similarity index 100% rename from tests/cpp/gen/ft/rename.yml rename to tests/cpp/sw-test/semiwrap/ft/rename.yml diff --git a/tests/cpp/gen/ft/retval.yml b/tests/cpp/sw-test/semiwrap/ft/retval.yml similarity index 100% rename from tests/cpp/gen/ft/retval.yml rename to tests/cpp/sw-test/semiwrap/ft/retval.yml diff --git a/tests/cpp/gen/ft/static_only.yml b/tests/cpp/sw-test/semiwrap/ft/static_only.yml similarity index 100% rename from tests/cpp/gen/ft/static_only.yml rename to tests/cpp/sw-test/semiwrap/ft/static_only.yml diff --git a/tests/cpp/gen/ft/subpkg.yml b/tests/cpp/sw-test/semiwrap/ft/subpkg.yml similarity index 100% rename from tests/cpp/gen/ft/subpkg.yml rename to tests/cpp/sw-test/semiwrap/ft/subpkg.yml diff --git a/tests/cpp/gen/ft/tbase.yml b/tests/cpp/sw-test/semiwrap/ft/tbase.yml similarity index 100% rename from tests/cpp/gen/ft/tbase.yml rename to tests/cpp/sw-test/semiwrap/ft/tbase.yml diff --git a/tests/cpp/gen/ft/tbasic.yml b/tests/cpp/sw-test/semiwrap/ft/tbasic.yml similarity index 100% rename from tests/cpp/gen/ft/tbasic.yml rename to tests/cpp/sw-test/semiwrap/ft/tbasic.yml diff --git a/tests/cpp/gen/ft/tconcrete.yml b/tests/cpp/sw-test/semiwrap/ft/tconcrete.yml similarity index 100% rename from tests/cpp/gen/ft/tconcrete.yml rename to tests/cpp/sw-test/semiwrap/ft/tconcrete.yml diff --git a/tests/cpp/gen/ft/tcrtp.yml b/tests/cpp/sw-test/semiwrap/ft/tcrtp.yml similarity index 100% rename from tests/cpp/gen/ft/tcrtp.yml rename to tests/cpp/sw-test/semiwrap/ft/tcrtp.yml diff --git a/tests/cpp/gen/ft/tcrtpfwd.yml b/tests/cpp/sw-test/semiwrap/ft/tcrtpfwd.yml similarity index 100% rename from tests/cpp/gen/ft/tcrtpfwd.yml rename to tests/cpp/sw-test/semiwrap/ft/tcrtpfwd.yml diff --git a/tests/cpp/gen/ft/tdependent_base.yml b/tests/cpp/sw-test/semiwrap/ft/tdependent_base.yml similarity index 100% rename from tests/cpp/gen/ft/tdependent_base.yml rename to tests/cpp/sw-test/semiwrap/ft/tdependent_base.yml diff --git a/tests/cpp/gen/ft/tdependent_param.yml b/tests/cpp/sw-test/semiwrap/ft/tdependent_param.yml similarity index 100% rename from tests/cpp/gen/ft/tdependent_param.yml rename to tests/cpp/sw-test/semiwrap/ft/tdependent_param.yml diff --git a/tests/cpp/gen/ft/tdependent_using.yml b/tests/cpp/sw-test/semiwrap/ft/tdependent_using.yml similarity index 100% rename from tests/cpp/gen/ft/tdependent_using.yml rename to tests/cpp/sw-test/semiwrap/ft/tdependent_using.yml diff --git a/tests/cpp/gen/ft/tdependent_using2.yml b/tests/cpp/sw-test/semiwrap/ft/tdependent_using2.yml similarity index 100% rename from tests/cpp/gen/ft/tdependent_using2.yml rename to tests/cpp/sw-test/semiwrap/ft/tdependent_using2.yml diff --git a/tests/cpp/gen/ft/tfn.yml b/tests/cpp/sw-test/semiwrap/ft/tfn.yml similarity index 100% rename from tests/cpp/gen/ft/tfn.yml rename to tests/cpp/sw-test/semiwrap/ft/tfn.yml diff --git a/tests/cpp/gen/ft/tnested.yml b/tests/cpp/sw-test/semiwrap/ft/tnested.yml similarity index 100% rename from tests/cpp/gen/ft/tnested.yml rename to tests/cpp/sw-test/semiwrap/ft/tnested.yml diff --git a/tests/cpp/gen/ft/tnumeric.yml b/tests/cpp/sw-test/semiwrap/ft/tnumeric.yml similarity index 100% rename from tests/cpp/gen/ft/tnumeric.yml rename to tests/cpp/sw-test/semiwrap/ft/tnumeric.yml diff --git a/tests/cpp/gen/ft/trampoline.yml b/tests/cpp/sw-test/semiwrap/ft/trampoline.yml similarity index 100% rename from tests/cpp/gen/ft/trampoline.yml rename to tests/cpp/sw-test/semiwrap/ft/trampoline.yml diff --git a/tests/cpp/gen/ft/tvbase.yml b/tests/cpp/sw-test/semiwrap/ft/tvbase.yml similarity index 100% rename from tests/cpp/gen/ft/tvbase.yml rename to tests/cpp/sw-test/semiwrap/ft/tvbase.yml diff --git a/tests/cpp/gen/ft/tvchild.yml b/tests/cpp/sw-test/semiwrap/ft/tvchild.yml similarity index 100% rename from tests/cpp/gen/ft/tvchild.yml rename to tests/cpp/sw-test/semiwrap/ft/tvchild.yml diff --git a/tests/cpp/gen/ft/type_caster.yml b/tests/cpp/sw-test/semiwrap/ft/type_caster.yml similarity index 100% rename from tests/cpp/gen/ft/type_caster.yml rename to tests/cpp/sw-test/semiwrap/ft/type_caster.yml diff --git a/tests/cpp/gen/ft/type_caster_nested.yml b/tests/cpp/sw-test/semiwrap/ft/type_caster_nested.yml similarity index 100% rename from tests/cpp/gen/ft/type_caster_nested.yml rename to tests/cpp/sw-test/semiwrap/ft/type_caster_nested.yml diff --git a/tests/cpp/gen/ft/using.yml b/tests/cpp/sw-test/semiwrap/ft/using.yml similarity index 100% rename from tests/cpp/gen/ft/using.yml rename to tests/cpp/sw-test/semiwrap/ft/using.yml diff --git a/tests/cpp/gen/ft/using2.yml b/tests/cpp/sw-test/semiwrap/ft/using2.yml similarity index 100% rename from tests/cpp/gen/ft/using2.yml rename to tests/cpp/sw-test/semiwrap/ft/using2.yml diff --git a/tests/cpp/gen/ft/usingparent.yml b/tests/cpp/sw-test/semiwrap/ft/usingparent.yml similarity index 100% rename from tests/cpp/gen/ft/usingparent.yml rename to tests/cpp/sw-test/semiwrap/ft/usingparent.yml diff --git a/tests/cpp/gen/ft/virtual_comma.yml b/tests/cpp/sw-test/semiwrap/ft/virtual_comma.yml similarity index 100% rename from tests/cpp/gen/ft/virtual_comma.yml rename to tests/cpp/sw-test/semiwrap/ft/virtual_comma.yml diff --git a/tests/cpp/gen/ft/virtual_xform.yml b/tests/cpp/sw-test/semiwrap/ft/virtual_xform.yml similarity index 100% rename from tests/cpp/gen/ft/virtual_xform.yml rename to tests/cpp/sw-test/semiwrap/ft/virtual_xform.yml diff --git a/tests/cpp/sw-test/semiwrap/modules/.gitignore b/tests/cpp/sw-test/semiwrap/modules/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/modules/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/sw-test/semiwrap/trampolines/.gitignore b/tests/cpp/sw-test/semiwrap/trampolines/.gitignore new file mode 100644 index 00000000..d698f725 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/trampolines/.gitignore @@ -0,0 +1 @@ +/meson.build diff --git a/tests/cpp/rpytest/__init__.py b/tests/cpp/sw-test/src/swtest/__init__.py similarity index 100% rename from tests/cpp/rpytest/__init__.py rename to tests/cpp/sw-test/src/swtest/__init__.py diff --git a/tests/cpp/rpytest/ft/__init__.py b/tests/cpp/sw-test/src/swtest/ft/__init__.py similarity index 96% rename from tests/cpp/rpytest/ft/__init__.py rename to tests/cpp/sw-test/src/swtest/ft/__init__.py index 2851c37c..16b9aa40 100644 --- a/tests/cpp/rpytest/ft/__init__.py +++ b/tests/cpp/sw-test/src/swtest/ft/__init__.py @@ -1,7 +1,7 @@ -from . import _init_rpytest_ft +import swtest_base -# autogenerated by 'robotpy-build create-imports rpytest.ft rpytest.ft._rpytest_ft' -from ._rpytest_ft import ( +# autogenerated by 'robotpy-build create-imports rpytest.ft swtest.ft._ft' +from ._ft import ( Abstract, Buffers, ClassWithFields, diff --git a/tests/cpp/rpytest/ft/include/abstract.h b/tests/cpp/sw-test/src/swtest/ft/include/abstract.h similarity index 100% rename from tests/cpp/rpytest/ft/include/abstract.h rename to tests/cpp/sw-test/src/swtest/ft/include/abstract.h diff --git a/tests/cpp/rpytest/ft/include/base_qualname.h b/tests/cpp/sw-test/src/swtest/ft/include/base_qualname.h similarity index 100% rename from tests/cpp/rpytest/ft/include/base_qualname.h rename to tests/cpp/sw-test/src/swtest/ft/include/base_qualname.h diff --git a/tests/cpp/rpytest/ft/include/base_qualname_hidden.h b/tests/cpp/sw-test/src/swtest/ft/include/base_qualname_hidden.h similarity index 100% rename from tests/cpp/rpytest/ft/include/base_qualname_hidden.h rename to tests/cpp/sw-test/src/swtest/ft/include/base_qualname_hidden.h diff --git a/tests/cpp/rpytest/ft/include/buffers.h b/tests/cpp/sw-test/src/swtest/ft/include/buffers.h similarity index 100% rename from tests/cpp/rpytest/ft/include/buffers.h rename to tests/cpp/sw-test/src/swtest/ft/include/buffers.h diff --git a/tests/cpp/rpytest/ft/include/custom_type_caster.h b/tests/cpp/sw-test/src/swtest/ft/include/custom_type_caster.h similarity index 100% rename from tests/cpp/rpytest/ft/include/custom_type_caster.h rename to tests/cpp/sw-test/src/swtest/ft/include/custom_type_caster.h diff --git a/tests/cpp/rpytest/ft/include/defaults.h b/tests/cpp/sw-test/src/swtest/ft/include/defaults.h similarity index 100% rename from tests/cpp/rpytest/ft/include/defaults.h rename to tests/cpp/sw-test/src/swtest/ft/include/defaults.h diff --git a/tests/cpp/rpytest/ft/include/docstrings.h b/tests/cpp/sw-test/src/swtest/ft/include/docstrings.h similarity index 100% rename from tests/cpp/rpytest/ft/include/docstrings.h rename to tests/cpp/sw-test/src/swtest/ft/include/docstrings.h diff --git a/tests/cpp/rpytest/ft/include/docstrings_append.h b/tests/cpp/sw-test/src/swtest/ft/include/docstrings_append.h similarity index 100% rename from tests/cpp/rpytest/ft/include/docstrings_append.h rename to tests/cpp/sw-test/src/swtest/ft/include/docstrings_append.h diff --git a/tests/cpp/rpytest/ft/include/enums.h b/tests/cpp/sw-test/src/swtest/ft/include/enums.h similarity index 100% rename from tests/cpp/rpytest/ft/include/enums.h rename to tests/cpp/sw-test/src/swtest/ft/include/enums.h diff --git a/tests/cpp/rpytest/ft/include/factory.h b/tests/cpp/sw-test/src/swtest/ft/include/factory.h similarity index 100% rename from tests/cpp/rpytest/ft/include/factory.h rename to tests/cpp/sw-test/src/swtest/ft/include/factory.h diff --git a/tests/cpp/rpytest/ft/include/fields.h b/tests/cpp/sw-test/src/swtest/ft/include/fields.h similarity index 100% rename from tests/cpp/rpytest/ft/include/fields.h rename to tests/cpp/sw-test/src/swtest/ft/include/fields.h diff --git a/tests/cpp/rpytest/ft/include/gilsafe_container.h b/tests/cpp/sw-test/src/swtest/ft/include/gilsafe_container.h similarity index 82% rename from tests/cpp/rpytest/ft/include/gilsafe_container.h rename to tests/cpp/sw-test/src/swtest/ft/include/gilsafe_container.h index ee06c851..16b86fc3 100644 --- a/tests/cpp/rpytest/ft/include/gilsafe_container.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/gilsafe_container.h @@ -4,9 +4,9 @@ #include class GilsafeContainer { - rpy::gilsafe_object m_o; + semiwrap::gilsafe_object m_o; public: - void assign(rpy::gilsafe_object o) { + void assign(semiwrap::gilsafe_object o) { m_o = o; } diff --git a/tests/cpp/rpytest/ft/include/ignore.h b/tests/cpp/sw-test/src/swtest/ft/include/ignore.h similarity index 100% rename from tests/cpp/rpytest/ft/include/ignore.h rename to tests/cpp/sw-test/src/swtest/ft/include/ignore.h diff --git a/tests/cpp/rpytest/ft/include/ignored_by_default.h b/tests/cpp/sw-test/src/swtest/ft/include/ignored_by_default.h similarity index 100% rename from tests/cpp/rpytest/ft/include/ignored_by_default.h rename to tests/cpp/sw-test/src/swtest/ft/include/ignored_by_default.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/ibase.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/ibase.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/ibase.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/ibase.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/ichild.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/ichild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/ichild.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/ichild.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/igchild.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/igchild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/igchild.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/igchild.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/imchild.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/imchild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/imchild.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/imchild.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/mvi.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/mvi.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/mvi.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/mvi.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/overloaded.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/overloaded.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/overloaded.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/overloaded.h diff --git a/tests/cpp/rpytest/ft/include/inheritance/usingparent.h b/tests/cpp/sw-test/src/swtest/ft/include/inheritance/usingparent.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inheritance/usingparent.h rename to tests/cpp/sw-test/src/swtest/ft/include/inheritance/usingparent.h diff --git a/tests/cpp/rpytest/ft/include/inline_code.h b/tests/cpp/sw-test/src/swtest/ft/include/inline_code.h similarity index 100% rename from tests/cpp/rpytest/ft/include/inline_code.h rename to tests/cpp/sw-test/src/swtest/ft/include/inline_code.h diff --git a/tests/cpp/rpytest/ft/include/keepalive.h b/tests/cpp/sw-test/src/swtest/ft/include/keepalive.h similarity index 76% rename from tests/cpp/rpytest/ft/include/keepalive.h rename to tests/cpp/sw-test/src/swtest/ft/include/keepalive.h index 1cb8653b..179da77b 100644 --- a/tests/cpp/rpytest/ft/include/keepalive.h +++ b/tests/cpp/sw-test/src/swtest/ft/include/keepalive.h @@ -13,7 +13,7 @@ struct PatientRef struct Nurse { - // robotpy-build should auto-detect the reference and add a keepalive here + // semiwrap should auto-detect the reference and add a keepalive here Nurse(PatientRef &p) : m_p(p) {} bool patientDead() diff --git a/tests/cpp/rpytest/ft/include/lifetime.h b/tests/cpp/sw-test/src/swtest/ft/include/lifetime.h similarity index 100% rename from tests/cpp/rpytest/ft/include/lifetime.h rename to tests/cpp/sw-test/src/swtest/ft/include/lifetime.h diff --git a/tests/cpp/rpytest/ft/include/nested.h b/tests/cpp/sw-test/src/swtest/ft/include/nested.h similarity index 100% rename from tests/cpp/rpytest/ft/include/nested.h rename to tests/cpp/sw-test/src/swtest/ft/include/nested.h diff --git a/tests/cpp/rpytest/ft/include/ns_class.h b/tests/cpp/sw-test/src/swtest/ft/include/ns_class.h similarity index 100% rename from tests/cpp/rpytest/ft/include/ns_class.h rename to tests/cpp/sw-test/src/swtest/ft/include/ns_class.h diff --git a/tests/cpp/rpytest/ft/include/ns_hidden.h b/tests/cpp/sw-test/src/swtest/ft/include/ns_hidden.h similarity index 100% rename from tests/cpp/rpytest/ft/include/ns_hidden.h rename to tests/cpp/sw-test/src/swtest/ft/include/ns_hidden.h diff --git a/tests/cpp/rpytest/ft/include/operators.h b/tests/cpp/sw-test/src/swtest/ft/include/operators.h similarity index 100% rename from tests/cpp/rpytest/ft/include/operators.h rename to tests/cpp/sw-test/src/swtest/ft/include/operators.h diff --git a/tests/cpp/rpytest/ft/include/overloads.h b/tests/cpp/sw-test/src/swtest/ft/include/overloads.h similarity index 100% rename from tests/cpp/rpytest/ft/include/overloads.h rename to tests/cpp/sw-test/src/swtest/ft/include/overloads.h diff --git a/tests/cpp/rpytest/ft/include/parameters.h b/tests/cpp/sw-test/src/swtest/ft/include/parameters.h similarity index 100% rename from tests/cpp/rpytest/ft/include/parameters.h rename to tests/cpp/sw-test/src/swtest/ft/include/parameters.h diff --git a/tests/cpp/rpytest/ft/include/protection/pbase.h b/tests/cpp/sw-test/src/swtest/ft/include/protection/pbase.h similarity index 100% rename from tests/cpp/rpytest/ft/include/protection/pbase.h rename to tests/cpp/sw-test/src/swtest/ft/include/protection/pbase.h diff --git a/tests/cpp/rpytest/ft/include/protection/pchild.h b/tests/cpp/sw-test/src/swtest/ft/include/protection/pchild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/protection/pchild.h rename to tests/cpp/sw-test/src/swtest/ft/include/protection/pchild.h diff --git a/tests/cpp/rpytest/ft/include/protection/pgchild.h b/tests/cpp/sw-test/src/swtest/ft/include/protection/pgchild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/protection/pgchild.h rename to tests/cpp/sw-test/src/swtest/ft/include/protection/pgchild.h diff --git a/tests/cpp/rpytest/ft/include/refqual.h b/tests/cpp/sw-test/src/swtest/ft/include/refqual.h similarity index 100% rename from tests/cpp/rpytest/ft/include/refqual.h rename to tests/cpp/sw-test/src/swtest/ft/include/refqual.h diff --git a/tests/cpp/sw-test/src/swtest/ft/include/remote_trampoline.h b/tests/cpp/sw-test/src/swtest/ft/include/remote_trampoline.h new file mode 100644 index 00000000..0f3a33d9 --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/include/remote_trampoline.h @@ -0,0 +1,11 @@ +#pragma once + +#include "baseclass.h" + +class RemoteTrampoline : public abaseclass { +public: + + inline virtual std::string fn() { + return "RemoteTrampoline"; + } +}; \ No newline at end of file diff --git a/tests/cpp/rpytest/ft/include/rename.h b/tests/cpp/sw-test/src/swtest/ft/include/rename.h similarity index 100% rename from tests/cpp/rpytest/ft/include/rename.h rename to tests/cpp/sw-test/src/swtest/ft/include/rename.h diff --git a/tests/cpp/rpytest/ft/include/retval.h b/tests/cpp/sw-test/src/swtest/ft/include/retval.h similarity index 100% rename from tests/cpp/rpytest/ft/include/retval.h rename to tests/cpp/sw-test/src/swtest/ft/include/retval.h diff --git a/tests/cpp/rpytest/ft/include/static_only.h b/tests/cpp/sw-test/src/swtest/ft/include/static_only.h similarity index 100% rename from tests/cpp/rpytest/ft/include/static_only.h rename to tests/cpp/sw-test/src/swtest/ft/include/static_only.h diff --git a/tests/cpp/rpytest/ft/include/subpkg.h b/tests/cpp/sw-test/src/swtest/ft/include/subpkg.h similarity index 100% rename from tests/cpp/rpytest/ft/include/subpkg.h rename to tests/cpp/sw-test/src/swtest/ft/include/subpkg.h diff --git a/tests/cpp/rpytest/ft/include/templates/basic.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/basic.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/basic.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/basic.h diff --git a/tests/cpp/rpytest/ft/include/templates/dependent_base.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_base.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/dependent_base.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_base.h diff --git a/tests/cpp/rpytest/ft/include/templates/dependent_param.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_param.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/dependent_param.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_param.h diff --git a/tests/cpp/rpytest/ft/include/templates/dependent_using.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_using.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/dependent_using.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_using.h diff --git a/tests/cpp/rpytest/ft/include/templates/dependent_using2.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_using2.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/dependent_using2.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/dependent_using2.h diff --git a/tests/cpp/rpytest/ft/include/templates/fn.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/fn.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/fn.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/fn.h diff --git a/tests/cpp/rpytest/ft/include/templates/nested.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/nested.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/nested.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/nested.h diff --git a/tests/cpp/rpytest/ft/include/templates/numeric.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/numeric.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/numeric.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/numeric.h diff --git a/tests/cpp/rpytest/ft/include/templates/tbase.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tbase.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tbase.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tbase.h diff --git a/tests/cpp/rpytest/ft/include/templates/tconcrete.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tconcrete.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tconcrete.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tconcrete.h diff --git a/tests/cpp/rpytest/ft/include/templates/tcrtp.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tcrtp.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tcrtp.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tcrtp.h diff --git a/tests/cpp/rpytest/ft/include/templates/tcrtpfwd.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tcrtpfwd.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tcrtpfwd.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tcrtpfwd.h diff --git a/tests/cpp/rpytest/ft/include/templates/tvbase.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tvbase.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tvbase.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tvbase.h diff --git a/tests/cpp/rpytest/ft/include/templates/tvchild.h b/tests/cpp/sw-test/src/swtest/ft/include/templates/tvchild.h similarity index 100% rename from tests/cpp/rpytest/ft/include/templates/tvchild.h rename to tests/cpp/sw-test/src/swtest/ft/include/templates/tvchild.h diff --git a/tests/cpp/rpytest/ft/include/trampoline.h b/tests/cpp/sw-test/src/swtest/ft/include/trampoline.h similarity index 100% rename from tests/cpp/rpytest/ft/include/trampoline.h rename to tests/cpp/sw-test/src/swtest/ft/include/trampoline.h diff --git a/tests/cpp/rpytest/ft/include/type_caster.h b/tests/cpp/sw-test/src/swtest/ft/include/type_caster.h similarity index 100% rename from tests/cpp/rpytest/ft/include/type_caster.h rename to tests/cpp/sw-test/src/swtest/ft/include/type_caster.h diff --git a/tests/cpp/rpytest/ft/include/type_caster_nested.h b/tests/cpp/sw-test/src/swtest/ft/include/type_caster_nested.h similarity index 100% rename from tests/cpp/rpytest/ft/include/type_caster_nested.h rename to tests/cpp/sw-test/src/swtest/ft/include/type_caster_nested.h diff --git a/tests/cpp/rpytest/ft/include/using.h b/tests/cpp/sw-test/src/swtest/ft/include/using.h similarity index 100% rename from tests/cpp/rpytest/ft/include/using.h rename to tests/cpp/sw-test/src/swtest/ft/include/using.h diff --git a/tests/cpp/rpytest/ft/include/using2.h b/tests/cpp/sw-test/src/swtest/ft/include/using2.h similarity index 100% rename from tests/cpp/rpytest/ft/include/using2.h rename to tests/cpp/sw-test/src/swtest/ft/include/using2.h diff --git a/tests/cpp/rpytest/ft/include/using_companion.h b/tests/cpp/sw-test/src/swtest/ft/include/using_companion.h similarity index 100% rename from tests/cpp/rpytest/ft/include/using_companion.h rename to tests/cpp/sw-test/src/swtest/ft/include/using_companion.h diff --git a/tests/cpp/rpytest/ft/include/virtual_comma.h b/tests/cpp/sw-test/src/swtest/ft/include/virtual_comma.h similarity index 100% rename from tests/cpp/rpytest/ft/include/virtual_comma.h rename to tests/cpp/sw-test/src/swtest/ft/include/virtual_comma.h diff --git a/tests/cpp/rpytest/ft/include/virtual_xform.h b/tests/cpp/sw-test/src/swtest/ft/include/virtual_xform.h similarity index 100% rename from tests/cpp/rpytest/ft/include/virtual_xform.h rename to tests/cpp/sw-test/src/swtest/ft/include/virtual_xform.h diff --git a/tests/cpp/rpytest/ft/src/fields.cpp b/tests/cpp/sw-test/src/swtest/ft/src/fields.cpp similarity index 100% rename from tests/cpp/rpytest/ft/src/fields.cpp rename to tests/cpp/sw-test/src/swtest/ft/src/fields.cpp diff --git a/tests/cpp/sw-test/src/swtest/ft/src/ft.cpp b/tests/cpp/sw-test/src/swtest/ft/src/ft.cpp new file mode 100644 index 00000000..8b5761ad --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/src/ft.cpp @@ -0,0 +1,7 @@ + +#include + +SEMIWRAP_PYBIND11_MODULE(m) +{ + initWrapper(m); +} \ No newline at end of file diff --git a/tests/cpp/rpytest/ft/src/using2.cpp b/tests/cpp/sw-test/src/swtest/ft/src/using2.cpp similarity index 100% rename from tests/cpp/rpytest/ft/src/using2.cpp rename to tests/cpp/sw-test/src/swtest/ft/src/using2.cpp diff --git a/tests/cpp/sw-test/src/swtest/ft/subpkg.py b/tests/cpp/sw-test/src/swtest/ft/subpkg.py new file mode 100644 index 00000000..52308958 --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/subpkg.py @@ -0,0 +1,4 @@ +# autogenerated by 'robotpy-build create-imports rpytest.ft swtest.ft._ft.subpkg' +from ._ft.subpkg import SPClass, sp_func + +__all__ = ["SPClass", "sp_func"] diff --git a/tests/cpp/rpytest/tc/include/rpyint.h b/tests/cpp/sw-test/src/swtest/tc/include/rpyint.h similarity index 100% rename from tests/cpp/rpytest/tc/include/rpyint.h rename to tests/cpp/sw-test/src/swtest/tc/include/rpyint.h diff --git a/tests/cpp/rpytest/tc/include/rpyint_type_caster.h b/tests/cpp/sw-test/src/swtest/tc/include/rpyint_type_caster.h similarity index 97% rename from tests/cpp/rpytest/tc/include/rpyint_type_caster.h rename to tests/cpp/sw-test/src/swtest/tc/include/rpyint_type_caster.h index 0e1cad72..f279739a 100644 --- a/tests/cpp/rpytest/tc/include/rpyint_type_caster.h +++ b/tests/cpp/sw-test/src/swtest/tc/include/rpyint_type_caster.h @@ -17,7 +17,7 @@ namespace pybind11 { namespace detail { * then you need to add a type alias or pybind11-stubgen gets * angry at you -- so this has to be a fully qualified name */ - PYBIND11_TYPE_CASTER(rpy::rpyint, const_name("rpytest.ft.rpyint")); + PYBIND11_TYPE_CASTER(rpy::rpyint, const_name("swtest.ft.rpyint")); /** * Conversion part 1 (Python->C++): convert a PyObject into a inty diff --git a/tests/requirements.txt b/tests/requirements.txt index 05d02b92..39bb1361 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,3 +1,5 @@ build pytest -wheel \ No newline at end of file +hatchling +hatch-meson +ninja \ No newline at end of file diff --git a/tests/test_caster_module.py b/tests/test_caster_module.py new file mode 100644 index 00000000..cf4429cf --- /dev/null +++ b/tests/test_caster_module.py @@ -0,0 +1,10 @@ +from swtest_base._module import add_to_inty +from sw_caster_consumer._module import add_more_to_inty + + +def test_add_to_inty(): + assert add_to_inty(1, 2) == 3 + + +def test_add_more_to_inty(): + assert add_more_to_inty(1, 2) == 3 diff --git a/tests/test_downloaded.py b/tests/test_downloaded.py deleted file mode 100644 index 8de83f3d..00000000 --- a/tests/test_downloaded.py +++ /dev/null @@ -1,10 +0,0 @@ -# just ensures the module built correctly, nothing fancy -import rpytest.dl - - -def test_downloaded_fn(): - assert rpytest.dl.downloaded_fn(3) == 0x42 + 3 - - -def test_extra_content(): - assert rpytest.dl.extra_content() == True diff --git a/tests/test_ft_defaults.py b/tests/test_ft_defaults.py index 177570de..1fbeee1a 100644 --- a/tests/test_ft_defaults.py +++ b/tests/test_ft_defaults.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft def test_defaults(): diff --git a/tests/test_ft_docs.py b/tests/test_ft_docs.py index 90a8ca52..669fa3e3 100644 --- a/tests/test_ft_docs.py +++ b/tests/test_ft_docs.py @@ -1,5 +1,5 @@ import inspect -from rpytest import ft +from swtest import ft def test_docstrings_enum(): @@ -29,7 +29,7 @@ def test_docstrings_cls(): def test_docstrings_meth(): assert inspect.getdoc(ft.DocClass.fn) == inspect.cleandoc( """ - fn(self: rpytest.ft._rpytest_ft.DocClass) -> None + fn(self: swtest.ft._ft.DocClass) -> None Function with docstring for good measure """ @@ -39,7 +39,7 @@ def test_docstrings_meth(): def test_docstrings_meth_kwd(): assert inspect.getdoc(ft.DocClass.fn2) == inspect.cleandoc( """ - fn2(self: rpytest.ft._rpytest_ft.DocClass, from_: typing.SupportsInt) -> None + fn2(self: swtest.ft._ft.DocClass, from_: typing.SupportsInt) -> None Function with parameter that's a python keyword @@ -51,7 +51,7 @@ def test_docstrings_meth_kwd(): def test_docstrings_meth_rename(): assert inspect.getdoc(ft.DocClass.fn3) == inspect.cleandoc( """ - fn3(self: rpytest.ft._rpytest_ft.DocClass, ohai: typing.SupportsInt) -> None + fn3(self: swtest.ft._ft.DocClass, ohai: typing.SupportsInt) -> None Function with renamed parameter @@ -104,7 +104,7 @@ def test_docstrings_append(): ) assert inspect.getdoc(ft.DocAppendClass.fn) == inspect.cleandoc( """ - fn(self: rpytest.ft._rpytest_ft.DocAppendClass) -> None + fn(self: swtest.ft._ft.DocAppendClass) -> None Function with docstring for good measure Useful extra information about this fn diff --git a/tests/test_ft_enums.py b/tests/test_ft_enums.py index 663f89ac..849f14a2 100644 --- a/tests/test_ft_enums.py +++ b/tests/test_ft_enums.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest @@ -12,8 +12,8 @@ def test_enums(): # Unnamed enums are hoisted as integers to their scope # - not supported yet for globals - # assert ft._rpytest_ft.UGEX == 7 - # assert ft._rpytest_ft.NSUGEX == 5 + # assert ft._ft.UGEX == 7 + # assert ft._ft.NSUGEX == 5 assert ft.EnumContainer.UEX == 4 # enum class are specific types diff --git a/tests/test_ft_fields.py b/tests/test_ft_fields.py index 1ba0cfd8..6ed5cf44 100644 --- a/tests/test_ft_fields.py +++ b/tests/test_ft_fields.py @@ -1,4 +1,4 @@ -from rpytest.ft import ClassWithFields, StructWithBitfields +from swtest.ft import ClassWithFields, StructWithBitfields def test_fields(): diff --git a/tests/test_ft_ignore.py b/tests/test_ft_ignore.py index bdcaf271..f88f75f7 100644 --- a/tests/test_ft_ignore.py +++ b/tests/test_ft_ignore.py @@ -1,14 +1,14 @@ -from rpytest import ft +from swtest import ft def test_ignore_fn(): - assert not hasattr(ft._rpytest_ft, "fnIgnore") + assert not hasattr(ft._ft, "fnIgnore") assert ft.fnIgnoredParam() == 3 def test_ignore_cls(): - assert not hasattr(ft._rpytest_ft, "IgnoredClass") - assert not hasattr(ft._rpytest_ft, "IgnoredClassWithEnum") + assert not hasattr(ft._ft, "IgnoredClass") + assert not hasattr(ft._ft, "IgnoredClassWithEnum") c = ft.ClassWithIgnored() @@ -25,7 +25,7 @@ def test_ignore_cls_enum(): def test_ignored_enums(): - assert not hasattr(ft._rpytest_ft, "IgnoredEnum") + assert not hasattr(ft._ft, "IgnoredEnum") assert not hasattr(ft.EnumWithIgnored, "Ignored") assert ft.EnumWithIgnored.NotIgnored == 1 @@ -37,19 +37,19 @@ def test_ignored_enums(): def test_ignored_by_default_fn(): - assert not hasattr(ft._rpytest_ft, "id_fnIgnore") - assert ft._rpytest_ft.id_fnEnable() == 2 + assert not hasattr(ft._ft, "id_fnIgnore") + assert ft._ft.id_fnEnable() == 2 def test_ignored_by_default_enum(): - assert not hasattr(ft._rpytest_ft, "id_IgnoredEnum") - assert ft._rpytest_ft.id_EnabledEnum.Param3 == 3 + assert not hasattr(ft._ft, "id_IgnoredEnum") + assert ft._ft.id_EnabledEnum.Param3 == 3 def test_ignored_by_default_class(): - assert not hasattr(ft._rpytest_ft, "id_IgnoreClass") - o = ft._rpytest_ft.id_EnabledClass() + assert not hasattr(ft._ft, "id_IgnoreClass") + o = ft._ft.id_EnabledClass() assert o.fn() == 3 assert o.fn_missing() == 4 - assert ft._rpytest_ft.id_EnabledClass.InnerEnum.Param6 == 6 - assert ft._rpytest_ft.id_EnabledClass.InnerEnumMissing.Param7 == 7 + assert ft._ft.id_EnabledClass.InnerEnum.Param6 == 6 + assert ft._ft.id_EnabledClass.InnerEnumMissing.Param7 == 7 diff --git a/tests/test_ft_inheritance.py b/tests/test_ft_inheritance.py index 37de6440..30a84cd5 100644 --- a/tests/test_ft_inheritance.py +++ b/tests/test_ft_inheritance.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest import inspect diff --git a/tests/test_ft_keepalive.py b/tests/test_ft_keepalive.py index 95bd3146..2a1e1833 100644 --- a/tests/test_ft_keepalive.py +++ b/tests/test_ft_keepalive.py @@ -1,11 +1,11 @@ import gc import sys -from rpytest import ft +from swtest import ft def test_ft_autokeepalive(): - # robotpy-build should keepalive the reference without being told in the YML + # semiwrap should keepalive the reference without being told in the YML p = ft.PatientRef() diff --git a/tests/test_ft_lifetime.py b/tests/test_ft_lifetime.py index 84b3d4cc..97d86ff7 100644 --- a/tests/test_ft_lifetime.py +++ b/tests/test_ft_lifetime.py @@ -1,4 +1,4 @@ -from rpytest.ft import LTTester, LTWithVirtual +from swtest.ft import LTTester, LTWithVirtual import gc diff --git a/tests/test_ft_misc.py b/tests/test_ft_misc.py index 5daf9f7d..9f60fd4c 100644 --- a/tests/test_ft_misc.py +++ b/tests/test_ft_misc.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest import re @@ -142,7 +142,7 @@ def test_cpp_code_with_constant(): def test_ns_class(): - assert ft._rpytest_ft.NSClass().getN() == 4 + assert ft._ft.NSClass().getN() == 4 # @@ -277,4 +277,4 @@ def __init__(self): def test_subpkg(): - from rpytest.ft.subpkg import SPClass + from swtest.ft.subpkg import SPClass diff --git a/tests/test_ft_overloaded.py b/tests/test_ft_overloaded.py index e36a2478..6874d872 100644 --- a/tests/test_ft_overloaded.py +++ b/tests/test_ft_overloaded.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft def test_overloaded(): diff --git a/tests/test_ft_overloads.py b/tests/test_ft_overloads.py index a2326377..20c870da 100644 --- a/tests/test_ft_overloads.py +++ b/tests/test_ft_overloads.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft def test_fn_overloads(): diff --git a/tests/test_ft_parameters.py b/tests/test_ft_parameters.py index aca76534..ba4e1b87 100644 --- a/tests/test_ft_parameters.py +++ b/tests/test_ft_parameters.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest diff --git a/tests/test_ft_protection.py b/tests/test_ft_protection.py index 95fcc426..4e933a21 100644 --- a/tests/test_ft_protection.py +++ b/tests/test_ft_protection.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest diff --git a/tests/test_ft_refquals.py b/tests/test_ft_refquals.py index 8356435e..89624376 100644 --- a/tests/test_ft_refquals.py +++ b/tests/test_ft_refquals.py @@ -1,4 +1,4 @@ -import rpytest.ft._rpytest_ft as ft +import swtest.ft._ft as ft def test_refquals_ref(): diff --git a/tests/test_ft_rename.py b/tests/test_ft_rename.py index 250b6305..fda87420 100644 --- a/tests/test_ft_rename.py +++ b/tests/test_ft_rename.py @@ -1,14 +1,14 @@ -from rpytest import ft +from swtest import ft def test_rename_fn(): - assert not hasattr(ft._rpytest_ft, "fnOriginal") + assert not hasattr(ft._ft, "fnOriginal") assert ft.fnRenamed() == 0x1 assert ft.fnRenamedParam(y=4) == 4 def test_rename_cls(): - assert not hasattr(ft._rpytest_ft, "OriginalClass") + assert not hasattr(ft._ft, "OriginalClass") c = ft.RenamedClass() @@ -36,7 +36,7 @@ def test_rename_cls(): def test_rename_enums(): - assert not hasattr(ft._rpytest_ft, "OriginalEnum") + assert not hasattr(ft._ft, "OriginalEnum") assert not hasattr(ft.RenamedEnum, "Original1") assert ft.RenamedEnum.Renamed1 == 1 diff --git a/tests/test_ft_templates.py b/tests/test_ft_templates.py index 9cc1af02..a00db1be 100644 --- a/tests/test_ft_templates.py +++ b/tests/test_ft_templates.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft def test_basic_template(): diff --git a/tests/test_ft_trampoline.py b/tests/test_ft_trampoline.py index 35fe0f49..6c222324 100644 --- a/tests/test_ft_trampoline.py +++ b/tests/test_ft_trampoline.py @@ -1,4 +1,5 @@ -from rpytest.ft import ClassWithTrampoline, ConstexprTrampoline +from swtest_base._module import abaseclass +from swtest.ft._ft import ClassWithTrampoline, ConstexprTrampoline, RemoteTrampoline def test_class_with_trampoline(): @@ -29,3 +30,13 @@ def fnWithMoveOnlyParam(self, i): def test_constexpr_trampoline(): ConstexprTrampoline() + + +def test_remote_trampoline(): + a = abaseclass() + assert a.fn() == "abaseclass" + + r = RemoteTrampoline() + assert r.fn() == "RemoteTrampoline" + + assert isinstance(r, abaseclass) diff --git a/tests/test_ft_typecaster.py b/tests/test_ft_typecaster.py index 6efd1b01..ed897778 100644 --- a/tests/test_ft_typecaster.py +++ b/tests/test_ft_typecaster.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft import pytest # diff --git a/tests/test_multiple_virtual_inheritance.py b/tests/test_multiple_virtual_inheritance.py index 9c73c6c8..666a2665 100644 --- a/tests/test_multiple_virtual_inheritance.py +++ b/tests/test_multiple_virtual_inheritance.py @@ -1,4 +1,4 @@ -from rpytest import ft +from swtest import ft def test_another_diamond_b(): diff --git a/tests/test_srconly.py b/tests/test_srconly.py deleted file mode 100644 index 7a61e164..00000000 --- a/tests/test_srconly.py +++ /dev/null @@ -1,6 +0,0 @@ -# just ensures the source-only module built correctly, nothing fancy -import rpytest.srconly - - -def test_srconly_fn(): - assert rpytest.srconly.srconly_fn(3) == 3 - 0x42 From ee0a97aab262fdd96d57020ada66bd166e91fe66 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 6 May 2025 01:37:05 -0400 Subject: [PATCH 09/11] Update CI --- .github/workflows/dist.yml | 61 ++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/.github/workflows/dist.yml b/.github/workflows/dist.yml index 03bcf51b..2ac4fb8d 100644 --- a/.github/workflows/dist.yml +++ b/.github/workflows/dist.yml @@ -50,7 +50,7 @@ jobs: with: submodules: true - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.8 - name: Sphinx @@ -67,7 +67,7 @@ jobs: submodules: recursive fetch-depth: 0 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.8 @@ -97,12 +97,12 @@ jobs: - '3.11' - '3.12' - '3.13' - architecture: [x86, x64] - exclude: - - os: macos-13 - architecture: x86 - - os: ubuntu-22.04 - architecture: x86 + architecture: [x64] + # exclude: + # - os: macos-13 + # architecture: x86 + # - os: ubuntu-22.04 + # architecture: x86 include: - os: macos-14 python_version: 3.9 @@ -116,7 +116,7 @@ jobs: with: submodules: true - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python_version }} architecture: ${{ matrix.architecture }} @@ -133,25 +133,32 @@ jobs: key: ${{ matrix.os }}-${{ matrix.architecture }}-${{ matrix.python_version }} variant: ccache + - name: Setup MSVC + uses: bus1/cabuild/action/msdevshell@e22aba57d6e74891d059d66501b6b5aed8123c4d # v1 + with: + architecture: x64 + if: runner.os == 'Windows' + + - name: Configure ccache for MSVC + shell: bash + # https://github.com/mesonbuild/meson/issues/10423 + run: | + echo "CC=ccache.exe cl" >> $GITHUB_ENV + echo "CXX=ccache.exe cl" >> $GITHUB_ENV + if: runner.os == 'Windows' + - name: Install shell: bash working-directory: dist run: python -m pip --disable-pip-version-check install *.whl - name: Install test dependencies - shell: bash working-directory: tests run: python -m pip --disable-pip-version-check install -r requirements.txt - name: Test wheel - shell: bash - env: - RPYBUILD_PARALLEL: 1 - RPYBUILD_STRIP_LIBPYTHON: 1 - RPYBUILD_CC_LAUNCHER: ccache working-directory: tests - run: | - python run_tests.py + run: python run_tests.py cross-build: runs-on: ubuntu-latest @@ -160,8 +167,8 @@ jobs: max-parallel: ${{ fromJSON(needs.setup_concurrency.outputs.max-parallel).v }} matrix: container: - - wpilib/roborio-cross-ubuntu:2024-22.04-py312 - - wpilib/raspbian-cross-ubuntu:bullseye-22.04-py312 + - wpilib/roborio-cross-ubuntu:2025-22.04-py313 + - wpilib/raspbian-cross-ubuntu:2025-bookworm-22.04-py313 container: image: "${{ matrix.container }}" @@ -183,33 +190,23 @@ jobs: name: dist path: dist - - name: Install setuptools - run: | - /build/venv/bin/build-pip --disable-pip-version-check install setuptools - /build/venv/bin/cross-pip --disable-pip-version-check install setuptools + - name: Setup cross environment + uses: robotpy/build-actions/setup-cross-meson@semiwrap - name: Install working-directory: dist run: | - # install to both cross and build in case things get mixed up /build/venv/bin/build-pip --disable-pip-version-check install *.whl - /build/venv/bin/cross-pip --disable-pip-version-check install *.whl - name: Install test dependencies shell: bash working-directory: tests run: | - # install to both cross and build in case things get mixed up /build/venv/bin/build-pip --disable-pip-version-check install -r requirements.txt - /build/venv/bin/cross-pip --disable-pip-version-check install -r requirements.txt - name: Build cross wheel - env: - RPYBUILD_PARALLEL: 1 - RPYBUILD_STRIP_LIBPYTHON: 1 - RPYBUILD_CC_LAUNCHER: ccache working-directory: tests/cpp - run: /build/venv/bin/cross-python run_install.py wheel + run: /build/venv/bin/cross-python run_install.py --config-settings=setup-args=--cross-file=cross.txt publish: runs-on: ubuntu-latest From 17ee328f2ef4acd7e3ba13b795062b9a12090dff Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Wed, 14 May 2025 00:27:14 -0400 Subject: [PATCH 10/11] Add support for conditionally processing headers --- pyproject.toml | 1 + src/semiwrap/config/pyproject_toml.py | 28 +++++++++++++++++-- src/semiwrap/pyproject.py | 23 +++++++++++++++ src/semiwrap/tool/scan_headers.py | 10 +++++-- tests/cpp/sw-test/pyproject.toml | 4 +++ .../sw-test/semiwrap/ft/cond_always_true.yml | 4 +++ .../src/swtest/ft/include/cond_always_true.h | 3 ++ .../src/swtest/ft/include/cond_never_true.h | 1 + 8 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/cpp/sw-test/semiwrap/ft/cond_always_true.yml create mode 100644 tests/cpp/sw-test/src/swtest/ft/include/cond_always_true.h create mode 100644 tests/cpp/sw-test/src/swtest/ft/include/cond_never_true.h diff --git a/pyproject.toml b/pyproject.toml index 230902b9..26a29271 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "sphinxify >= 0.7.3", "validobj ~= 1.2", "cxxheaderparser[pcpp] ~= 1.5", + "packaging", "tomli", "tomli_w", "toposort", diff --git a/src/semiwrap/config/pyproject_toml.py b/src/semiwrap/config/pyproject_toml.py index a68452b2..8d343706 100644 --- a/src/semiwrap/config/pyproject_toml.py +++ b/src/semiwrap/config/pyproject_toml.py @@ -4,7 +4,7 @@ import dataclasses import re -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union _arch_re = re.compile(r"\{\{\s*ARCH\s*\}\}") _os_re = re.compile(r"\{\{\s*OS\s*\}\}") @@ -60,6 +60,28 @@ class TypeCasterConfig: headers: List[TypeCasterHeader] = dataclasses.field(default_factory=list) +@dataclasses.dataclass +class ConditionalHeader: + """ + Allows specifying that a header will only be autogenerated if the specified + condition is true. + + .. code-block:: toml + + [tool.semiwrap.extension_modules."PACKAGE.NAME".headers] + Name = { header="header.h", enable_if = "platform_machine == 'aarch64'" } + + """ + + #: Name of the header file + header: str + + #: This is a PEP 508 environment marker specification. + #: + #: The header will not be parsed if this does not evaluate to true + enable_if: str + + @dataclasses.dataclass class ExtensionModuleConfig: """ @@ -123,7 +145,9 @@ class ExtensionModuleConfig: #: #: .. seealso:: :ref:`autowrap` #: - headers: Dict[str, str] = dataclasses.field(default_factory=dict) + headers: Dict[str, Union[str, ConditionalHeader]] = dataclasses.field( + default_factory=dict + ) #: Path to a directory of yaml files. Generation data will be looked up #: using the key in the headers dictionary. diff --git a/src/semiwrap/pyproject.py b/src/semiwrap/pyproject.py index 77f76964..6281ceb1 100644 --- a/src/semiwrap/pyproject.py +++ b/src/semiwrap/pyproject.py @@ -1,6 +1,7 @@ import pathlib import typing as T +import packaging.markers import tomli from .config.util import parse_input @@ -21,6 +22,19 @@ def __init__(self, pyproject_path: T.Optional[pathlib.Path] = None) -> None: self._all_deps = None + self._evaluated_markers: T.Dict[str, bool] = {} + + def _enable_if(self, condition: str) -> bool: + """ + Evaluates a string containing PEP 508 environment markers + """ + ok = self._evaluated_markers.get(condition) + if ok is None: + ok = packaging.markers.Marker(condition).evaluate() + self._evaluated_markers[condition] = ok + + return ok + @property def package_root(self) -> pathlib.Path: if self._package_root is None: @@ -71,3 +85,12 @@ def get_extension_deps(self, extension: ExtensionModuleConfig) -> T.List[str]: deps.append(wrap) deps.extend(extension.depends) return deps + + def get_extension_headers( + self, extension: ExtensionModuleConfig + ) -> T.Generator[T.Tuple[str, str], None, None]: + for yml, hdr in extension.headers.items(): + if isinstance(hdr, str): + yield yml, hdr + elif self._enable_if(hdr.enable_if): + yield yml, hdr.header diff --git a/src/semiwrap/tool/scan_headers.py b/src/semiwrap/tool/scan_headers.py index 71167ba7..547d63a8 100644 --- a/src/semiwrap/tool/scan_headers.py +++ b/src/semiwrap/tool/scan_headers.py @@ -80,9 +80,15 @@ def _should_ignore(f): all_present.add(inc / h.header) for name, ext in project.extension_modules.items(): - if not ext.headers: + files = [] + for _, f in ext.headers.items(): + if isinstance(f, str): + files.append(Path(f)) + else: + files.append(Path(f.header)) + + if not files: continue - files = [Path(f) for f in ext.headers.values()] for incdir in search_paths[name]: incdir = Path(incdir) for f in files: diff --git a/tests/cpp/sw-test/pyproject.toml b/tests/cpp/sw-test/pyproject.toml index 1f2d403f..83b3c676 100644 --- a/tests/cpp/sw-test/pyproject.toml +++ b/tests/cpp/sw-test/pyproject.toml @@ -65,6 +65,10 @@ using2 = "using2.h" virtual_comma = "virtual_comma.h" virtual_xform = "virtual_xform.h" +# conditional headers +cond_always_true = { header="cond_always_true.h", enable_if="python_version > '2'"} +cond_never_true = { header="cond_never_true.h", enable_if="python_version < '2'"} + # Inheritance IBase = "inheritance/ibase.h" IChild = "inheritance/ichild.h" diff --git a/tests/cpp/sw-test/semiwrap/ft/cond_always_true.yml b/tests/cpp/sw-test/semiwrap/ft/cond_always_true.yml new file mode 100644 index 00000000..ee9abf54 --- /dev/null +++ b/tests/cpp/sw-test/semiwrap/ft/cond_always_true.yml @@ -0,0 +1,4 @@ +--- + +functions: + always_compiled: diff --git a/tests/cpp/sw-test/src/swtest/ft/include/cond_always_true.h b/tests/cpp/sw-test/src/swtest/ft/include/cond_always_true.h new file mode 100644 index 00000000..fec3f4f3 --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/include/cond_always_true.h @@ -0,0 +1,3 @@ +#pragma once + +inline int always_compiled() { return 1; } diff --git a/tests/cpp/sw-test/src/swtest/ft/include/cond_never_true.h b/tests/cpp/sw-test/src/swtest/ft/include/cond_never_true.h new file mode 100644 index 00000000..0a94cd15 --- /dev/null +++ b/tests/cpp/sw-test/src/swtest/ft/include/cond_never_true.h @@ -0,0 +1 @@ +#error "Should never be compiled" From e11c7a3ef4acd1a9de24e240a2777d3b608e58e8 Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Thu, 15 May 2025 11:47:23 -0400 Subject: [PATCH 11/11] Update documentation --- LICENSE.txt | 2 +- README.md | 19 +++--- docs/autowrap.rst | 19 +++--- docs/conf.py | 2 +- docs/config/autowrap.rst | 56 ++++++++-------- docs/config/pyproject.rst | 101 ++++------------------------ docs/demo.rst | 135 +++++++++++++++++++++++--------------- docs/deps.rst | 62 ----------------- docs/index.rst | 5 +- docs/install.rst | 47 ++++++++----- docs/intro.rst | 66 ++++++++++++------- docs/platform.rst | 27 -------- docs/support.rst | 6 +- docs/tips.rst | 68 ++++--------------- docs/tools.rst | 69 +++++++++++-------- docs/troubleshooting.rst | 18 ++--- docs/type_casters.rst | 27 +++++--- 17 files changed, 302 insertions(+), 427 deletions(-) delete mode 100644 docs/deps.rst diff --git a/LICENSE.txt b/LICENSE.txt index 4d2b8db1..119591a3 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2020 Dustin Spicuzza , All rights reserved. +Copyright (c) 2020-2025 Dustin Spicuzza , All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/README.md b/README.md index d44537ae..e0ee7405 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ semiwrap ======== -This is a build tool intended to be generally useful for any python project -that has binary dependencies. It is especially designed to meet the needs -of RobotPy's various wrapper libraries, chiefly around: +semiwrap is a build tool that makes it simpler to wrap C/C++ libraries with +pybind11 by automating large portions of the wrapping process and handling some +of the more complex aspects of creating pybind11 based wrappers (especially with +trampolines to allow inheriting from C++ classes from Python). -* Managing upstream binary dependencies -* Autogenerating pybind11 wrappers around those dependencies -* Building wheels from those generated wrappers +semiwrap includes a hatchling plugin that autogenerates `meson.build` files that +can be built using meson, and those build files parse your wrapped headers and +generate/compile pybind11 based wrappers into python extension modules. Requires Python 3.8+ @@ -21,8 +22,8 @@ Author Dustin Spicuzza is the primary author of semiwrap. -Semiwrap is a direct decendant of the robotpy-build project, and is -culmination of many years of experimentation with automated wrapper -generation by members of the RobotPy community. +Semiwrap is a direct decendant of the robotpy-build project, and is culmination +of many years of experimentation with automated wrapper generation by members of +the RobotPy community. semiwrap is available under the BSD 3-clause license. diff --git a/docs/autowrap.rst b/docs/autowrap.rst index 352f2cd7..9f583cc5 100644 --- a/docs/autowrap.rst +++ b/docs/autowrap.rst @@ -4,7 +4,7 @@ Automated C++ header wrapping ============================= -robotpy-build can be told to parse C/C++ headers and automatically generate +semiwrap can be told to parse C/C++ headers and automatically generate :std:doc:`pybind11 ` wrappers around the functions and objects found in that header. @@ -15,9 +15,9 @@ and objects found in that header. C++ Features ------------ -robotpy-build uses a pure python C++ parser and macro processor to attempt to +semiwrap uses a pure python C++ parser and macro processor to attempt to parse header files. As a result, a full AST of the header files is not created. -This means particularly opaque code might confuse the parser, as robotpy-build +This means particularly opaque code might confuse the parser, as semiwrap only receives the names, not the actual type information. However, most basic features typically work without needing to coerce the @@ -34,6 +34,7 @@ generator into working correctly, including: * final classes/methods - cannot be overridden from Python code * Enumerations * Global variables +* Many many more weird edge cases too Additionally, the following features are supported, but require some manual intervention: @@ -46,22 +47,22 @@ to your package in ``pyproject.toml``: .. code-block:: toml - [tool.robotpy-build.wrappers."MYPACKAGE".autogen_headers] + [tool.semiwrap.extension_modules."PACKAGE.NAME".headers] demo = "demo.h" That causes ``demo.h`` to be parsed and wrapped. .. note:: If you're importing a large number of headers, the - ``robotpy-build scan-headers`` tool can generate this for you + ``semiwrap scan-headers`` tool can generate this list for you automatically. Documentation ------------- -robotpy-build will find doxygen documentation comments on many types of elements -and use sphinxify to translate them into python docstrings. All elements that -support documentation strings can have their docstrings set explicitly using -a ``doc`` value in the YAML file. +semiwrap will find doxygen documentation comments on many types of elements +and use sphinxify to translate them into python docstrings. If this is not +sufficient, all elements that support documentation strings can have their +docstrings set explicitly using a ``doc`` value in the YAML file. .. code-block:: yaml diff --git a/docs/conf.py b/docs/conf.py index 2c8c507e..e0163ed6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # -- Project information ----------------------------------------------------- project = "semiwrap" -copyright = "2020, RobotPy Development Team" +copyright = "2025, RobotPy Development Team" author = "RobotPy Development Team" # The full version, including alpha/beta/rc tags diff --git a/docs/config/autowrap.rst b/docs/config/autowrap.rst index 64e5f2c3..84407788 100644 --- a/docs/config/autowrap.rst +++ b/docs/config/autowrap.rst @@ -4,7 +4,7 @@ Generator Customization ======================= -Because robotpy-build's code generation is intended to be a semi-automated +Because semiwrap's code generation is intended to be a semi-automated process (except for simple C++ code), a rich set of per-{class/function/parameter} configuration options can be specified in per-file YAML configurations. @@ -12,9 +12,9 @@ Additionally, some headers are too complex for the autogenerator to completely process, so when this occurs you must manually specify required information in the YAML file. -Most files generated by robotpy-build are customizable. +Most files generated by semiwrap are customizable. -.. note:: robotpy-build is designed for the RobotPy project and may contain +.. note:: semiwrap is designed for the RobotPy project and may contain defaults that aren't appropriate for all projects. If you find that you need more customization, file an issue on github and let's talk about it! @@ -22,48 +22,50 @@ Most files generated by robotpy-build are customizable. Location of customization file ------------------------------ -In your ``pyproject.toml``, you can specify either a single YAML file with -customizations, or you can specify a directory that robotpy-build will search -for YAML files. - -Single file: - -.. code-block:: toml - - [tool.robotpy-build.wrappers."PACKAGENAME"] - generation_data = "gen/data.yml" - -Multiple files: +By default customization files are located in the ``semiwrap`` directory. The name +of the YAML file is the ``key`` in the extension module headers table: .. code-block:: toml - [tool.robotpy-build.wrappers."PACKAGENAME"] - generation_data = "gen" - -When a directory is specified, pybind11 will search for YAML files in the -directory based on the header filename. In the above example, customization -data for ``header.h`` could be specified in ``gen/header.yml``. + [tool.semiwrap.extension_modules."PACKAGE.NAME".headers] + # yaml file is `semiwrap/demo.yml` + demo = "include/demo.h" Autogeneration -------------- -The default values for these YAML files can be generated via the robotpy-build -command line tool: +The default values for these YAML files should be generated via the semiwrap command +line tool: .. code-block:: sh - robotpy-build create-gen --write + semiwrap create-yaml --write This can be a good way to get the boilerplate out of the way when you need to provide customizations. +Manual pybind11 APIs +-------------------- + +You can write your own custom .cpp files and add them to the python extensions +generated by semiwrap. In your ``meson.build`` after you include the generated +``semiwrap`` subdir, add files to ``NAME_sources``. + +.. code-block:: meson + + subdir('semiwrap') + + my_module_sources += files( + 'src/my_module/main.cpp', + ) + + Reference --------- The following strctures describe the dictionaries that are read from the YAML -file. The toplevel structure is :class:`.AutowrapConfigYaml`. +files. The toplevel structure is :class:`.AutowrapConfigYaml`. -.. automodule:: robotpy_build.config.autowrap_yml +.. automodule:: semiwrap.config.autowrap_yml :members: - :undoc-members: \ No newline at end of file diff --git a/docs/config/pyproject.rst b/docs/config/pyproject.rst index e13e5618..038749ad 100644 --- a/docs/config/pyproject.rst +++ b/docs/config/pyproject.rst @@ -1,40 +1,26 @@ .. _pyproject: -setup.py and pyproject.toml -=========================== - -setup.py --------- - -Projects that use robotpy-build must use the setup function provided by -robotpy-build. Your project's setup.py should look like this: - -.. code-block:: py - - #!/usr/bin/env python3 - from robotpy_build.setup import setup - setup() - pyproject.toml --------------- +============== -Projects that use robotpy-build must add a ``pyproject.toml`` to the root of +Projects that use semiwrap must add a ``pyproject.toml`` to the root of their project as specified in `PEP 518 `_. -It is recommended that projects include the standard ``build-system`` section to -tell pip to install robotpy-build (and any other dependencies) before starting -a build. +Because semiwrap is a hatchling plugin, you should add semiwrap and hatchling +to your ``build-system.requires``. You need meson to build the project, so we +recommend also using ``hatch-meson`` to do so. .. code-block:: toml [build-system] - requires = ["robotpy-build>=2020.1.0,<2021.0.0"] + build-backend = "hatchling.build" + requires = ["semiwrap", "hatch-meson", "hatchling"] -Projects must include robotpy-build specific sections in their pyproject.toml. -robotpy-build takes ``pyproject.toml`` and converts it to a python dictionary -using ``toml.load``. The resulting dictionary is given to pydantic, which -validates the structure of the dictionary against the objects described below. +Projects must include semiwrap specific sections in their pyproject.toml. +semiwrap takes ``pyproject.toml`` and converts it to a python dictionary +using ``toml.load``. The resulting dictionary is converted to the dataclasses +described below. Required sections: @@ -49,73 +35,12 @@ Optional sections: * :class:`.PatchInfo` - patch downloaded sources -.. note:: For a complete example pyproject.toml file, see ``tests/cpp/pyproject.toml.tmpl`` - -.. _pyproject_overrides: - -Overrides ---------- - -You can define 'override' sections that will be grafted onto the configuration -if they match a particular platform. For example, to change the dependencies -for a wrapper section on Windows: - -.. code-block:: toml - - [tool.robotpy-build.wrappers."PACKAGENAME".override.os_windows] - depends = ["windows-thing"] - -Any element in the robotpy-build section of pyproject.toml can be overridden -by specifying the identical section as '.override.KEYNAME'. If the key matches -the current configuration, the override will be written to the original section. -The matched keys are generated at runtime. Current supported platform override -keys are: - -* ``arch_{platform_arch}`` -* ``os_{platform_os}`` -* ``platform_{platform_os}_{platform_arch}`` - -To get information about the current platform, you can run: - -.. code-block:: sh - - robotpy-build platform-info - -To show the available platforms: - -.. code-block:: sh - - robotpy-build platform-info --list - -To process a pyproject.toml and see the result of applying various overrides, -you can use this tool to process for the current platform: - -.. code-block:: sh - - robotpy-build show-override - -To show what would be processed for a different platform: - -.. code-block:: sh - - robotpy-build show-override -p linux-athena - -.. _platforms: - -Current supported platform/os/arch combinations are: - -* OS: windows/osx/linux -* Arch: x86/x86-64/armv7l/aarch64 - -For ARM linux distributions we support: +.. note:: For a complete example pyproject.toml file, see ``tests/cpp/*/pyproject.toml`` -* armv7l + nilrt (RoboRIO) -* armv7l + raspbian (Raspbian 10) -* aarch64 + bionic (Ubuntu 18.04) Reference --------- -.. automodule:: robotpy_build.config.pyproject_toml +.. automodule:: semiwrap.config.pyproject_toml :members: :exclude-members: __init__, Config diff --git a/docs/demo.rst b/docs/demo.rst index 2198b4e2..966debc7 100644 --- a/docs/demo.rst +++ b/docs/demo.rst @@ -3,42 +3,35 @@ Demo Project ============ This walkthrough will take you through all the steps to create a simple -robotpy-build project that autogenerates a working wrapper around a C++ +semiwrap project that autogenerates a working wrapper around a C++ class and it's methods. This demo should work on Linux, OSX, and Windows. Make sure you have -robotpy_build installed first! +``semiwrap`` installed first! .. note:: This demo shows building a python wrapper around C++ code that is - self-contained in this project. However, ``robotpy-build`` also + self-contained in this project. However, ``semiwrap`` also supports wrapping externally compiled libraries and inter-package - shared library dependencies. + shared library dependencies available through ``pypi-pkgconf`` Files + descriptions -------------------- .. note:: If you're lazy, the files for this demo are checked into the - robotpy-build repository at ``examples/demo``. + semiwrap repository at ``examples/demo``. All of the content required for this demo is contained inline below. Let's start by creating a new directory for your project, and we're going to create the following files: -setup.py -~~~~~~~~ - -Traditionally python projects required a setup.py that contained all the -information and logic needed to compile/install the project. Every project -that uses robotpy-build has an identical ``setup.py`` that looks like this: - -.. literalinclude:: ../examples/demo/setup.py - pyproject.toml ~~~~~~~~~~~~~~ -Projects that use robotpy-build must add a ``pyproject.toml`` to the root of +Projects that use semiwrap must add a ``pyproject.toml`` to the root of their project as specified in `PEP 518 `_. -This file is used to configure your project. +This file is used to configure your project. The semiwrap configuration is +in the ``tool.semiwrap`` table, the rest of the file are hatchling configuration +directives to load the semiwrap and hatch-meson plugins. Comments describing the function of each section can be found inline below. @@ -47,43 +40,52 @@ Comments describing the function of each section can be found inline below. .. seealso:: For detailed information about the contents of ``pyproject.toml`` see :ref:`pyproject`. -rpydemo/__init__.py -~~~~~~~~~~~~~~~~~~~ +meson.build +~~~~~~~~~~~ + +Semiwrap generates python modules that are built using the ``meson`` build system, +and you must provide your own ``meson.build`` that includes the build files that +semiwrap generates and any other build customizations required for your project. + +.. literalinclude:: ../examples/demo/meson.build + +swdemo/__init__.py +~~~~~~~~~~~~~~~~~~ .. code-block:: python - # file is empty for now + # file is empty for now -rpydemo/src/demo.cpp -~~~~~~~~~~~~~~~~~~~~ +swdemo/src/demo.cpp +~~~~~~~~~~~~~~~~~~~ This is the (very simple) C++ code that we will wrap so that it can be called from python. -.. literalinclude:: ../examples/demo/rpydemo/src/demo.cpp +.. literalinclude:: ../examples/demo/swdemo/src/demo.cpp -rpydemo/include/demo.h +swdemo/include/demo.h ~~~~~~~~~~~~~~~~~~~~~~ This is the C++ header file for the code that we're wrapping. In ``pyproject.toml`` -we told ``robotpy-build`` to parse this file and autogenerate wrappers for it. +we told ``semiwrap`` to parse this file and autogenerate wrappers for it. For simple C++ code such as this, autogeneration will 'just work' and no other customization is required. However, certain C++ code (templates and sometimes code that depends on templated types, and other complex circumstances) will require providing customization in a YAML file. -.. literalinclude:: ../examples/demo/rpydemo/include/demo.h +.. literalinclude:: ../examples/demo/swdemo/include/demo.h -rpydemo/src/main.cpp +swdemo/src/main.cpp ~~~~~~~~~~~~~~~~~~~~ Finally, you need to define your pybind11 python module. Custom ``pybind11`` projects would use a ``PYBIND11_MODULE`` macro to define a module, but it's -easier to use the ``RPYBUILD_PYBIND11_MODULE`` macro which automatically sets -the module name when robotpy-build compiles the file. +easier to use the ``SEMIWRAP_PYBIND11_MODULE`` macro which automatically sets +the module name when semiwrap compiles the file. -.. literalinclude:: ../examples/demo/rpydemo/src/main.cpp +.. literalinclude:: ../examples/demo/swdemo/src/main.cpp .. note:: If you wanted to add your own handwritten pybind11 code here, you can add it in addition to the ``initWrapper`` call made here. See @@ -97,48 +99,73 @@ which will build/install everything in the currect directory. .. code-block:: sh - $ python3 setup.py develop + $ python3 -m pip install -v -e . + +If you've been following our instructions so far, this will fail with an error +similar to this: -.. note:: When using develop mode for robotpy-build projects, it is recommended - to invoke setup.py directly instead of using ``pip3 install -e`` +.. code-block:: text + + semiwrap.makeplan.PlanError: swdemo._demo failed + - caused by FileNotFoundError: semiwrap/demo.yml: use `python3 -m semiwrap create-yaml --write` to generate + +``semiwrap`` requires all headers listed in ``tool.semiwrap.extension_modules."PACKAGE.NAME".headers`` +to have an associated YAML file in the semiwrap directory. You can create them manually, +or just use the following command to autogenerate it. + +.. code-block:: sh + + python3 -m semiwrap create-yaml --write + +Now there will be a semiwrap generation configuration YAML file at ``semiwrap/demo.yml``: + +.. literalinclude:: ../examples/demo/semiwrap/demo.yml + +Now if you run the install command again it should build and install the package: + +.. code-block:: sh + + $ python3 -m pip install -v -e . Adjust the project ------------------ As we've currently built the project, the CPython extension will be built -as ``rpydemo._rpydemo``. For example: +as ``swdemo._swdemo``. For example: .. code-block:: pycon - >>> from rpydemo._rpydemo import DemoClass - >>> DemoClass - + >>> from swdemo._demo import DemoClass + >>> DemoClass + While that works, we really would like users to be able to access our module -directly by importing them into ``__init__.py``. There's a robotpy_build command -for generating the contents of ``__init__.py``: +directly by importing them into ``__init__.py``. The ``semiwrap update-init`` +command can automatically add this to specified packages. Ensure that your +``pyproject.toml`` contains the package names in ``update_init``: -.. code-block:: sh +.. code-block:: toml - $ python -m robotpy_build create-imports rpydemo rpydemo._rpydemo + [tool.semiwrap] + update_init = ["swdemo swdemo._demo"] -The output from this you can put into your ``rpydemo/__init__.py``. It'll look -like this: +Then run this command: -.. code-block:: python +.. code-block:: sh + + $ python -m semiwrap update-init - # autogenerated by 'robotpy-build create-imports rpydemo rpydemo._rpydemo' - from ._rpydemo import DemoClass, add2 +This will add the following to your ``__init__.py``: - __all__ = ["DemoClass", "add2"] +.. literalinclude:: ../examples/demo/swdemo/__init__.py Now when we put this in our ``__init__.py``, that allows this to work instead: .. code-block:: pycon - >>> from rpydemo import DemoClass - >>> DemoClass - + >>> from swdemo import DemoClass + >>> DemoClass + Trying out the project ---------------------- @@ -147,10 +174,10 @@ Alright, now that all the pieces are assembled, we can try out our project: .. code-block:: pycon - >>> import rpydemo - >>> rpydemo.add2(2) + >>> import swdemo + >>> swdemo.add2(2) 4 - >>> d = rpydemo.DemoClass() + >>> d = swdemo.DemoClass() >>> d.setX(2) >>> d.getX() 2 @@ -158,5 +185,5 @@ Alright, now that all the pieces are assembled, we can try out our project: More Examples ------------- -The integration tests in ``tests/cpp`` contains a python project that contains -several autogenerated wrappers packages and various customizations. +The integration tests in ``tests/cpp`` contains a several projects that contains +autogenerated wrappers packages and various customizations. diff --git a/docs/deps.rst b/docs/deps.rst deleted file mode 100644 index 68d56ba0..00000000 --- a/docs/deps.rst +++ /dev/null @@ -1,62 +0,0 @@ -External dependencies -===================== - -Download files --------------- - -Used to download files from an arbitrary URL. This can download headers, -shared/static libraries, and sources. - -.. code-block:: toml - - [[tool.robotpy-build.wrappers."PACKAGENAME".download]] - url = "https://my/url/something.zip" - incdir = "include" - libs = ["mylib"] - libdir = "libpath" - -That tells robotpy-build: - -* To download the zipfile at that URL -* Anything in the "include" directory will be extracted to the include path - for the wrapper -* Libraries will be searched in the "libpath" directory (default is "") -* The shared library "mylib" will be linked to the python module being built - -.. note:: When ``RPYBUILD_DEBUG`` isn't specified, downloaded shared libraries - are stripped on Linux. - -Maven artifacts ---------------- - -Used to download artifacts from a maven repository. This can download headers, -shared libraries, and sources. - -.. code-block:: toml - - [tool.robotpy-build.wrappers."PACKAGENAME".maven_lib_download] - artifact_id = "mything" - group_id = "com.example.thing" - repo_url = "http://example.com/maven" - version = "1.2.3" - -When robotpy-build downloads an artifact from a maven repo, it will unpack it -into a temporary directory and add the relevant directories to your package's -include/library paths. Additionally, any headers will automatically be copied -into your wheel so that other packages can utilize them when linking to your -wheel. - -For development purposes, you can download/extract the files locally by -running ``python3 setup.py build_dl``. - -.. seealso:: Reference for all :class:`.MavenLibDownload` options - -.. note:: It's possible that the archive format is specific to FIRST Robotics. - If you're trying to use this and it isn't working, file a bug on - github! - - For FIRST Robotics related projects, the metadata for - ``maven_lib_download`` can be found in a vendor JSON file. - -.. note:: When ``RPYBUILD_DEBUG`` isn't specified, downloaded shared libraries - are stripped on Linux. diff --git a/docs/index.rst b/docs/index.rst index 236a4bba..6941969a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,8 +3,8 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -robotpy-build: (mostly) automated C++ wrapping for Python -========================================================= +semiwrap: (mostly) automated C++ wrapping for Python +==================================================== .. toctree:: :maxdepth: 2 @@ -19,7 +19,6 @@ robotpy-build: (mostly) automated C++ wrapping for Python :caption: User's Guide autowrap - deps type_casters platform tips diff --git a/docs/install.rst b/docs/install.rst index b81daac2..8f65eb32 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -1,47 +1,58 @@ Installation ============ -.. note:: ``robotpy-build`` is a build dependency, not a runtime dependency -- +This describes how to install the ``semiwrap`` development tool if you +need to use the tooling to develop packages that depend on ``semiwrap``. + +.. note:: ``semiwrap`` is a build dependency, not a runtime dependency -- in otherwords, you only need it to build binary wheels. Once you have - a wheel, the wheel is usable without ``robotpy-build`` + a wheel, the wheel is usable without ``semiwrap`` + + When building a wheel that requires ``semiwrap``, you do not need to + install semiwrap separately unless you have build isolation disabled + for your build tool. Just make sure that ``semiwrap`` is in the + ``build-requires`` section of ``pyproject.toml``. Supported platforms ------------------- -``robotpy-build`` requires Python 3.6 to be installed. Our integration tests -are ran on Ubuntu 18.04, OSX, and Windows. - -* Linux and Windows are expected to work in almost any case -* OSX should work, but when depending on shared libraries there are some cases - where relinking doesn't work - -To compile the generated code created by ``robotpy-build``, you must have a -modern C++ compiler that is supported by pybind11. +``semiwrap`` requires a minimum of Python 3.8 to be installed, and should +work on Linux, macOS, and Windows. To compile the generated code created by +``semiwrap``, you must have a modern C++ compiler that is supported by pybind11. Install ------- -``robotpy-build`` is distributed on pypi and is installable via pip. +To run the semiwrap development tooling, you should install ``semiwrap`` from +pypi using your preferred python installer program. On Linux/OSX, you can install with the following command: .. code:: sh - $ pip3 install robotpy-build + $ pip3 install semiwrap On Windows, you can install via: .. code:: sh - py -3 -m pip install robotpy-build + py -3 -m pip install semiwrap + +Once ``semiwrap`` is installed you can run the tools via one of the following: + +.. code:: sh + + semiwrap + + python -m semiwrap pybind11 -------- -pybind11 is a header-only C++ library that robotpy-build leverages to easily -bind together Python and C++ code. All robotpy-build projects are built using +pybind11 is a header-only C++ library that semiwrap leverages to easily +bind together Python and C++ code. All semiwrap projects are built using a bundled version of pybind11, which is typically a bleeding edge version of pybind11 with custom patches that have not been accepted upstream yet. -.. warning:: robotpy-build does not use the pybind11 package distributed on - pypi. +.. warning:: semiwrap does not currently use the pybind11 package distributed + on pypi. diff --git a/docs/intro.rst b/docs/intro.rst index 8bcca693..801e4c5a 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -6,34 +6,50 @@ libraries to allow its users to use Python for the FIRST Robotics Competition. It was much too work to create/maintain handwritten wrappers around these libraries, and other tools in the python ecosystem did not meet our needs. -robotpy-build was created to automate large parts of the work required -to maintain these wrappers. If you have this problem too, we hope that -robotpy-build will be useful for you as well. +robotpy-build was created in 2020 to automate large portions of the work required +to maintain and build these wrappers. semiwrap is a rewrite of that project, +incorporating five years of lessons learned while maintaining over a dozen +different projects wrapping hundreds of C++ classes and functions. + +We hope that this tool will be useful for you as well. Goals ----- -A primary goal of robotpy-build is to make it really simple to define and build -python wrappers for C++ code using pybind11. To make that possible, it also -bundles these capabilities: +A primary goal of semiwrap is to make it really simple to define and build +python wrappers for C++ code using pybind11, requiring little boilerplate. -* Manages native/binary C/C++ dependencies via pypi compatible packages -* Autogenerate python wrappers around the native code by parsing C++ header files, - including those that contain modern C++ features +* Autogenerate python extension module wrappers around the native code by + parsing C++ header files, including those that contain modern C++ features * Support extensive customization of the wrappers when the autogeneration fails -* Builds normal python wheels from the generated and handwritten code that can - be installed by pip and imported as a normal python package -* robotpy-build projects can depend on native/binary libraries installed by - other robotpy-build projects - -robotpy-build is intended to be a generally useful tool for any python project -that has C/C++ dependencies. If you find that isn't the case, please report a -bug on github. - -Non-goals ---------- - -* robotpy-build built wheels do not currently conform to the manylinux - specification as it prohibits dependencies outside of the wheel -* We don't intend to build wrappers around libraries installed on your - system +* Autogenerate build files for the autogenerated code +* semiwrap projects can depend on (and wrap) native/binary libraries in other + python wheels +* Provide tooling to make it easier to update projects as their dependencies + evolve + +Technologies +------------ + +Unlike robotpy-build, semiwrap does not actually build your code. Instead, +semiwrap ships with a hatchling plugin that autogenerates ``meson.build`` files that +will autogenerate pybind11 wrappers and build python extension modules from them. +You should use meson to actually build your code (we recommend the ``hatch-meson`` +hatchling plugin, which is maintained by the authors of semiwrap). + +To support finding dependencies in other wheels, semiwrap uses the pypi-pkgconf +project to resolve pkg-config packages, and exports pkg-config files that can +be resolved by other projects. + +.. note:: Currently, semiwrap only ships with a build file generator for meson. + In theory the underlying generator is abstracted such that it should + be simple to generate build files for other build systems (CMake, Bazel, etc). + If this is something you're interested in please reach out. + +Related projects +---------------- + +* `hatch-nativelib `_ - a hatchling plugin + that can generate and export pkg-config files and python module loaders for libraries +* `hatch-robotpy `_ - a hatchling plugin + that can download prebuilt binaries and ship them in a wheel diff --git a/docs/platform.rst b/docs/platform.rst index fd03978e..b340fef8 100644 --- a/docs/platform.rst +++ b/docs/platform.rst @@ -8,30 +8,3 @@ We have successfully used `crossenv `_ to cross-compile python modules. For an example set of dockerfiles used to compile for the RoboRIO platform, see the `robotpy-cross-docker `_ project. - -macOS ------ - -macOS handles library files differently than Windows or Linux. In particular, -macOS will not automatically find and link libraries at runtime. Rather, -libraries need to be explicitly linked beforehand. On Linux/Windows, if your -dependency is not specified explcitly, but you import the dependency before -importing your package, it often works without complaining. **This is not the -case on OSX**. - -This presents a special challenge when repackaging libraries to be shipped -in wheels, particularly when linking to libraries installed by other wheels. -robotpy-build provides automatic relinking support for libraries that are -installed via other robotpy-build created wheels, provided that you specify -your dependencies explcitly. - -External libraries required must always be defined by ``depends``, even if -they are indirect dependencies. - -.. code:: toml - - [tool.robotpy-build.wrappers."PACKAGENAME"] - depends = ["ntcore"] - -If you forget an external dependency, the library won't load and a linking -error will occur at runtime when your package is imported. diff --git a/docs/support.rst b/docs/support.rst index 3cc3456d..1fdf98ce 100644 --- a/docs/support.rst +++ b/docs/support.rst @@ -1,10 +1,10 @@ Support ======= -If you run into a problem with robotpy-build that you think is a bug, or +If you run into a problem with semiwrap that you think is a bug, or perhaps there is something wrong with the documentation or just too difficult to do, please feel free to file bug reports on the `github issue tracker -`_. +`_. The developers can also be reached at the `RobotPy Gitter channel -`_. \ No newline at end of file +`_. diff --git a/docs/tips.rst b/docs/tips.rst index 911b9dcf..9e30a0fc 100644 --- a/docs/tips.rst +++ b/docs/tips.rst @@ -2,55 +2,25 @@ Developer tips ============== Over time, we've collected a number of tips and best practices for working -with robotpy-build projects which will save you lots of time -- particularly +with semiwrap projects which will save you lots of time -- particularly when dealing with very large projects. -Build using develop mode ------------------------- +Use editable installation +------------------------- -For rapid iteration development, we highly recommend using ``python3 setup.py -develop`` to build your project. This allows in-tree editing of python files -and leads to faster results if you're disciplined. - -Use parallel builds -------------------- - -While setuptools does not support parallel builds, robotpy-build will compile -things in parallel if you define the environment variable ``RPYBUILD_PARALLEL=1``. - -.. code-block:: sh - - $ RPYBUILD_PARALLEL=1 python3 setup.py develop - -Partial code generation ------------------------ - -When developing wrappers of very large projects, the wrapper regeneration step -can take a very long time. Often you find that you only want to modify a single -file. You can define a YAML file and tell robotpy-build to only regenerate the -output for that single file, instead of scanning all files and regenerating -everything. - -Create a filter.yml: - -.. code-block:: yaml - - # the names in this list correspond to the keys in your 'generate' section - only_generate: - - SendableChooser - -Then run your build like so: - -.. code-block:: bash - - $ RPYBUILD_GEN_FILTER=filter.yml python setup.py develop +It is highly recommended to use editable installations when developing your +semiwrap based projects. This allows in-tree editing of python files and can +allow you to take advantage of meson's incremental compilation when making +small modifications. +See the `pypa documentation `_ +for more information. Use ccache ---------- -If you create a large robotpy-build based project, you'll notice that your -build times can be *really* long. This occurs because robotpy-build uses +If you create a large semiwrap based project, you'll notice that your +build times can be *really* long. This occurs because semiwrap uses pybind11 for connecting C++ and Python code, and pybind11 relies heaviliy on C++ templates and other modern C++ techniques which cause build times to take a really long time. @@ -62,17 +32,5 @@ From ccache's website: being done again. This can dramatically improve your compile times if you're just changing -a small portions of your project. - -Once you install ccache, define these environment variables: - -.. code-block:: bash - - export RPYBUILD_PARALLEL=1 - export RPYBUILD_CC_LAUNCHER=ccache - export GCC_COLORS=1 - -The first one will make robotpy-build compile in parallel. The second tells -robotpy-build to use ccache, and the third makes error output nice when using -ccache. - +a small portions of your project. meson will automatically use ccache if +it is installed. diff --git a/docs/tools.rst b/docs/tools.rst index 883d4456..41ee4bd6 100644 --- a/docs/tools.rst +++ b/docs/tools.rst @@ -1,46 +1,58 @@ Tools ===== -robotpy-build has some useful command line tools that can be executed via -``python3 -m robotpy_build TOOLNAME``. On OSX/Linux, it is also installed -as the ``robotpy-build`` script which can be executed directly. +semiwrap has some useful command line tools that can be executed via +``python3 -m semiwrap TOOLNAME``. On OSX/Linux, it is also installed +as the ``semiwrap`` script which can be executed directly. + +.. _build_dep: + +build-dep +--------- + +Grabs the contents of ``build-system.requires`` from ``pyproject.toml`` and +installs them via pip. .. _scan_headers: scan-headers ------------ -This tool is designed to make it easy to populate the ``generate`` key for -your python package. - -This will scan all of your defined includes directories (including those of -downloaded artifacts) and output something you can paste into the ``generate`` -key of ``pyproject.toml``. By default it will only show files that are not -present in ``pyproject.toml`` -- to show all files use the ``--all`` argument. +This will scan all directories in the search path for your extension modules +and output something you can paste into ``[tool.semiwrap.extension_modules."PACKAGE.NAME".headers]``. +By default it will only show files that are not present in ``pyproject.toml`` +which makes it useful to determine if your dependencies have added any files. +To show all files use the ``--all`` argument. Often there are files that you don't want to wrap. You can add them to the -``pyproject.toml`` file and they will be ignored. The list accepts glob patterns -supported by the fnmatch module. +``pyproject.toml`` file in ``scan_headers_ignore`` and they will be ignored. +The list accepts glob patterns supported by the fnmatch module. .. code-block:: toml - [tool.robotpy-build] + [tool.semiwrap] scan_headers_ignore = [ "ignored_header.h", "ignore_dir/*", ] -.. _create_gen: +To output the scanned headers to a list of ignored files, you can use the +``--as-ignore`` option. + +.. _create_yaml: -create-gen ----------- +create-yaml +----------- -This will parse your defined generate items (``generate`` key of ``pyproject.toml``) -and output yaml for them in the directory defined by ``generation_data``. By default -it will just print the file contents to stdout. +Once you have added headers to your semiwrap configuration in ``pyproject.toml``, +the ``create-yaml`` tool can be used to create an initial YAML file that describes +the header file. Additionally, if a YAML file is already present, it will print +out elements that are in the header but not in the YAML file. -Use the ``--write`` argument to write the files, but it won't overwrite existing -files. +By default it will just print the file contents to stdout. Use the ``--write`` argument +to write the files, but it won't overwrite existing files. + +.. _create_imports: create-imports -------------- @@ -52,16 +64,19 @@ python package. .. code-block:: sh - $ python -m robotpy_build create-imports rpydemo rpydemo._rpydemo + $ semiwrap create-imports rpydemo rpydemo._rpydemo Use the ``--write`` argument to write the file. -To write a list of ``__init__.py`` files, you can specify them in the ``pyproject.toml`` -file like so: +.. _update_init: + +update-init +----------- + +This does the same thing as create-imports, but it will automatically update +all modules that are specified in ``pyproject.toml``: .. code-block:: toml - [tool.robotpy-build] + [tool.semiwrap] update_init = ["rpydemo rpydemo._rpydemo"] - -To actually update the files, run ``python setup.py update_init``. \ No newline at end of file diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 4e24bd3c..ae028543 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -33,7 +33,7 @@ had the following C++ code: void fnWithEnum(SomeEnum e); -And ``SomeEnum`` was actually ``somens::SomeEnum`` but robotpy-build wasn't +And ``SomeEnum`` was actually ``somens::SomeEnum`` but semiwrap wasn't able to automatically determine that. You could fix it like so: .. code-block:: yaml @@ -53,11 +53,11 @@ pybind11 has a static assert that occurs when an trampoline class is specified for a class that is a subclass of some other class, but none of its bases have any virtual functions. This is to prevent -robotpy-build generates trampoline classes to allow python code to override -virtual functions in C++ classes. robotpy-build isn't always able to detect +semiwrap generates trampoline classes to allow python code to override +virtual functions in C++ classes. semiwrap isn't always able to detect when a trampoline isn't appropriate. -When this error occurs, you can force robotpy-build to turn off trampoline classes +When this error occurs, you can force semiwrap to turn off trampoline classes for a specific type: .. code-block:: yaml @@ -69,7 +69,7 @@ for a specific type: error: 'rpygen/__SomeClassName.hpp' file not found ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This occurs when robotpy-build has created a trampoline class for a child +This occurs when semiwrap has created a trampoline class for a child class, and it is trying to include the header for the parent trampoline class. There are several reasons why this might happen. @@ -77,7 +77,7 @@ Sometimes this error occurs because the parent lives in a different package that you didn't declare a dependency on in ``pyproject.toml``. Add the dependency and the parent trampoline class should be found. -If the base class is polymorphic in a way that robotpy-build wasn't able to +If the base class is polymorphic in a way that semiwrap wasn't able to detect, you can force it to be polymorphic: .. code-block:: yaml @@ -111,8 +111,8 @@ What this means is the argument 'arg' is not currently wrapped by pybind11 -- or if it is, it hasn't been imported by the current python package. * If it hasn't been wrapped, wrap it -* If it hasn't been imported, you can just add the other package to the wrapper's - ``depends`` list. +* If it hasn't been imported, import it in your ``__init__.py`` or you can just + add the other package to the wrapper's ``depends`` list. Runtime errors @@ -129,7 +129,7 @@ these contents: .. code-block:: c++ - #include + #include RPYBUILD_PYBIND11_MODULE(m) { initWrapper(m); diff --git a/docs/type_casters.rst b/docs/type_casters.rst index 146e174a..f1af680a 100644 --- a/docs/type_casters.rst +++ b/docs/type_casters.rst @@ -12,10 +12,11 @@ types (such as STL containers) are converted indirectly and require special Using type casters ------------------ -When parsing a header, robotpy-build will match the types found against its +When parsing a header, semiwrap will match the types found against its list of type casters. If found, it will automatically add an include directive -for the header that contains the type caster. robotpy-build contains support -for the STL type casters that come with pybind11 (vector, function, map, etc). +for the header that contains the type caster. semiwrap contains support +for the STL type casters that come with pybind11 (vector, function, map, etc) +and usually can autodetect and include the appropriate type casters. Sometimes the autodetection will fail because a type is hidden in some way. You can specify the missing types on a per-class basis. For example, if ``std::vector`` @@ -36,7 +37,7 @@ won't cover that here, refer to the :std:doc:`pybind11 documentation for custom instead. However, once you have a custom type caster, here's what you need to do to -automatically include it in a robotpy-build project. +automatically include it in a semiwrap project. 1. Create a header file and put the type caster in that header file. It should be in one of your project's python packages. @@ -45,12 +46,20 @@ automatically include it in a robotpy-build project. .. code-block:: toml - [[tool.robotpy-build.wrappers."package_name".type_casters]] - header = "header_name.h" - types = ["somens::SomeType"] + [tool.semiwrap.export_type_casters.NAME] + pypackage = "PACKAGE.NAME" + includedir = ["path/to/type_casters"] - This directive will cause robotpy-build's autogenerator to automatically - include ``header_name.h`` whenever it notices ``SomeType`` in a file. This + [[tool.semiwrap.export_type_casters.NAME.headers]] + header = "mytype.h" + types = ["MyType"] + + This will publish a ``NAME`` package via ``pkgconf``, and other ``semiwrap`` + projects (or your extension modules in your project) can consume it by + adding ``NAME`` to the list of dependencies. + + This directive will cause semiwrap's autogenerator to automatically + include ``mytype.h`` whenever it notices ``MyType`` in a file. This allows pybind11 to use it. The type specification is a list, so if there are multiple types covered in the same header, just specify all the types. However, most of the time it is preferred to put type casters for separate