Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,16 @@ jobs:

- name: Generate new wrappers
run: |
cd examples
rm -rf shapes/wrapper/geometry \
shapes/wrapper/math_funcs \
shapes/wrapper/primitives
python shapes/wrapper/generate.py \
--source_root shapes/src/ \
--wrapper_root shapes/wrapper/ \
--castxml_binary $(which castxml) \
--package_info shapes/wrapper/package_info.yaml \
--includes shapes/src/
cd examples/shapes/wrapper
rm -rf geometry math_funcs primitives
cd ..
cppwg src/ \
--wrapper_root wrapper/ \
--package_info wrapper/package_info.yaml \
--include src/geometry/ \
--include src/math_funcs/ \
--include src/primitives/ \
--include src/python/

- name: Build Python module
run: |
Expand Down
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,19 @@ pip install .
To generate the full `pyshapes` wrapper, do:

```bash
python examples/shapes/wrapper/generate.py \
--source_root examples/shapes/src/ \
--wrapper_root examples/shapes/wrapper/ \
--castxml_binary /path/to/bin/castxml \
--package_info examples/shapes/wrapper/package_info.yaml \
--includes examples/shapes/src/
cd examples/shapes
cppwg src/ \
--wrapper_root wrapper/ \
--package_info wrapper/package_info.yaml \
--include src/geometry/ \
--include src/math_funcs/ \
--include src/primitives/ \
--include src/python/
```

where `/path/to/bin/castxml` is the path to your castxml installation.
If it is on the `PATH`, you can find it with `which castxml`.

To build the example package do:

```bash
cd examples/shapes
mkdir build
cd build
cmake ..
Expand Down
102 changes: 102 additions & 0 deletions cppwg/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Entry point for the cppwg package."""

import argparse
import logging

from cppwg import CppWrapperGenerator


def parse_args() -> argparse.Namespace:
"""
Parse command line arguments.

Returns
-------
argparse.Namespace: The parsed command line arguments.
"""
parser = argparse.ArgumentParser(
prog="cppwg",
description="Generate Python Wrappers for C++ code",
)

parser.add_argument(
"source_root",
metavar="SOURCE_ROOT",
type=str,
help="Path to the root directory of the input C++ source code.",
)

parser.add_argument(
"-w",
"--wrapper_root",
type=str,
help="Path to the output directory for the Pybind11 wrapper code.",
)

parser.add_argument(
"-p", "--package_info", type=str, help="Path to the package info file."
)

parser.add_argument(
"-c",
"--castxml_binary",
type=str,
help="Path to the castxml executable.",
)

parser.add_argument(
"-f",
"--castxml_cflags",
type=str,
help="Extra cflags for CastXML e.g. '-std=c++17'.",
)

parser.add_argument(
"-i",
"--include",
type=str,
action="append",
help="Paths to include directories.",
)

args = parser.parse_args()

return args


def generate(args: argparse.Namespace) -> None:
"""
Generate the Python wrappers.

Parameters
----------
args : argparse.Namespace
The parsed command line arguments.
"""
generator = CppWrapperGenerator(
source_root=args.source_root,
source_includes=args.include,
wrapper_root=args.wrapper_root,
package_info_path=args.package_info,
castxml_binary=args.castxml_binary,
castxml_cflags=args.castxml_cflags,
)

generator.generate_wrapper()


def main() -> None:
"""Generate wrappers from command line arguments."""
logging.basicConfig(
format="%(levelname)s %(message)s",
handlers=[logging.FileHandler("filename.log"), logging.StreamHandler()],
)
logger = logging.getLogger()
logger.setLevel(logging.INFO)

args = parse_args()
generate(args)


if __name__ == "__main__":
main()
118 changes: 71 additions & 47 deletions cppwg/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
import os
import re
import subprocess
import uuid
from pathlib import Path
from typing import List, Optional

import pygccxml.utils
from pygccxml import __version__ as pygccxml_version
from pygccxml.declarations.namespace import namespace_t

from cppwg.input.class_info import CppClassInfo
from cppwg.input.free_function_info import CppFreeFunctionInfo
Expand All @@ -18,7 +19,11 @@
from cppwg.parsers.package_info_parser import PackageInfoParser
from cppwg.parsers.source_parser import CppSourceParser
from cppwg.templates import pybind11_default as wrapper_templates
from cppwg.utils.constants import CPPWG_EXT, CPPWG_HEADER_COLLECTION_FILENAME
from cppwg.utils.constants import (
CPPWG_DEFAULT_WRAPPER_DIR,
CPPWG_EXT,
CPPWG_HEADER_COLLECTION_FILENAME,
)
from cppwg.writers.header_collection_writer import CppHeaderCollectionWriter
from cppwg.writers.module_writer import CppModuleWrapperWriter

Expand All @@ -36,14 +41,12 @@ class CppWrapperGenerator:
wrapper_root : str
The output directory for the wrapper code
castxml_binary : str
The path to the CastXML binary
The path to the castxml binary
castxml_cflags : str
Optional cflags to be passed to CastXML e.g. "-std=c++17"
Optional cflags to be passed to castxml e.g. "-std=c++17"
package_info_path : str
The path to the package info yaml config file; defaults to "package_info.yaml"
source_hpp_files : List[str]
The list of C++ source header files
source_ns : namespace_t
source_ns : pygccxml.declarations.namespace_t
The namespace containing C++ declarations parsed from the source tree
package_info : PackageInfo
A data structure containing the information parsed from package_info_path
Expand All @@ -54,16 +57,50 @@ def __init__(
source_root: str,
source_includes: Optional[List[str]] = None,
wrapper_root: Optional[str] = None,
castxml_binary: Optional[str] = "castxml",
castxml_binary: Optional[str] = None,
package_info_path: Optional[str] = None,
castxml_cflags: Optional[str] = "-std=c++17",
castxml_cflags: Optional[str] = None,
):
logging.basicConfig(
format="%(levelname)s %(message)s",
handlers=[logging.FileHandler("filename.log"), logging.StreamHandler()],
)
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Check that castxml_binary exists and is executable
self.castxml_binary: str = ""

if castxml_binary:
if os.path.isfile(castxml_binary) and os.access(castxml_binary, os.X_OK):
self.castxml_binary = castxml_binary
else:
logger.warning(
"Could not find specified castxml binary. Searching on path."
)

# Search for castxml_binary
if not self.castxml_binary:
path_to_castxml, _ = pygccxml.utils.find_xml_generator(name="castxml")

if path_to_castxml:
self.castxml_binary = path_to_castxml
logger.info(f"Found castxml binary: {self.castxml_binary}")
else:
logger.error("Could not find a castxml binary.")
raise FileNotFoundError()

# Check castxml and pygccxml versions
castxml_version: str = (
subprocess.check_output([self.castxml_binary, "--version"])
.decode("ascii")
.strip()
)
castxml_version = re.search(
r"castxml version \d+\.\d+\.\d+", castxml_version
).group(0)
logger.info(castxml_version)
logger.info(f"pygccxml version {pygccxml_version}")

# Sanitize castxml_cflags
self.castxml_cflags: str = ""
if castxml_cflags:
self.castxml_cflags = castxml_cflags

# Sanitize source_root
self.source_root: str = os.path.abspath(source_root)
Expand All @@ -72,21 +109,20 @@ def __init__(
raise FileNotFoundError()

# Sanitize wrapper_root
self.wrapper_root: str # type hinting
self.wrapper_root: str = ""

if wrapper_root:
# Create the specified wrapper root directory if it doesn't exist
self.wrapper_root = os.path.abspath(wrapper_root)

if not os.path.isdir(self.wrapper_root):
logger.info(
f"Could not find wrapper root directory - creating it at {self.wrapper_root}"
)
os.makedirs(self.wrapper_root)
else:
self.wrapper_root = self.source_root
logger.info(
"Wrapper root not specified - using source_root: {self.source_root}"
)
wrapper_dirname = CPPWG_DEFAULT_WRAPPER_DIR + "_" + uuid.uuid4().hex[:8]
self.wrapper_root = os.path.join(self.source_root, wrapper_dirname)
logger.info(f"Wrapper root not specified - using {self.wrapper_root}")

if not os.path.isdir(self.wrapper_root):
# Create the wrapper root directory if it doesn't exist
logger.info(f"Creating wrapper root directory: {self.wrapper_root}")
os.makedirs(self.wrapper_root)

# Sanitize source_includes
self.source_includes: List[str] # type hinting
Expand All @@ -108,13 +144,13 @@ def __init__(
self.package_info_path: Optional[str] = None
if package_info_path:
# If a package info config file is specified, check that it exists
self.package_info_path = package_info_path
self.package_info_path = os.path.abspath(package_info_path)
if not os.path.isfile(package_info_path):
logger.error(f"Could not find package info file: {package_info_path}")
raise FileNotFoundError()
else:
# If no package info config file has been supplied, check the default
default_package_info_file = os.path.abspath("./package_info.yaml")
default_package_info_file = os.path.join(os.getcwd(), "package_info.yaml")
if os.path.isfile(default_package_info_file):
self.package_info_path = default_package_info_file
logger.info(
Expand All @@ -123,25 +159,8 @@ def __init__(
else:
logger.warning("No package info file found - using default settings.")

# Check castxml and pygccxml versions
self.castxml_binary: str = castxml_binary
castxml_version: str = (
subprocess.check_output([self.castxml_binary, "--version"])
.decode("ascii")
.strip()
)
castxml_version = re.search(
r"castxml version \d+\.\d+\.\d+", castxml_version
).group(0)
logger.info(castxml_version)
logger.info(f"pygccxml version {pygccxml_version}")

self.castxml_cflags: str = castxml_cflags

# Initialize remaining attributes
self.source_hpp_files: List[str] = []

self.source_ns: Optional[namespace_t] = None
self.source_ns: Optional[pygccxml.declarations.namespace_t] = None

self.package_info: Optional[PackageInfo] = None

Expand Down Expand Up @@ -173,6 +192,11 @@ def collect_source_hpp_files(self) -> None:

self.package_info.source_hpp_files.append(filepath)

# Check if any source files were found
if not self.package_info.source_hpp_files:
logging.error(f"No header files found in source root: {self.source_root}")
raise FileNotFoundError()

def extract_templates_from_source(self) -> None:
"""Extract template arguments for each class from the associated source file."""
for module_info in self.package_info.module_info_collection:
Expand Down Expand Up @@ -200,7 +224,7 @@ def parse_header_collection(self) -> None:
"""
Parse the hpp files to collect C++ declarations.

Parse the headers with pygccxml and CastXML to populate the source
Parse the headers with pygccxml and castxml to populate the source
namespace with C++ declarations collected from the source tree.
"""
source_parser = CppSourceParser(
Expand Down Expand Up @@ -324,7 +348,7 @@ def generate_wrapper(self) -> None:
# Write the header collection to file
self.write_header_collection()

# Parse the headers with pygccxml and CastXML
# Parse the headers with pygccxml and castxml
self.parse_header_collection()

# Update the Class Info from the parsed code
Expand Down
2 changes: 2 additions & 0 deletions cppwg/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@

CPPWG_TRUE_STRINGS = ["ON", "YES", "Y", "TRUE", "T"]
CPPWG_FALSE_STRINGS = ["OFF", "NO", "N", "FALSE", "F"]

CPPWG_DEFAULT_WRAPPER_DIR = "cppwg_wrappers"
Loading