Bug#1145040: casadi: FTBFS against python 3.15rc2

Maximiliano Curia maxy at debian.org
Wed Sep 9 21:30:37 BST 2026


user debian-python at lists.debian.org
usertags 1145040 python3.15
thanks

Hi!

While rebuilding the python related packages against the Python 3.15rc2
version I ran into this FTBFS [1].

I had to make a few changes to get the package back to a healthy state.
- I applied upstream's commit 28f0910 [2]: Python: stop relying on
   SWIG's removed Py2 compat macros [ci:run_slow]. This is a different
   patch than the one proposed by Rafael, more invasive but as it's
   already applied upstream it makes sense to use that one.
- The package repository was missing the `debian/*.install` files, so I
   added those
- And I had to also add a missing python3-numpy dependency.

I've applied these fixes in the sandbox [3] to verify that it builds
successfully, these should be applied in Debian to get the package back
to a healthy state.

Happy hacking,

[1]: https://debusine.debian.net/debian/r-python-python3.15/artifact/TODO/
[2]: https://github.com/casadi/casadi/commit/28f0910eedc41861e8544d4c7590b67f934dd57c
[3]: https://debusine.debian.net/debian/r-python-python3.15/

-- 
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
-------------- next part --------------
diff -Nru casadi-3.7.2+ds2/debian/casadi.install casadi-3.7.2+ds2/debian/casadi.install
--- casadi-3.7.2+ds2/debian/casadi.install	2025-07-30 13:35:32.000000000 +0200
+++ casadi-3.7.2+ds2/debian/casadi.install	2026-09-08 14:27:12.000000000 +0200
@@ -1 +1 @@
-usr/bin
+usr/bin/casadi-cli
diff -Nru casadi-3.7.2+ds2/debian/changelog casadi-3.7.2+ds2/debian/changelog
--- casadi-3.7.2+ds2/debian/changelog	2026-06-30 20:49:54.000000000 +0200
+++ casadi-3.7.2+ds2/debian/changelog	2026-09-08 14:27:12.000000000 +0200
@@ -1,3 +1,16 @@
+casadi (3.7.2+ds2-1.1) UNRELEASED; urgency=medium
+
+  [ Bastian Germann ]
+  * Non-maintainer upload.
+  * d/copyright: delete empty line
+
+  [ Maximiliano Curia ]
+  * Add upstream patch 0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch
+  * commit missing install files
+  * Add numpy runtime dependency
+
+ -- Maximiliano Curia <maxy at debian.org>  Tue, 08 Sep 2026 14:27:12 +0200
+
 casadi (3.7.2+ds2-1) unstable; urgency=medium
 
   * New upstream release.
diff -Nru casadi-3.7.2+ds2/debian/control casadi-3.7.2+ds2/debian/control
--- casadi-3.7.2+ds2/debian/control	2026-06-30 19:42:29.000000000 +0200
+++ casadi-3.7.2+ds2/debian/control	2026-09-08 14:27:12.000000000 +0200
@@ -74,6 +74,7 @@
 Architecture: any
 Depends:
  python3,
+ python3-numpy,
  ${misc:Depends},
  ${python3:Depends},
  ${shlibs:Depends},
diff -Nru casadi-3.7.2+ds2/debian/copyright casadi-3.7.2+ds2/debian/copyright
--- casadi-3.7.2+ds2/debian/copyright	2026-06-30 19:42:26.000000000 +0200
+++ casadi-3.7.2+ds2/debian/copyright	2026-09-08 14:27:12.000000000 +0200
@@ -173,7 +173,6 @@
  modification, are permitted provided that the following conditions
  are met:
  .
-
   * Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
  .
diff -Nru casadi-3.7.2+ds2/debian/libcasadi3.7.install casadi-3.7.2+ds2/debian/libcasadi3.7.install
--- casadi-3.7.2+ds2/debian/libcasadi3.7.install	2025-07-30 14:26:40.000000000 +0200
+++ casadi-3.7.2+ds2/debian/libcasadi3.7.install	2026-09-08 14:27:12.000000000 +0200
@@ -1 +1 @@
-usr/lib/${DEB_HOST_MULTIARCH}/*.so.*
+usr/lib/*/libcasadi*.so.*
diff -Nru casadi-3.7.2+ds2/debian/libcasadi-dev.install casadi-3.7.2+ds2/debian/libcasadi-dev.install
--- casadi-3.7.2+ds2/debian/libcasadi-dev.install	2025-06-24 16:25:41.000000000 +0200
+++ casadi-3.7.2+ds2/debian/libcasadi-dev.install	2026-09-08 14:27:12.000000000 +0200
@@ -1,4 +1,4 @@
-usr/include
-usr/lib/${DEB_HOST_MULTIARCH}/cmake
-usr/lib/${DEB_HOST_MULTIARCH}/pkgconfig
-usr/lib/*/lib*.so
+usr/include/casadi
+usr/lib/*/cmake/casadi
+usr/lib/*/libcasadi*.so
+usr/lib/*/pkgconfig/casadi.pc
diff -Nru casadi-3.7.2+ds2/debian/patches/0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch casadi-3.7.2+ds2/debian/patches/0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch
--- casadi-3.7.2+ds2/debian/patches/0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch	1970-01-01 01:00:00.000000000 +0100
+++ casadi-3.7.2+ds2/debian/patches/0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch	2026-09-08 14:27:12.000000000 +0200
@@ -0,0 +1,239 @@
+From: Joris Gillis <joris.gillis42 at gmail.com>
+Date: Thu, 20 Aug 2026 21:01:08 +0200
+Subject: Python: stop relying on SWIG's removed Py2 compat macros [ci:run_slow]
+
+SWIG 4.5.0 removed the Python 2 compatibility macros that pyhead.swg used
+to inject into every generated wrapper, so casadi.i no longer compiles
+with a current SWIG.  Substitute the macro bodies as SWIG documents them:
+PyString_FromString -> PyUnicode_FromString, PyInt_Check -> PyLong_Check,
+PyInt_AsLong -> PyLong_AsLong.
+
+PyString_Check expanded to PyBytes_Check, not PyUnicode_Check, so its
+three call sites need care rather than a rename: keep excluding bytes
+from the "iterable of matrices" path (iterating b"ab" would silently
+yield [97, 98]), and drop bytes from to_ptr(std::string) and the
+std::map key check, where python_string_to_std_string fed a NULL from
+PyUnicode_AsUTF8* straight into a std::string -- casadi.SX.sym(b"x")
+segfaulted the interpreter.
+
+Harden python_string_to_std_string itself rather than leaving that a
+caller-side convention: it now decodes to "" instead of dereferencing
+NULL, for both a missing object and a failed decode.  That closes a
+second reachable crash -- handle_director_exception calls it on
+PyObject_Str(pvalue), which returns NULL when the exception's __str__
+raises, and then Py_DECREF'd that NULL as well.
+
+Auditing the rest of the file for the same shape turned up two more:
+
+- the 1-D probe in to_ptr(std::vector) released its shape tuple only on
+  the reject path, so every 1-D numeric-like argument leaked a reference
+  per probe (measured: 3000 for 1000 conversions).  It also fed a
+  possibly-NULL shape to PyTuple_Check.
+- PyObjectHasClassName had the same unchecked-getattr pattern twice over
+  plus two bare Py_DECREFs.  It has no callers anywhere in the tree or in
+  the generated wrappers, so drop it rather than harden dead code.
+
+Covered by typemaps.py test_bytes_typemaps and test_shape_attr_no_refleak,
+and function.py test_Callback_unstringable_exception.
+
+Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
+Origin: upstream, https://github.com/casadi/casadi/commit/28f0910eedc41861e8544d4c7590b67f934dd57c
+Applied-Upstream: 3.8.0
+---
+ swig/casadi.i                | 68 +++++++++++++++++++++++++++++---------------
+ swig/python/casadi_numpy.hpp |  4 ---
+ 2 files changed, 45 insertions(+), 27 deletions(-)
+
+diff --git a/swig/casadi.i b/swig/casadi.i
+index 7be64320c..d5b64090a 100644
+--- a/swig/casadi.i
++++ b/swig/casadi.i
+@@ -109,16 +109,20 @@
+       // SWIG_PYTHON_THREAD_END_BLOCK is not needed, destructor will release GIL
+     }
+ 
++    // Decodes to "" rather than dereferencing NULL when str_py is absent or not a str
+     std::string python_string_to_std_string(PyObject *str_py) {
++      if (!str_py) return "";
+ #if SWIG_VERSION < 0x040200
+-      const char *str_char = SWIG_Python_str_AsChar(str_py);
+-      std::string str(str_char);
+-      SWIG_Python_str_DelForPy3(str_char);
++      const char *chars = SWIG_Python_str_AsChar(str_py);
++      std::string str(chars ? chars : "");
++      SWIG_Python_str_DelForPy3(chars);
+ #else
+       PyObject *bytes = NULL;
+-      std::string str(SWIG_PyUnicode_AsUTF8AndSize(str_py, NULL, &bytes));
++      const char *chars = SWIG_PyUnicode_AsUTF8AndSize(str_py, NULL, &bytes);
++      std::string str(chars ? chars : "");
+       Py_XDECREF(bytes);
+ #endif
++      if (!chars) PyErr_Clear();
+       return str;
+     }
+ 
+@@ -139,7 +143,7 @@
+       PyErr_Fetch(&ptype, &pvalue, &ptraceback);
+       PyObject* msg_py = PyObject_Str(pvalue);
+       msg = python_string_to_std_string(msg_py);
+-      Py_DECREF(msg_py);
++      Py_XDECREF(msg_py);
+       PyErr_Restore(ptype, pvalue, ptraceback);
+       PyErr_Print();
+ #ifndef CASADI_WITH_PYTHON_GIL_RELEASE
+@@ -684,7 +688,7 @@ namespace std {
+     GUESTOBJECT* full(const DM& m, bool simplify=false) {
+ #ifdef SWIGPYTHON
+       PyObject *p = from_ptr(&m);
+-      PyObject *method_name = PyString_FromString("toarray");
++      PyObject *method_name = PyUnicode_FromString("toarray");
+       PyObject *cr = PyObject_CallMethodObjArgs(p, method_name, (simplify? Py_True: Py_False), 0);
+       Py_DECREF(method_name);
+       Py_DECREF(p);
+@@ -737,7 +741,7 @@ namespace std {
+ %#if PY_VERSION_HEX < 0x03070000
+       PyObject* module = PyImport_AddModule("casadi");
+ %#else
+-      PyObject* c_name = PyString_FromString("casadi");
++      PyObject* c_name = PyUnicode_FromString("casadi");
+       PyObject* module = PyImport_GetModule(c_name);
+       Py_DECREF(c_name);
+ %#endif
+@@ -1273,15 +1277,18 @@ namespace std {
+ #ifdef SWIGPYTHON
+ 
+       // Some built-in types are iterable
+-      if (PyDict_Check(p) || PyString_Check(p) || PySet_Check(p) || PyUnicode_Check(p)) return false;
++      // bytes is iterable but yields ints; walking it as a vector of numbers
++      // would silently turn b"ab" into [97, 98] instead of raising.
++      if (PyDict_Check(p) || PyBytes_Check(p) || PySet_Check(p) || PyUnicode_Check(p)) return false;
+ 
+       // Make sure shape is 1D, if defined.
++      if (PyErr_Occurred()) PyErr_Clear(); // Clear pending exception before type check
+       if (PyObject_HasAttrString(p, "shape")) {
+         PyObject * shape = PyObject_GetAttrString(p, "shape");
+-        if(!PyTuple_Check(shape) || PyTuple_Size(shape)!=1) {
+-          Py_DECREF(shape);
+-          return false;
+-        }
++        if (!shape) { PyErr_Clear(); return false; }
++        bool is_1d = PyTuple_Check(shape) && PyTuple_Size(shape)==1;
++        Py_DECREF(shape);
++        if (!is_1d) return false;
+       }
+ 
+       // Iterator to the sequence
+@@ -1554,7 +1561,9 @@ namespace std {
+       }
+ 
+ #ifdef SWIGPYTHON
+-      if (PyString_Check(p) || PyUnicode_Check(p)) {
++      // str only: python_string_to_std_string goes through
++      // PyUnicode_AsUTF8*, which returns NULL for a bytes object.
++      if (PyUnicode_Check(p)) {
+         if (m) {
+           (*m)->clear();
+           (*m)->append(python_string_to_std_string(p));
+@@ -1589,7 +1598,7 @@ namespace std {
+ 
+     GUESTOBJECT* from_ptr(const std::string *a) {
+ #ifdef SWIGPYTHON
+-      return PyString_FromString(a->c_str());
++      return PyUnicode_FromString(a->c_str());
+ #elif defined(SWIGMATLAB)
+       return mxCreateString(a->c_str());
+ #else
+@@ -1614,23 +1623,40 @@ namespace std {
+ #ifdef SWIGPYTHON
+ 
+       // Python casadi_int
+-      if (PyInt_Check(p)) {
++      if (PyLong_Check(p)) {
+         if (m) {
+-          (**m).start = PyInt_AsLong(p);
++          (**m).start = PyLong_AsLong(p);
+           (**m).stop = (**m).start+1;
+           if ((**m).stop==0) (**m).stop = std::numeric_limits<casadi_int>::max();
+         }
+         return true;
+       }
+-      // Python slice
++      // Python slice - use Limited API compatible approach
+       if (PySlice_Check(p)) {
+-        PySliceObject *r = (PySliceObject*)(p);
++        Py_ssize_t start, stop, step;
++%#if PY_VERSION_HEX >= 0x03060100
++        int res = PySlice_Unpack(p, &start, &stop, &step);
++%#else
++        // Python 2.7 and early Python 3.x use _PySlice_Unpack (private API)
++        int res = _PySlice_Unpack(p, &start, &stop, &step);
++%#endif
++        if (res < 0) {
++          return false;  // TypeError already set by PySlice_Unpack
++        }
++
+         if (m) {
+-          (**m).start = (r->start == Py_None || PyNumber_AsSsize_t(r->start, NULL) <= std::numeric_limits<int>::min())
+-            ? std::numeric_limits<casadi_int>::min() : PyInt_AsLong(r->start);
+-          (**m).stop  = (r->stop ==Py_None || PyNumber_AsSsize_t(r->stop, NULL)>= std::numeric_limits<int>::max())
+-            ? std::numeric_limits<casadi_int>::max() : PyInt_AsLong(r->stop);
+-          if(r->step !=Py_None) (**m).step  = PyInt_AsLong(r->step);
++          // Map sentinel values from PySlice_Unpack to CasADi's limits
++          // PySlice_Unpack returns PY_SSIZE_T_MIN or PY_SSIZE_T_MAX as sentinels
++          // depending on step direction, so check both extremes
++          (**m).start = (start == PY_SSIZE_T_MIN || start == PY_SSIZE_T_MAX)
++              ? std::numeric_limits<casadi_int>::min()
++              : static_cast<casadi_int>(start);
++          (**m).stop = (stop == PY_SSIZE_T_MAX || stop == PY_SSIZE_T_MIN)
++              ? std::numeric_limits<casadi_int>::max()
++              : static_cast<casadi_int>(stop);
++          if (step != 1) {
++            (**m).step = static_cast<casadi_int>(step);
++          }
+         }
+         return true;
+       }
+@@ -1655,7 +1681,7 @@ namespace std {
+         PyObject *key, *value;
+         Py_ssize_t pos = 0;
+         while (PyDict_Next(p, &pos, &key, &value)) {
+-          if (!(PyString_Check(key) || PyUnicode_Check(key))) return false;
++          if (!PyUnicode_Check(key)) return false;
+           if (m) {
+             M *v=&(**m)[python_string_to_std_string(key)], *v2=v;
+             if (!casadi::to_ptr(value, &v)) return false;
+@@ -1912,18 +1938,6 @@ namespace std {
+ 
+ %fragment("casadi_dmatrix", "header", fragment="casadi_aux") {
+   namespace casadi {
+-#ifdef SWIGPYTHON
+-    /** Check PyObjects by class name */
+-    bool PyObjectHasClassName(PyObject* p, const char * name) {
+-      PyObject * classo = PyObject_GetAttrString( p, "__class__");
+-      PyObject * classname = PyObject_GetAttrString( classo, "__name__");
+-
+-      bool ret = python_string_to_std_string(classname) == name;
+-      Py_DECREF(classo);Py_DECREF(classname);
+-      return ret;
+-    }
+-#endif // SWIGPYTHON
+-
+     bool to_ptr(GUESTOBJECT *p, DM** m) {
+       // Treat Null
+       if (is_null(p)) return false;
+diff --git a/swig/python/casadi_numpy.hpp b/swig/python/casadi_numpy.hpp
+index 2cdbd6e85..2f5ee9329 100644
+--- a/swig/python/casadi_numpy.hpp
++++ b/swig/python/casadi_numpy.hpp
+@@ -21,10 +21,6 @@
+ #define array_size(a,i)     (PyArray_DIM(((PyArrayObject *)a),i))
+ #define array_data(a)       (PyArray_DATA(((PyArrayObject *)a)))
+ 
+-#if PY_MAJOR_VERSION >= 3
+-#define PyInt_Type PyLong_Type
+-#endif 
+-
+ /* Convert the given PyObject to a NumPy array with the given
+  * typecode.  On success, return a valid PyArrayObject* with the
+  * correct type.  On failure, the python error string will be set and
diff -Nru casadi-3.7.2+ds2/debian/patches/series casadi-3.7.2+ds2/debian/patches/series
--- casadi-3.7.2+ds2/debian/patches/series	2026-06-30 20:22:49.000000000 +0200
+++ casadi-3.7.2+ds2/debian/patches/series	2026-09-08 14:27:12.000000000 +0200
@@ -1 +1,2 @@
 0001-Testsuite-force-python3.patch
+0002-Python-stop-relying-on-SWIG-s-removed-Py2-compat-mac.patch
diff -Nru casadi-3.7.2+ds2/debian/python3-casadi.install casadi-3.7.2+ds2/debian/python3-casadi.install
--- casadi-3.7.2+ds2/debian/python3-casadi.install	2025-06-22 17:00:23.000000000 +0200
+++ casadi-3.7.2+ds2/debian/python3-casadi.install	2026-09-08 14:27:12.000000000 +0200
@@ -1 +1 @@
-usr/lib/python3
+usr/lib/python3/dist-packages/casadi


More information about the debian-science-maintainers mailing list