bytedeco/javacpp-presets

[CPython] Include TCL/TK for Windows binding

HannesWell opened this issue · 3 comments

When using Python's tkinter directly or indirectly a local installation of tcl and tk is necessary.
And while JavaCPP's cPypthon includes the tkinter module it doesn't contain tcl/tk.

Installing tcl/tk via PIP or alike is not possible. On Windows the CPython can install tcl/tk as part of the python installation and on Linux it can be installed via apt-get and is often already available:
https://stackoverflow.com/questions/69603788/how-to-pip-install-tkinter

Therefore I would like to suggest to include tcl/tk into JavaCPP's cpython-preset for Windows.
Without that the following Python program cannot be executed using javacpp-embedded-python:

        Pip.install("matplotlib"); // "numpy"

        StringJoiner lines = new StringJoiner("\n");
        lines.add("import matplotlib.pyplot as plt");
        lines.add("import numpy as np");
        lines.add("x = np.linspace(0, 2 * np.pi, 200)");
        lines.add("y = np.sin(x)");
        lines.add("fig, ax = plt.subplots()");
        lines.add("ax.plot(x, y)");
        lines.add("fig.savefig('plot.png')");

        Python.eval(lines.toString());
org.bytedeco.embeddedpython.PythonException: Failed to execute:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2 * np.pi, 200)
y = np.sin(x)
fig, ax = plt.subplots()
ax.plot(x, y)
fig.savefig('plot.png')
Traceback (most recent call last):
  File "<string>", line 6, in <module>
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\pyplot.py",
line 1759, in subplots
    fig = figure(**fig_kw)
          ^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\pyplot.py",
line 1027, in figure
    manager = new_figure_manager(
              ^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\pyplot.py",
line 550, in new_figure_manager
    return _get_backend_mod().new_figure_manager(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\backend_bases.py",
line 3507, in new_figure_manager
    return cls.new_figure_manager_given_figure(num, fig)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\backend_bases.py",
line 3512, in new_figure_manager_given_figure
    return cls.FigureCanvas.new_manager(figure, num)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\backend_bases.py",
line 1797, in new_manager
    return cls.manager_class.create_with_canvas(cls, figure, num)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\site-packages\matplotlib\backends\_backend_tk.py",
line 483, in create_with_canvas
    window = tk.Tk(className="matplotlib")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "~\.javacpp\cache\cpython-3.12.1-1.5.10-windows-x86_64.jar\org\bytedeco\cpython\windows-x86_64\lib\tkinter\__init__.py",
line 2340, in __init__
    self.tk = _tkinter.create(screenName, baseName, className,
interactive, wantobjects, useTk, sync, use)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_tkinter.TclError: Can't find a usable init.tcl in the following
directories:
    ~/.javacpp/cache/cpython-3.12.1-1.5.10-windows-x86_64.jar/org/bytedeco/cpython/windows-x86_64/lib/tcl8.6

This probably means that Tcl wasn't installed properly.

A workaround is to wrap the tcl folder of a 'classic' Cpython installation and distribute and cache it along with javacpp-cpython and run the following as first steps after the initialization of python (e.g. via Py_Initialize()):

File tclFolder = Loader.cacheResource("/foo/bar/tcl/" + Loader.getPlatform());
Path tclLibrary = tclFolder.toPath().resolve("tcl8.6");
Path tkLibrary = tclFolder.toPath().resolve("tk8.6");
Path tixLibrary = tclFolder.toPath().resolve("tix8.4.3");
PyRun_SimpleStringFlags(String.format("""
	import os
	os.environ['TCL_LIBRARY'] = r'%1$s'
	os.environ['TK_LIBRARY'] = r'%2$s'
	os.environ['TIX_LIBRARY'] = r'%3$s'
""", tclLibrary, tkLibrary, tixLibrary));

That's possible, but cumbersome and not easy to get to if one is not deeply familiar with the topic.

Unfortunately I cannot tell yet how including tcl/tk could be achieved, since I haven't fully understood yet how the CPython installer bundle tcl/tk.

Looks like we can use PCbuild/prepare_tcltk.bat for that on Windows:
https://github.com/python/cpython/blob/main/PCbuild/readme.txt

Thanks for the pointer. This looks promising. I'll look into it.
Do you have any immediate hints where this could maybe applied in the cpython preset?

I'm guessing somewhere before calling build.bat here:
https://github.com/bytedeco/javacpp-presets/blob/master/cpython/cppbuild.sh#L189