# The importable module name; the RPM is python-lib3mf, the module is lib3mf. %global import_name lib3mf Name: python-lib3mf Version: 2.2.0 Release: 1%{?dist} Summary: Python bindings for lib3mf, the 3D Manufacturing Format library # Verified from the tarball's own LICENSE, not from PyPI metadata (which says # only "Simplified BSD"): exactly two numbered conditions and no "Neither the # name of the copyright holder" clause, so BSD-2-Clause rather than BSD-3. License: BSD-2-Clause URL: https://github.com/3MFConsortium/lib3mf Source0: %{url}/archive/refs/tags/v%{version}/lib3mf-%{version}.tar.gz # Upstream ships the binding as a bare autogenerated file with no packaging # metadata of any kind, so supply a minimal one. Source1: python-lib3mf-pyproject.toml # The package initialiser. See the comments in it for why Fedora does not # reproduce upstream's bundled-library path computation. Source2: python-lib3mf-init.py BuildArch: noarch BuildRequires: python3-devel BuildRequires: pyproject-rpm-macros # %%check constructs a Wrapper(), which dlopens the real shared library. BuildRequires: lib3mf %global _description %{expand: lib3mf is the 3MF Consortium's reference implementation of the 3D Manufacturing Format, the 3D printing interchange format that carries meshes together with colour, materials, beam lattices and slice data in a single package. This is the Python binding: a ctypes wrapper over the lib3mf C API, so Python programs can read, write and validate 3MF files. It is used by InVesalius to import and export 3MF surfaces alongside its STL, PLY and OBJ support. The binding is autogenerated by the Automatic Component Toolkit from the same interface description as the C++ API, so it tracks the library version exactly -- which is why it is built here from the matching upstream source rather than taken from PyPI, whose wheels bundle their own copy of the native library.} %description %_description %package -n python3-%{import_name} Summary: %{summary} # The wrapper reaches the library through ctypes.CDLL, which rpm cannot see, so # the dependency has to be declared by hand. No %%{?_isa}: this is noarch, and # the library is found by SONAME through the dynamic linker rather than by a # path baked in at build time. Requires: lib3mf %description -n python3-%{import_name} %_description %prep %autosetup -n lib3mf-%{version} # Upstream's repository has no Python package layout at all -- just the one # generated module. The PyPI distribution wraps it as lib3mf/{__init__,Lib3MF}.py # plus a BUNDLED lib3mf.so. Reproduce that layout, minus the bundled library. mkdir -p %{import_name} cp -p Autogenerated/Bindings/Python/Lib3MF.py %{import_name}/Lib3MF.py cp -p %{SOURCE2} %{import_name}/__init__.py cp -p %{SOURCE1} pyproject.toml # Load Fedora's shared library instead of a bundled copy. # # Wrapper.__init__ defaults libraryName to a bundled file next to the module and # then unconditionally appends the platform suffix: # # path = libraryName + '.' + ending # always ".so" # # so it can only ever open an UNVERSIONED filename. Fedora's lib3mf package # ships lib3mf.so.2 and lib3mf.so.2.2.0.0; the bare lib3mf.so symlink lives in # lib3mf-devel, which must not be a runtime dependency. Point the default at the # SONAME and leave an already-suffixed path alone. Both edits assert on their # target so an upstream rewrite fails the build here rather than at the user's # first 3MF export. python3 - %{import_name}/Lib3MF.py <<'PYFIX' import sys path = sys.argv[1] with open(path, encoding="utf-8") as fh: src = fh.read() old_default = "\t\t\tlibraryName = os.path.join(os.path.dirname(os.path.realpath(__file__)),'lib3mf')" new_default = "\t\t\tlibraryName = 'lib3mf.so.2'" assert src.count(old_default) == 1, ( "bundled-library default not found exactly once -- upstream changed Wrapper.__init__" ) src = src.replace(old_default, new_default) old_concat = "\t\t\tpath = libraryName + '.' + ending" new_concat = ( "\t\t\tpath = libraryName if ('.so.' in libraryName or " "libraryName.endswith('.' + ending)) else libraryName + '.' + ending" ) assert src.count(old_concat) == 1, ( "suffix concatenation not found exactly once -- upstream changed Wrapper.__init__" ) src = src.replace(old_concat, new_concat) # The 2.2.0 generator emitted a Python reserved word as an enum member: # # class BeamLatticeBallMode(CTypesEnum): # None = 0 # # so the module does not even byte-compile -- "SyntaxError: cannot assign to # None" -- which fails brp-python-bytecompile long before anything imports it. # Upstream fixed this in v2.3.2 by renaming the member to # BeamLatticeBallModeNone (same in v2.5.0); use that exact name so Fedora # matches the upstream fix instead of inventing a third spelling. old_enum = "\tNone = 0\n" new_enum = "\tBeamLatticeBallModeNone = 0\n" assert src.count(old_enum) == 1, ( "reserved-word enum member not found exactly once -- upstream may have fixed it" ) src = src.replace(old_enum, new_enum) with open(path, "w", encoding="utf-8") as fh: fh.write(src) print("Lib3MF.py: loads the system lib3mf.so.2 by SONAME; nothing bundled") print("Lib3MF.py: BeamLatticeBallMode.None -> BeamLatticeBallModeNone (upstream's v2.3.2 name)") PYFIX %generate_buildrequires %pyproject_buildrequires %build %pyproject_wheel %install %pyproject_install %pyproject_save_files %{import_name} %check %pyproject_check_import # Importing the module never dlopens anything -- only Wrapper() does. Exercise # the real library so a SONAME, ABI or function-table mistake fails the build # instead of surfacing on the user's first 3MF export. PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} - <<'PYCHK' import lib3mf wrapper = lib3mf.Wrapper() version = wrapper.GetLibraryVersion() print("lib3mf reports library version: %s" % (version,)) # Mirror the binding's own _checkBinaryVersion rule -- major must match exactly # and the library's minor must be >= the binding's -- rather than pinning to # exactly 2.2, which would fail the build on a compatible Fedora lib3mf bump. assert version[0] == 2 and version[1] >= 2, ( "loaded lib3mf %s, which cannot serve a binding generated for the 2.2.x API" % (version,) ) model = wrapper.CreateModel() model.SetUnit(lib3mf.ModelUnit.MilliMeter) print("Wrapper/CreateModel/SetUnit against the system library: OK") PYCHK %files -n python3-%{import_name} -f %{pyproject_files} %license LICENSE %doc README.md %changelog * Sat Sep 26 2026 Morgan Hough - 2.2.0-1 - Initial package. Python bindings for lib3mf, needed by InVesalius for 3MF import/export; nothing else in the repo requires them yet. - Built from the upstream v2.2.0 tag to match Fedora's lib3mf 2.2.0. The generated binding is ABI-locked to the library it was generated from, so the tag has to track the Fedora package rather than the newest upstream release. - NOT built from PyPI. The lib3mf wheels are binary-only (no sdist) and bundle a 5.3 MB copy of the native library; this builds the same importable package from source against Fedora's shared library. - InVesalius pins lib3mf==2.3.2, but every call it makes -- Wrapper, ModelUnit, Position, Triangle, Color, SetUnit, AddMeshObject, CreateModel, SetGeometry, AddBuildItem -- is present in 2.2.0, so the Fedora library is sufficient. - Carries upstream's interpreter-shutdown workaround for Base.__del__ reaching a torn-down ErrorCodes. Upstream added it for Python 3.13; Fedora is on 3.14. - Fixes a generator bug that stops the 2.2.0 binding byte-compiling at all: BeamLatticeBallMode has a member named "None", a Python reserved word, so the file raises "SyntaxError: cannot assign to None". Renamed to BeamLatticeBallModeNone, the name upstream itself adopted in v2.3.2. Worth reporting against the 2.2.x series. Nothing in InVesalius uses BeamLattice. - Deliberately NOT switching to the v2.3.2 binding to dodge that bug: Wrapper ends in _checkBinaryVersion(), which raises INCOMPATIBLEBINARYVERSION unless the library's minor version is >= the binding's, so a 2.3.2 binding would refuse to load against Fedora's 2.2.0 library.