diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac6b69f..83bb5dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main, master] + branches: [main] pull_request: - branches: [main, master] + branches: [main] release: types: [published] workflow_dispatch: @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Set up Python run: uv python install 3.12 @@ -33,7 +33,7 @@ jobs: - name: Build wheel run: uv build --wheel - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: wheel path: dist/*.whl @@ -44,16 +44,16 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Set up Python run: uv python install 3.12 - name: Download wheel - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: wheel path: dist @@ -66,6 +66,7 @@ jobs: build_wheels: name: Build wheels on ${{ matrix.os }} + if: github.event_name == 'release' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -73,12 +74,12 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Install dependencies (Ubuntu) if: runner.os == 'Linux' @@ -97,11 +98,11 @@ jobs: vcpkg install openssl:x64-windows zlib:x64-windows libxml2:x64-windows flatbuffers:x64-windows - name: Build wheels - uses: pypa/cibuildwheel@v2.22.0 + uses: pypa/cibuildwheel@v3.3.1 env: # Use uv for faster builds CIBW_BUILD_FRONTEND: "build[uv]" - # Build for Python 3.9-3.13 + # Build for Python 3.10-3.14 CIBW_BUILD: "cp310-* cp311-* cp312-* cp313-* cp314-*" # Skip 32-bit builds and musllinux CIBW_SKIP: "*-win32 *-manylinux_i686 *-musllinux_*" @@ -110,11 +111,10 @@ jobs: CIBW_MANYLINUX_AARCH64_IMAGE: manylinux_2_28 # Install build dependencies in the container (need GCC 13 for C++23, OpenSSL 3.x) CIBW_BEFORE_ALL_LINUX: > - dnf install -y gcc-toolset-13 perl-IPC-Cmd libxml2-devel zlib-devel && - curl -L https://www.openssl.org/source/openssl-3.0.13.tar.gz | tar xz && - cd openssl-3.0.13 && - CC=/opt/rh/gcc-toolset-13/root/usr/bin/gcc - CXX=/opt/rh/gcc-toolset-13/root/usr/bin/g++ + dnf install -y gcc-toolset-13 perl-IPC-Cmd perl-Time-Piece libxml2-devel zlib-devel && + source /opt/rh/gcc-toolset-13/enable && + curl -L https://www.openssl.org/source/openssl-3.0.19.tar.gz | tar xz && + cd openssl-3.0.19 && ./config --prefix=/usr/local/openssl3 --openssldir=/usr/local/openssl3 && make -j$(nproc) && make install_sw install_ssldirs @@ -130,8 +130,8 @@ jobs: uv pip install --system cmake swig && if ! command -v flatc; then cd /tmp && - curl -L https://github.com/google/flatbuffers/archive/refs/tags/v24.3.25.tar.gz | tar xz && - cd flatbuffers-24.3.25 && + curl -L https://github.com/google/flatbuffers/archive/refs/tags/v25.12.19.tar.gz | tar xz && + cd flatbuffers-25.12.19 && cmake -B build -DCMAKE_BUILD_TYPE=Release -DFLATBUFFERS_BUILD_TESTS=OFF && cmake --build build --target install; fi @@ -153,26 +153,27 @@ jobs: CIBW_TEST_REQUIRES: pytest CIBW_TEST_COMMAND: pytest {project}/tests -v - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: wheels-${{ matrix.os }} path: ./wheelhouse/*.whl build_sdist: name: Build source distribution + if: github.event_name == 'release' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: submodules: recursive - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v7 - name: Build sdist run: uv build --sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v6 with: name: sdist path: dist/*.tar.gz @@ -189,13 +190,13 @@ jobs: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: pattern: wheels-* path: dist merge-multiple: true - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: name: sdist path: dist diff --git a/.gitignore b/.gitignore index 15b7a40..13552d0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,11 @@ Thumbs.db # Test artifacts *.cdoc +*.asice +*.bdoc + +# Local notes +TODO.md # Lock files (library - not committed) uv.lock diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c60bcc0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,53 @@ +# pycdoc + +Python bindings for [libcdoc](https://github.com/open-eid/libcdoc) via SWIG. Produces installable wheels for reading and writing encrypted CDOC containers. + +## Architecture + +- `libcdoc/` — upstream C++ library as a git submodule +- `patches/libcdoc-python.patch` — adds Python typemaps, templates, and director support to upstream `libcdoc.i` +- `CMakeLists.txt` — applies patch, builds upstream statically, then builds SWIG Python module +- `swig/pycdoc.i` — thin wrapper: `%rename` for snake_case + `%include "libcdoc.i"` +- `src/pycdoc/__init__.py` — re-exports SWIG symbols, provides high-level `encrypt()` API + +## Key decisions + +- **Static linking** (`BUILD_SHARED_LIBS=OFF`) — self-contained wheels, LGPL-2.1 compliance via RELINKING.md + sdist source +- **Patch-based approach** — upstream libcdoc.i is modified via `patches/libcdoc-python.patch` at CMake configure time (rather than forking) +- **SWIG `%rename` before `%include`** — order matters, rename rules must appear before upstream interface is included +- **`MACOSX_DEPLOYMENT_TARGET=15.0`** — required by Homebrew OpenSSL 3 + +## Test + +```bash +uv sync --dev +uv run pytest tests/ -v +``` + +## Build + +```bash +# System deps (Ubuntu): libssl-dev libxml2-dev zlib1g-dev flatbuffers-compiler libflatbuffers-dev +uv build --wheel +``` + +Uses scikit-build-core as the build backend bridging CMake. Requires C++23, SWIG 4.0+, CMake 3.20+. + +## Git + +Never use `git -C ` — always run git commands from the working directory. + +## CI + +`.github/workflows/build.yml` — 5 jobs: +1. `build` — every push, Ubuntu only (quick feedback) +2. `test` — runs pytest against the built wheel +3. `build_wheels` — on release/PR, cibuildwheel v3.3.1 across Linux/macOS/Windows +4. `build_sdist` — on release/PR +5. `publish` — OIDC trusted publishing to PyPI + +Linux wheels build OpenSSL and flatbuffers from source inside manylinux_2_28 with GCC 13. + +## Version management + +`bump-my-version` configured in pyproject.toml. Updates version in pyproject.toml, `__init__.py`, and CMakeLists.txt, commits, and tags. diff --git a/RELINKING.md b/RELINKING.md index 8fe2dab..cf90589 100644 --- a/RELINKING.md +++ b/RELINKING.md @@ -5,7 +5,7 @@ Section 6, you have the right to modify libcdoc and relink. Here's how. ## Prerequisites -- Python 3.9+ +- Python 3.10+ - CMake 3.20+ - C++23 compiler (GCC 13+, Clang 16+, MSVC 2022+) - SWIG 4.0+ diff --git a/patches/libcdoc-python.patch b/patches/libcdoc-python.patch index bbd54e7..ba14892 100644 --- a/patches/libcdoc-python.patch +++ b/patches/libcdoc-python.patch @@ -110,7 +110,7 @@ index c681964..173770d 100644 +%feature("director") libcdoc::PKCS11Backend; +%feature("director") libcdoc::NetworkBackend; +%feature("director") libcdoc::Configuration; -+%feature("director") libcdoc::ILogger; ++%feature("director") libcdoc::Logger; +#endif + #ifdef SWIGJAVA diff --git a/pyproject.toml b/pyproject.toml index cc3d38b..4a87917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,11 +28,14 @@ classifiers = [ "Programming Language :: Python :: 3.14", "Programming Language :: C++", "Topic :: Security :: Cryptography", + "Topic :: Software Development :: Libraries :: Python Modules", ] [project.optional-dependencies] ldap = ["ldap3>=2.9"] -test = ["pytest>=7.0"] + +[dependency-groups] +dev = ["pytest>=7.0", "bump-my-version>=0.31", "ruff>=0.9"] [project.urls] Homepage = "https://github.com/namespace-ee/pycdoc" @@ -44,20 +47,36 @@ Issues = "https://github.com/namespace-ee/pycdoc/issues" [tool.scikit-build] cmake.version = ">=3.20" cmake.build-type = "Release" -cmake.args = [ - "-DBUILD_SHARED_LIBS=OFF", - "-DBUILD_TOOLS=OFF", - "-DFRAMEWORK=OFF", - "-DLIBCDOC_WITH_DOCS=OFF", -] wheel.install-dir = "pycdoc" wheel.packages = [] build-dir = "build/{wheel_tag}" # git submodules are excluded from sdists by default (git archive ignores them) sdist.include = ["libcdoc/**"] -[tool.scikit-build.cmake.define] -BUILD_SHARED_LIBS = "OFF" -BUILD_TOOLS = "OFF" -FRAMEWORK = "OFF" -LIBCDOC_WITH_DOCS = "OFF" +[tool.bumpversion] +current_version = "0.1.0" +commit = true +tag = true +tag_name = "v{new_version}" + +[[tool.bumpversion.files]] +filename = "pyproject.toml" +search = 'version = "{current_version}"' +replace = 'version = "{new_version}"' + +[[tool.bumpversion.files]] +filename = "src/pycdoc/__init__.py" +search = '__version__ = "{current_version}"' +replace = '__version__ = "{new_version}"' + +[[tool.bumpversion.files]] +filename = "CMakeLists.txt" +search = "VERSION {current_version}" +replace = "VERSION {new_version}" + +[tool.ruff] +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "SIM", "RUF"] +ignore = ["I001", "RUF022"] diff --git a/src/pycdoc/__init__.py b/src/pycdoc/__init__.py index d7c0392..a6d8a41 100644 --- a/src/pycdoc/__init__.py +++ b/src/pycdoc/__init__.py @@ -7,10 +7,6 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Union from pycdoc.libcdoc import ( # Version and utilities diff --git a/swig/pycdoc.i b/swig/pycdoc.i index 0985fef..ae24585 100644 --- a/swig/pycdoc.i +++ b/swig/pycdoc.i @@ -2,7 +2,7 @@ * pycdoc - Python-specific SWIG interface for libcdoc * * This wraps the upstream libcdoc.i and adds Python-specific - * template instantiations and director support. + * camelCase → snake_case renaming. */ /* Global camelCase → snake_case renaming for all functions and methods. @@ -10,18 +10,5 @@ %rename("%(undercase)s", %$isfunction, %$not %$isconstructor, %$not %$isdestructor) ""; %rename("%(undercase)s", %$ismember, %$not %$isenumitem, %$not %$isconstant, %$not %$isconstructor, %$not %$isdestructor, %$not %$isenum) ""; -/* Enable directors for Python subclassing of C++ classes */ -%feature("director") libcdoc::DataSource; -%feature("director") libcdoc::CryptoBackend; -%feature("director") libcdoc::PKCS11Backend; -%feature("director") libcdoc::NetworkBackend; -%feature("director") libcdoc::Configuration; -%feature("director") libcdoc::Logger; - /* Include the upstream libcdoc SWIG interface */ %include "libcdoc.i" - -/* Python-specific std::vector template instantiations */ -%template(ByteVector) std::vector; -%template(ByteVectorVector) std::vector>; -%template(StringVector) std::vector;