可脚本化应用程序示例

这个示例演示了如何使一个Qt C++应用程序可编写脚本。

它有一个类 MainWindow(文件 mainwindow.cpp,h),该类继承自 QMainWindow,并使用 Shiboken 生成绑定。

头文件 wrappedclasses.h 被传递给 Shiboken,它在名为 AppLib/ 的子目录中生成类包装器和头文件,这些文件链接到应用程序。

文件 pythonutils.cpp,h 包含一些代码,这些代码将 MainWindow 的实例绑定到全局 Python 命名空间 (__main___) 中一个名为 mainWindow 的变量。然后可以运行如下的 Python 脚本片段:

mainWindow.testFunction1()

触发底层的C++函数。

构建项目

这个示例可以使用CMakeQMake构建, 但有一些共同的要求你需要考虑:

  • 确保将独立的 PySide 包(与 Qt 库捆绑在一起)安装到当前活动的 Python 环境(系统或虚拟环境)中。

  • qmake 必须在你的 PATH 中:

    • 这样 CMake 的 find_package(Qt6 COMPONENTS Core) 才能工作(用于包含头文件),

    • 用于使用 qmake 而不是 CMake 构建应用程序

  • 使用与构建PySide时相同的Qt版本来构建示例应用程序,这是为了确保新生成的绑定库、PySide库和Qt库之间的二进制兼容性。

对于Windows用户,您还需要: * 在您的终端中激活一个Visual Studio环境

  • 选择了正确的Visual Studio架构(32位与64位)

  • 确保您的 Qt + Python + PySide 包 + 应用程序构建配置相同(全部为 Release,这更有可能,或者全部为 Debug)。

  • 确保您的 Qt + Python + PySide 包 + 应用程序使用兼容的 MSVC 版本构建,以避免混合使用 C++ 运行时库。

两种构建选项都将使用pyside_config.py文件来配置项目,使用当前的PySide/Shiboken安装(对于qmake通过pyside.pri,对于CMake通过项目CMakeLists.txt)。

使用CMake

要使用CMake构建此示例,您将需要最新版本的CMake(3.16+)。

你可以通过在终端中执行以下命令(根据你的文件系统布局稍作调整)来构建这个示例:

macOS/Linux:

cd ~/pyside-setup/examples/scriptableapplication
mkdir build
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja
./scriptableapplication

在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

使用QMake

文件 scriptableapplication.pro 是使用 qmake 时与示例关联的项目文件。

你可以通过执行以下命令来构建这个示例:

mkdir build
cd build
qmake ..
make # or nmake / jom for Windows

Windows 故障排除

使用qmake应该可以直接使用,之前有一个已知的问题与目录和空格有关,通过使用“~1”字符解决了这个问题,所以路径将从: c:\Program Files\Python39\libs 变为 c:\Progra~1\Python39\libs 这将避免在生成Makefiles时出现的问题。

在使用CMake时,可能会为不同的架构选择错误的编译器,但可以通过设置CC环境变量来明确解决这个问题:

set CC=cl

在命令行上传递编译器:

cmake -S.. -B. -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe

或使用 -G 选项:

cmake -S.. -B. -G "Visual Studio 14 Win64" -DCMAKE_BUILD_TYPE=Release

如果使用了-G "Visual Studio 14 Win64"选项,将会生成一个sln文件,并且可以使用MSBuild代替ninja

MSBuild scriptableapplication.sln "/p:Configuration=Release"

请注意,使用“Ninja”生成器比MSBuild更受推荐,因为在后一种情况下,可执行文件被放置在与包含依赖项dlls(shiboken,pyside)的目录不同的目录中。如果应用程序在Release子目录中启动,而不是在包含依赖项的目录中启动,这会导致执行问题。

Virtualenv 支持

如果应用程序是从一个激活了Python虚拟环境的终端启动的,那么该环境的包将用于Python模块导入过程。 在这种情况下,请确保应用程序是在virtualenv激活时构建的,以便构建系统能够获取正确的Python共享库和PySide包。

Linux共享库笔记

出于本示例的目的,我们链接到共享库的绝对路径(libshibokenlibpyside),因为模块的安装是通过 wheels 完成的,并且没有干净的解决方案可以在包中包含符号链接(以便常规的 -lshiboken 起作用)。

Windows 笔记

应用程序的构建配置(Debug 或 Release)应与 PySide6 的构建配置匹配,否则应用程序将无法正常工作。

实际上,这意味着唯一支持的配置是:

  1. 发布应用程序的配置构建 + PySide setup.py 不带 --debug 标志 + python.exe 用于 PySide 构建过程 + python39.dll 用于链接的共享 库 + Qt 的发布构建。

  2. 调试应用程序的配置构建 + PySide setup.py 使用 --debug 标志 + python_d.exe 用于 PySide 构建过程 + python39_d.dll 用于链接的共享 库 + Qt 的调试构建。

这是必要的,因为所有相关的共享库都必须链接到相同的C++运行时库(msvcrt.dllmsvcrtd.dll)。为了使示例尽可能自包含,使用的共享库(pyside6.dllshiboken6.dll)被硬链接到应用程序的构建文件夹中。

下载 这个 示例

// 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)