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
51 changes: 26 additions & 25 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -66,19 +66,20 @@ 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
matrix:
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'
Expand All @@ -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_*"
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ Thumbs.db

# Test artifacts
*.cdoc
*.asice
*.bdoc

# Local notes
TODO.md

# Lock files (library - not committed)
uv.lock
53 changes: 53 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <path>` — 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.
2 changes: 1 addition & 1 deletion RELINKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+
Expand Down
2 changes: 1 addition & 1 deletion patches/libcdoc-python.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 31 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
4 changes: 0 additions & 4 deletions src/pycdoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 1 addition & 14 deletions swig/pycdoc.i
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,13 @@
* 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.
Must appear BEFORE %include of upstream interface. */
%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<uint8_t>;
%template(ByteVectorVector) std::vector<std::vector<uint8_t>>;
%template(StringVector) std::vector<std::string>;
Loading