Scriptable Application Example¶
This example demonstrates how to make a Qt C++ application scriptable.
It has a class MainWindow
(files mainwindow.cpp,h
)
that inherits from QMainWindow
, for which bindings are generated
using Shiboken.
The header wrappedclasses.h
is passed to Shiboken which generates
class wrappers and headers in a sub directory called AppLib/
which are linked to the application.
The files pythonutils.cpp,h
contain some code which binds the
instance of MainWindow
to a variable called mainWindow
in
the global Python namespace (__main___
).
It is then possible to run Python script snippets like:
mainWindow.testFunction1()
which trigger the underlying C++ function.
Building the project¶
This example can be built using CMake
or QMake
,
but there are common requirements that you need to take into
consideration:
Make sure that a –standalone PySide package (bundled with Qt libraries) is installed into the current active Python environment (system or virtualenv)
qmake has to be in your PATH:
so that CMake find_package(Qt6 COMPONENTS Core) works (used for include headers),
used for building the application with qmake instead of CMake
use the same Qt version for building the example application, as was used for building PySide, this is to ensure binary compatibility between the newly generated bindings libraries, the PySide libraries and the Qt libraries.
For Windows you will also need: * a Visual Studio environment to be active in your terminal
Correct visual studio architecture chosen (32 vs 64 bit)
Make sure that your Qt + Python + PySide package + app build configuration is the same (all Release, which is more likely, or all Debug).
Make sure that your Qt + Python + PySide package + app are built with a compatible version of MSVC, to avoid mixing of C++ runtime libraries.
Both build options will use the pyside_config.py
file to configure the project
using the current PySide/Shiboken installation (for qmake via pyside.pri
,
and for CMake via the project CMakeLists.txt
).
Using CMake¶
To build this example with CMake you will need a recent version of CMake (3.16+).
You can build this example by executing the following commands (slightly adapted to your file system layout) in a terminal:
macOS/Linux:
cd ~/pyside-setup/examples/scriptableapplication
mkdir build
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
./scriptableapplication
On Windows:
cd C:\pyside-setup\examples\scriptableapplication
mkdir build
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl.exe
ninja
.\scriptableapplication.exe
Using QMake¶
The file scriptableapplication.pro
is the project file associated
to the example when using qmake.
You can build this example by executing:
mkdir build
cd build
qmake ..
make # or nmake / jom for Windows
Windows troubleshooting¶
Using qmake
should work out of the box, there was a known issue
with directories and white spaces that is solved by using the
“~1” character, so the path will change from:
c:\Program Files\Python39\libs
to
c:\Progra~1\Python39\libs
this will avoid the issues when the Makefiles are generated.
It is possible when using CMake
to pick up the wrong compiler
for a different architecture, but it can be addressed explicitly
by setting the CC
environment variable:
set CC=cl
passing the compiler on the command line:
cmake -S.. -B. -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe
or using the -G option:
cmake -S.. -B. -G "Visual Studio 14 Win64" -DCMAKE_BUILD_TYPE=Release
If the -G "Visual Studio 14 Win64"
option is used, a sln
file
will be generated, and can be used with MSBuild
instead of ninja
.
MSBuild scriptableapplication.sln "/p:Configuration=Release"
Note that using the “Ninja” generator is preferred to the MSBuild one, because in the latter case the executable is placed into a directory other than the one that contains the dependency dlls (shiboken, pyside). This leads to execution problems if the application is started within the Release subdirectory and not the one containing the dependencies.
Virtualenv Support¶
If the application is started from a terminal with an activated python virtual environment, that environment’s packages will be used for the python module import process. In this case, make sure that the application was built while the virtualenv was active, so that the build system picks up the correct python shared library and PySide package.
Windows Notes¶
The build config of the application (Debug or Release) should match the PySide6 build config, otherwise the application will not properly work.
In practice this means the only supported configurations are:
release config build of the application + PySide
setup.py
without--debug
flag +python.exe
for the PySide build process +python39.dll
for the linked in shared library + release build of Qt.debug config build of the application + PySide
setup.py
with--debug
flag +python_d.exe
for the PySide build process +python39_d.dll
for the linked in shared library + debug build of Qt.
This is necessary because all the shared libraries in question have to
link to the same C++ runtime library (msvcrt.dll
or msvcrtd.dll
).
To make the example as self-contained as possible, the shared libraries
in use (pyside6.dll
, shiboken6.dll
) are hard-linked into the build
folder of the application.
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "mainwindow.h"
#include <QApplication>
#include <QScreen>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow mainWindow;
const QRect availableGeometry = mainWindow.screen()->availableGeometry();
mainWindow.resize(availableGeometry.width() / 2, availableGeometry.height() / 2);
mainWindow.show();
return a.exec();
}
<?xml version="1.0"?>
<!--
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-->
<typesystem package="AppLib">
<load-typesystem name="typesystem_widgets.xml" generate="no"/>
<object-type name="MainWindow"/>
</typesystem>
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "mainwindow.h"
#include "pythonutils.h"
#include <QtWidgets/QApplication>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QPlainTextEdit>
#include <QtWidgets/QStatusBar>
#include <QtWidgets/QToolBar>
#include <QtWidgets/QVBoxLayout>
#include <QtGui/QAction>
#include <QtGui/QFontDatabase>
#include <QtGui/QIcon>
#include <QtCore/QDebug>
#include <QtCore/QTextStream>
using namespace Qt::StringLiterals;
static const auto defaultScript = R"(import AppLib
print("Hello, world")
mainWindow.testFunction1()
)"_L1;
MainWindow::MainWindow()
: m_scriptEdit(new QPlainTextEdit(defaultScript, this))
{
setWindowTitle(tr("Scriptable Application"));
auto *fileMenu = menuBar()->addMenu(tr("&File"));
const QIcon runIcon = QIcon::fromTheme("system-run"_L1);
auto *runAction = fileMenu->addAction(runIcon, tr("&Run..."),
this, &MainWindow::slotRunScript);
runAction->setShortcut(Qt::CTRL | Qt::Key_R);
auto *diagnosticAction = fileMenu->addAction(tr("&Print Diagnostics"),
this, &MainWindow::slotPrintDiagnostics);
diagnosticAction->setShortcut(Qt::CTRL | Qt::Key_D);
fileMenu->addAction(tr("&Invoke testFunction1()"),
this, &MainWindow::testFunction1);
const QIcon quitIcon = QIcon::fromTheme(QIcon::ThemeIcon::ApplicationExit);
auto *quitAction = fileMenu->addAction(quitIcon, tr("&Quit"),
qApp, &QCoreApplication::quit);
quitAction->setShortcut(Qt::CTRL | Qt::Key_Q);
auto *editMenu = menuBar()->addMenu(tr("&Edit"));
const QIcon clearIcon = QIcon::fromTheme(QIcon::ThemeIcon::EditClear);
auto *clearAction = editMenu->addAction(clearIcon, tr("&Clear"),
m_scriptEdit, &QPlainTextEdit::clear);
auto *helpMenu = menuBar()->addMenu(tr("&Help"));
const QIcon aboutIcon = QIcon::fromTheme(QIcon::ThemeIcon::HelpAbout);
auto *aboutAction = helpMenu->addAction(aboutIcon, tr("&About Qt"),
qApp, &QApplication::aboutQt);
auto *toolBar = new QToolBar;
addToolBar(toolBar);
toolBar->addAction(quitAction);
toolBar->addSeparator();
toolBar->addAction(clearAction);
toolBar->addSeparator();
toolBar->addAction(runAction);
toolBar->addSeparator();
toolBar->addAction(aboutAction);
m_scriptEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
setCentralWidget(m_scriptEdit);
if (!PythonUtils::bindAppObject("__main__"_L1, "mainWindow"_L1,
PythonUtils::MainWindowType, this)) {
statusBar()->showMessage(tr("Error loading the application module"));
}
}
void MainWindow::slotRunScript()
{
const QString text = m_scriptEdit->toPlainText().trimmed();
if (!text.isEmpty())
runScript(text);
}
void MainWindow::slotPrintDiagnostics()
{
const QString script = R"P(import sys
print('Path=', sys.path)
print('Executable=', sys.executable)
)P"_L1;
runScript(script);
}
void MainWindow::runScript(const QString &script)
{
if (!::PythonUtils::runScript(script))
statusBar()->showMessage(tr("Error running script"));
}
void MainWindow::testFunction1()
{
static int n = 1;
QString message;
QTextStream(&message) << __FUNCTION__ << " called #" << n++;
qDebug().noquote() << message;
statusBar()->showMessage(message);
}
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QtWidgets/QMainWindow>
QT_FORWARD_DECLARE_CLASS(QPlainTextEdit)
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow();
void testFunction1();
static constexpr auto TEST = QLatin1StringView("test");
private Q_SLOTS:
void slotRunScript();
void slotPrintDiagnostics();
private:
void runScript(const QString &);
QPlainTextEdit *m_scriptEdit;
};
#endif // MAINWINDOW_H
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#include "pythonutils.h"
#include <QtCore/QByteArray>
#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtCore/QOperatingSystemVersion>
#include <QtCore/QStringList>
#include <QtCore/QTemporaryFile>
#include <QtCore/QDir>
#include <sbkpython.h>
#include <sbkconverter.h>
#include <sbkmodule.h>
extern "C" PyObject *PyInit_AppLib();
static const char moduleName[] = "AppLib";
// This variable stores all Python types exported by this module.
extern PyTypeObject **SbkAppLibTypes;
// This variable stores all type converters exported by this module.
extern SbkConverter **SbkAppLibTypeConverters;
namespace PythonUtils {
static State state = PythonUninitialized;
static void cleanup()
{
if (state > PythonUninitialized) {
Py_Finalize();
state = PythonUninitialized;
}
}
static const char virtualEnvVar[] = "VIRTUAL_ENV";
// If there is an active python virtual environment, use that environment's
// packages location.
static void initVirtualEnvironment()
{
// As of Python 3.8, Python is no longer able to run stand-alone in a
// virtualenv due to missing libraries. Add the path to the modules instead.
if (QOperatingSystemVersion::currentType() == QOperatingSystemVersion::Windows
&& (PY_MAJOR_VERSION > 3 || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION >= 8))) {
const QByteArray virtualEnvPath = qgetenv(virtualEnvVar);
qputenv("PYTHONPATH", virtualEnvPath + "\\Lib\\site-packages");
}
}
State init()
{
if (state > PythonUninitialized)
return state;
if (qEnvironmentVariableIsSet(virtualEnvVar))
initVirtualEnvironment();
if (PyImport_AppendInittab(moduleName, PyInit_AppLib) == -1) {
qWarning("Failed to add the module '%s' to the table of built-in modules.", moduleName);
return state;
}
Py_Initialize();
qAddPostRoutine(cleanup);
state = PythonInitialized;
const bool pythonInitialized = PyInit_AppLib() != nullptr;
const bool pyErrorOccurred = PyErr_Occurred() != nullptr;
if (pythonInitialized && !pyErrorOccurred) {
state = AppModuleLoaded;
} else {
if (pyErrorOccurred)
PyErr_Print();
qWarning("Failed to initialize the module.");
}
return state;
}
bool bindAppObject(const QString &moduleName, const QString &name,
int index, QObject *o)
{
if (init() != AppModuleLoaded)
return false;
PyTypeObject *typeObject = SbkAppLibTypes[index];
PyObject *po = Shiboken::Conversions::pointerToPython(typeObject, o);
if (!po) {
qWarning() << __FUNCTION__ << "Failed to create wrapper for" << o;
return false;
}
Py_INCREF(po);
PyObject *module = PyImport_AddModule(moduleName.toLocal8Bit().constData());
if (!module) {
Py_DECREF(po);
if (PyErr_Occurred())
PyErr_Print();
qWarning() << __FUNCTION__ << "Failed to locate module" << moduleName;
return false;
}
if (PyModule_AddObject(module, name.toLocal8Bit().constData(), po) < 0) {
if (PyErr_Occurred())
PyErr_Print();
qWarning() << __FUNCTION__ << "Failed add object" << name << "to" << moduleName;
return false;
}
return true;
}
bool runScript(const QString &script)
{
if (init() == PythonUninitialized)
return false;
// Executing the whole script as one line
bool result = true;
const QByteArray line = script.toUtf8();
if (PyRun_SimpleString(line.constData()) == -1) {
if (PyErr_Occurred())
PyErr_Print();
result = false;
}
return result;
}
} // namespace PythonUtils
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef PYTHONUTILS_H
#define PYTHONUTILS_H
#include <QtCore/QStringList>
QT_FORWARD_DECLARE_CLASS(QObject)
namespace PythonUtils {
enum AppLibTypes
{
MainWindowType = 0 // SBK_MAINWINDOW_IDX
};
enum State
{
PythonUninitialized,
PythonInitialized,
AppModuleLoaded
};
State init();
bool bindAppObject(const QString &moduleName, const QString &name,
int index, QObject *o);
bool runScript(const QString &script);
} // namespace PythonUtils
#endif // PYTHONUTILS_H
// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
#ifndef WRAPPEDCLASSES_H
#define WRAPPEDCLASSES_H
#include <mainwindow.h>
#endif // WRAPPEDCLASSES_H
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
cmake_minimum_required(VERSION 3.18)
cmake_policy(VERSION 3.18)
# Enable policy to run automoc on generated files.
if(POLICY CMP0071)
cmake_policy(SET CMP0071 NEW)
endif()
project(scriptableapplication)
# Set CPP standard to C++17 minimum.
set(CMAKE_CXX_STANDARD 17)
# Find required Qt packages.
find_package(Qt6 COMPONENTS Core Gui Widgets)
# Use provided python interpreter if given.
if(NOT python_interpreter)
if(WIN32 AND "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
find_program(python_interpreter "python_d")
if(NOT python_interpreter)
message(FATAL_ERROR
"A debug Python interpreter could not be found, which is a requirement when "
"building this example in a debug configuration. Make sure python_d.exe is in "
"PATH.")
endif()
else()
find_program(python_interpreter "python")
if(NOT python_interpreter)
message(FATAL_ERROR
"No Python interpreter could be found. Make sure python is in PATH.")
endif()
endif()
endif()
message(STATUS "Using python interpreter: ${python_interpreter}")
# Macro to get various pyside / python include / link flags.
macro(pyside_config option output_var)
if(${ARGC} GREATER 2)
set(is_list ${ARGV2})
else()
set(is_list "")
endif()
execute_process(
COMMAND ${python_interpreter} "${CMAKE_SOURCE_DIR}/../utils/pyside_config.py"
${option}
OUTPUT_VARIABLE ${output_var}
OUTPUT_STRIP_TRAILING_WHITESPACE)
if ("${${output_var}}" STREQUAL "")
message(FATAL_ERROR "Error: Calling pyside_config.py ${option} returned no output.")
endif()
if(is_list)
string (REPLACE " " ";" ${output_var} "${${output_var}}")
endif()
endmacro()
# Query for the shiboken6-generator path, PySide6 path, Python path, include paths and linker flags.
pyside_config(--shiboken-module-path SHIBOKEN_MODULE_PATH)
pyside_config(--shiboken-generator-path SHIBOKEN_GENERATOR_PATH)
pyside_config(--pyside-path PYSIDE_PATH)
pyside_config(--python-include-path PYTHON_INCLUDE_DIR)
pyside_config(--shiboken-generator-include-path SHIBOKEN_GENERATOR_INCLUDE_DIR 1)
pyside_config(--pyside-include-path PYSIDE_INCLUDE_DIR 1)
pyside_config(--python-link-flags-cmake PYTHON_LINKING_DATA 0)
pyside_config(--shiboken-module-shared-libraries-cmake SHIBOKEN_MODULE_SHARED_LIBRARIES 0)
pyside_config(--pyside-shared-libraries-cmake PYSIDE_SHARED_LIBRARIES 0)
set(SHIBOKEN_PATH "${SHIBOKEN_GENERATOR_PATH}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}")
if(NOT EXISTS ${SHIBOKEN_PATH})
message(FATAL_ERROR "Shiboken executable not found at path: ${SHIBOKEN_PATH}")
endif()
# Get all relevant Qt include dirs, to pass them on to shiboken.
get_property(QT_WIDGETS_INCLUDE_DIRS TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES)
set(INCLUDES "")
foreach(INCLUDE_DIR ${QT_WIDGETS_INCLUDE_DIRS})
list(APPEND INCLUDES "-I${INCLUDE_DIR}")
endforeach()
# On macOS, check if Qt is a framework build. This affects how include paths should be handled.
get_target_property(QtCore_is_framework Qt6::Core FRAMEWORK)
if (QtCore_is_framework)
get_target_property(qt_core_library_location Qt6::Core LOCATION)
# PYSIDE-623: We move up until the directory contains all the frameworks.
# This is "lib" in ".../lib/QtCore.framework/Versions/A/QtCore".
get_filename_component(lib_dir "${qt_core_library_location}/../../../.." ABSOLUTE)
list(APPEND INCLUDES "--framework-include-paths=${lib_dir}")
endif()
# Set up the options to pass to shiboken.
set(WRAPPED_HEADER ${CMAKE_SOURCE_DIR}/wrappedclasses.h)
set(TYPESYSTEM_FILE ${CMAKE_SOURCE_DIR}/scriptableapplication.xml)
set(SHIBOKEN_OPTIONS --generator-set=shiboken --enable-parent-ctor-heuristic
--enable-pyside-extensions --enable-return-value-heuristic --use-isnull-as-nb-bool
--avoid-protected-hack
${INCLUDES}
-I${CMAKE_SOURCE_DIR}
-T${CMAKE_SOURCE_DIR}
-T${PYSIDE_PATH}/typesystems
--output-directory=${CMAKE_CURRENT_BINARY_DIR}
)
# Specify which sources will be generated by shiboken, and their dependencies.
set(GENERATED_SOURCES
${CMAKE_CURRENT_BINARY_DIR}/AppLib/applib_module_wrapper.cpp
${CMAKE_CURRENT_BINARY_DIR}/AppLib/mainwindow_wrapper.cpp)
set(GENERATED_SOURCES_DEPENDENCIES
${WRAPPED_HEADER}
${TYPESYSTEM_FILE}
)
# Add custom target to run shiboken.
add_custom_command(OUTPUT ${GENERATED_SOURCES}
COMMAND ${SHIBOKEN_PATH}
${SHIBOKEN_OPTIONS} ${WRAPPED_HEADER} ${TYPESYSTEM_FILE}
DEPENDS ${GENERATED_SOURCES_DEPENDENCIES}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Running generator for ${TYPESYSTEM_FILE}.")
# Set the CPP files.
set(SOURCES
mainwindow.cpp
pythonutils.cpp
${GENERATED_SOURCES}
)
# We need to include the headers for the module bindings that we use.
set(PYSIDE_ADDITIONAL_INCLUDES "")
foreach(INCLUDE_DIR ${PYSIDE_INCLUDE_DIR})
list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtCore")
list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtGui")
list(APPEND PYSIDE_ADDITIONAL_INCLUDES "${INCLUDE_DIR}/QtWidgets")
endforeach()
# =============================================================================================
# !!! (The section below is deployment related, so in a real world application you will want to
# take care of this properly with some custom script or tool).
# =============================================================================================
# Enable rpaths so that the example can be executed from the build dir.
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH ${PYSIDE_PATH} ${SHIBOKEN_MODULE_PATH})
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================
# Declare executable so we can enable automoc.
add_executable(${PROJECT_NAME} main.cpp)
# Enable automoc.
set_property(TARGET ${PROJECT_NAME} PROPERTY AUTOMOC 1)
# Add the rest of the sources.
target_sources(${PROJECT_NAME} PUBLIC ${SOURCES})
# Apply relevant include and link flags.
target_include_directories(${PROJECT_NAME} PRIVATE ${PYTHON_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${SHIBOKEN_GENERATOR_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_INCLUDE_DIR})
target_include_directories(${PROJECT_NAME} PRIVATE ${PYSIDE_ADDITIONAL_INCLUDES})
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Widgets)
target_link_libraries(${PROJECT_NAME} PRIVATE ${SHIBOKEN_MODULE_SHARED_LIBRARIES})
target_link_libraries(${PROJECT_NAME} PRIVATE ${PYSIDE_SHARED_LIBRARIES})
# Find and link to the python library.
list(GET PYTHON_LINKING_DATA 0 PYTHON_LIBDIR)
list(GET PYTHON_LINKING_DATA 1 PYTHON_LIB)
find_library(PYTHON_LINK_FLAGS ${PYTHON_LIB} PATHS ${PYTHON_LIBDIR} HINTS ${PYTHON_LIBDIR})
target_link_libraries(${PROJECT_NAME} PRIVATE ${PYTHON_LINK_FLAGS})
# Same as CONFIG += no_keywords to avoid syntax errors in object.h due to the usage of the word Slot
target_compile_definitions(${PROJECT_NAME} PRIVATE QT_NO_KEYWORDS)
if(WIN32)
# =============================================================================================
# !!! (The section below is deployment related, so in a real world application you will want to
# take care of this properly (this is simply to eliminate errors that users usually encounter.
# =============================================================================================
# Circumvent some "#pragma comment(lib)"s in "include/pyconfig.h" which might force to link
# against a wrong python shared library.
set(PYTHON_VERSIONS_LIST 3 36 37 38 39)
set(PYTHON_ADDITIONAL_LINK_FLAGS "")
foreach(VER ${PYTHON_VERSIONS_LIST})
set(PYTHON_ADDITIONAL_LINK_FLAGS
"${PYTHON_ADDITIONAL_LINK_FLAGS} /NODEFAULTLIB:\"python${VER}_d.lib\"")
set(PYTHON_ADDITIONAL_LINK_FLAGS
"${PYTHON_ADDITIONAL_LINK_FLAGS} /NODEFAULTLIB:\"python${VER}.lib\"")
endforeach()
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${PYTHON_ADDITIONAL_LINK_FLAGS}")
# Add custom target to hard link PySide6 shared libraries (just like in qmake example), so you
# don't have to set PATH manually to point to the PySide6 package.
set(shared_libraries ${SHIBOKEN_MODULE_SHARED_LIBRARIES} ${PYSIDE_SHARED_LIBRARIES})
foreach(LIBRARY_PATH ${shared_libraries})
string(REGEX REPLACE ".lib$" ".dll" LIBRARY_PATH ${LIBRARY_PATH})
get_filename_component(BASE_NAME ${LIBRARY_PATH} NAME)
file(TO_NATIVE_PATH ${LIBRARY_PATH} SOURCE_PATH)
file(TO_NATIVE_PATH "${CMAKE_CURRENT_BINARY_DIR}/${BASE_NAME}" DEST_PATH)
add_custom_command(OUTPUT "${BASE_NAME}"
COMMAND mklink /H "${DEST_PATH}" "${SOURCE_PATH}"
DEPENDS ${LIBRARY_PATH}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Creating hardlink to PySide6 shared library ${BASE_NAME}")
# Fake target that depends on the previous one, but has special ALL keyword, which means
# it will always be executed.
add_custom_target("fake_${BASE_NAME}" ALL DEPENDS ${BASE_NAME})
endforeach()
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================
endif()
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import sysconfig
from enum import Enum
import glob
import os
import re
import sys
PYSIDE = 'pyside6'
PYSIDE_MODULE = 'PySide6'
SHIBOKEN = 'shiboken6'
class Package(Enum):
SHIBOKEN_MODULE = 1
SHIBOKEN_GENERATOR = 2
PYSIDE_MODULE = 3
generic_error = ('Did you forget to activate your virtualenv? Or perhaps'
f' you forgot to build / install {PYSIDE_MODULE} into your currently active Python'
' environment?')
pyside_error = f'Unable to locate {PYSIDE_MODULE}. {generic_error}'
shiboken_module_error = f'Unable to locate {SHIBOKEN}-module. {generic_error}'
shiboken_generator_error = f'Unable to locate shiboken-generator. {generic_error}'
pyside_libs_error = f'Unable to locate the PySide shared libraries. {generic_error}'
python_link_error = 'Unable to locate the Python library for linking.'
python_include_error = 'Unable to locate the Python include headers directory.'
options = []
# option, function, error, description
options.append(("--shiboken-module-path",
lambda: find_shiboken_module(),
shiboken_module_error,
"Print shiboken module location"))
options.append(("--shiboken-generator-path",
lambda: find_shiboken_generator(),
shiboken_generator_error,
"Print shiboken generator location"))
options.append(("--pyside-path", lambda: find_pyside(), pyside_error,
f"Print {PYSIDE_MODULE} location"))
options.append(("--python-include-path",
lambda: get_python_include_path(),
python_include_error,
"Print Python include path"))
options.append(("--shiboken-generator-include-path",
lambda: get_package_include_path(Package.SHIBOKEN_GENERATOR),
pyside_error,
"Print shiboken generator include paths"))
options.append(("--pyside-include-path",
lambda: get_package_include_path(Package.PYSIDE_MODULE),
pyside_error,
"Print PySide6 include paths"))
options.append(("--python-link-flags-qmake", lambda: python_link_flags_qmake(), python_link_error,
"Print python link flags for qmake"))
options.append(("--python-link-flags-cmake", lambda: python_link_flags_cmake(), python_link_error,
"Print python link flags for cmake"))
options.append(("--shiboken-module-qmake-lflags",
lambda: get_package_qmake_lflags(Package.SHIBOKEN_MODULE), pyside_error,
"Print shiboken6 shared library link flags for qmake"))
options.append(("--pyside-qmake-lflags",
lambda: get_package_qmake_lflags(Package.PYSIDE_MODULE), pyside_error,
"Print PySide6 shared library link flags for qmake"))
options.append(("--shiboken-module-shared-libraries-qmake",
lambda: get_shared_libraries_qmake(Package.SHIBOKEN_MODULE), pyside_libs_error,
"Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for qmake"))
options.append(("--shiboken-module-shared-libraries-cmake",
lambda: get_shared_libraries_cmake(Package.SHIBOKEN_MODULE), pyside_libs_error,
"Print paths of shiboken shared libraries (.so's, .dylib's, .dll's) for cmake"))
options.append(("--pyside-shared-libraries-qmake",
lambda: get_shared_libraries_qmake(Package.PYSIDE_MODULE), pyside_libs_error,
"Print paths of f{PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) "
"for qmake"))
options.append(("--pyside-shared-libraries-cmake",
lambda: get_shared_libraries_cmake(Package.PYSIDE_MODULE), pyside_libs_error,
f"Print paths of {PYSIDE_MODULE} shared libraries (.so's, .dylib's, .dll's) "
"for cmake"))
options_usage = ''
for i, (flag, _, _, description) in enumerate(options):
options_usage += f' {flag:<45} {description}'
if i < len(options) - 1:
options_usage += '\n'
usage = f"""
Utility to determine include/link options of shiboken/PySide and Python for qmake/CMake projects
that would like to embed or build custom shiboken/PySide bindings.
Usage: pyside_config.py [option]
Options:
{options_usage}
-a Print all options and their values
--help/-h Print this help
"""
option = sys.argv[1] if len(sys.argv) == 2 else '-a'
if option == '-h' or option == '--help':
print(usage)
sys.exit(0)
def clean_path(path):
return path if sys.platform != 'win32' else path.replace('\\', '/')
def shared_library_suffix():
if sys.platform == 'win32':
return 'lib'
elif sys.platform == 'darwin':
return 'dylib'
# Linux
else:
return 'so.*'
def import_suffixes():
import importlib.machinery
return importlib.machinery.EXTENSION_SUFFIXES
def is_debug():
debug_suffix = '_d.pyd' if sys.platform == 'win32' else '_d.so'
return any([s.endswith(debug_suffix) for s in import_suffixes()])
def shared_library_glob_pattern():
glob = '*.' + shared_library_suffix()
return glob if sys.platform == 'win32' else 'lib' + glob
def filter_shared_libraries(libs_list):
def predicate(lib_name):
basename = os.path.basename(lib_name)
if 'shiboken' in basename or 'pyside6' in basename:
return True
return False
result = [lib for lib in libs_list if predicate(lib)]
return result
# Return qmake link option for a library file name
def link_option(lib):
# On Linux:
# Since we cannot include symlinks with wheel packages
# we are using an absolute path for the libpyside and libshiboken
# libraries when compiling the project
baseName = os.path.basename(lib)
link = ' -l'
if sys.platform in ['linux', 'linux2']: # Linux: 'libfoo.so' -> '/absolute/path/libfoo.so'
link = lib
elif sys.platform in ['darwin']: # Darwin: 'libfoo.so' -> '-lfoo'
link += os.path.splitext(baseName[3:])[0]
else: # Windows: 'libfoo.dll' -> 'libfoo.dll'
link += os.path.splitext(baseName)[0]
return link
# Locate PySide6 via sys.path package path.
def find_pyside():
return find_package_path(PYSIDE_MODULE)
def find_shiboken_module():
return find_package_path(SHIBOKEN)
def find_shiboken_generator():
return find_package_path(f"{SHIBOKEN}_generator")
def find_package(which_package):
if which_package == Package.SHIBOKEN_MODULE:
return find_shiboken_module()
if which_package == Package.SHIBOKEN_GENERATOR:
return find_shiboken_generator()
if which_package == Package.PYSIDE_MODULE:
return find_pyside()
return None
def find_package_path(dir_name):
for p in sys.path:
if 'site-' in p:
package = os.path.join(p, dir_name)
if os.path.exists(package):
return clean_path(os.path.realpath(package))
return None
# Return version as "x.y" (e.g. 3.9, 3.12, etc)
def python_version():
return str(sys.version_info[0]) + '.' + str(sys.version_info[1])
def get_python_include_path():
return sysconfig.get_path('include')
def python_link_flags_qmake():
flags = python_link_data()
if sys.platform == 'win32':
libdir = flags['libdir']
# This will add the "~1" shortcut for directories that
# contain white spaces
# e.g.: "Program Files" to "Progra~1"
for d in libdir.split("\\"):
if " " in d:
libdir = libdir.replace(d, d.split(" ")[0][:-1] + "~1")
lib_flags = flags['lib']
return f'-L{libdir} -l{lib_flags}'
elif sys.platform == 'darwin':
libdir = flags['libdir']
lib_flags = flags['lib']
return f'-L{libdir} -l{lib_flags}'
else:
# Linux and anything else
libdir = flags['libdir']
lib_flags = flags['lib']
return f'-L{libdir} -l{lib_flags}'
def python_link_flags_cmake():
flags = python_link_data()
libdir = flags['libdir']
lib = re.sub(r'.dll$', '.lib', flags['lib'])
return f'{libdir};{lib}'
def python_link_data():
# @TODO Fix to work with static builds of Python
libdir = sysconfig.get_config_var('LIBDIR')
if libdir is None:
libdir = os.path.abspath(os.path.join(
sysconfig.get_config_var('LIBDEST'), "..", "libs"))
version = python_version()
version_no_dots = version.replace('.', '')
flags = {}
flags['libdir'] = libdir
if sys.platform == 'win32':
suffix = '_d' if is_debug() else ''
flags['lib'] = f'python{version_no_dots}{suffix}'
elif sys.platform == 'darwin':
flags['lib'] = f'python{version}'
# Linux and anything else
else:
flags['lib'] = f'python{version}{sys.abiflags}'
return flags
def get_package_include_path(which_package):
package_path = find_package(which_package)
if package_path is None:
return None
includes = f"{package_path}/include"
return includes
def get_package_qmake_lflags(which_package):
package_path = find_package(which_package)
if package_path is None:
return None
link = f"-L{package_path}"
glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern()))
for lib in filter_shared_libraries(glob_result):
link += ' '
link += link_option(lib)
return link
def get_shared_libraries_data(which_package):
package_path = find_package(which_package)
if package_path is None:
return None
glob_result = glob.glob(os.path.join(package_path, shared_library_glob_pattern()))
filtered_libs = filter_shared_libraries(glob_result)
libs = []
if sys.platform == 'win32':
for lib in filtered_libs:
libs.append(os.path.realpath(lib))
else:
for lib in filtered_libs:
libs.append(lib)
return libs
def get_shared_libraries_qmake(which_package):
libs = get_shared_libraries_data(which_package)
if libs is None:
return None
if sys.platform == 'win32':
if not libs:
return ''
dlls = ''
for lib in libs:
dll = os.path.splitext(lib)[0] + '.dll'
dlls += dll + ' '
return dlls
else:
libs_string = ''
for lib in libs:
libs_string += lib + ' '
return libs_string
def get_shared_libraries_cmake(which_package):
libs = get_shared_libraries_data(which_package)
result = ';'.join(libs)
return result
print_all = option == "-a"
for argument, handler, error, _ in options:
if option == argument or print_all:
handler_result = handler()
if handler_result is None:
sys.exit(error)
line = handler_result
if print_all:
line = f"{argument:<40}: {line}"
print(line)