示例绑定示例

这个例子展示了如何为非Qt C++库生成Python绑定。

该示例定义了一个CMake项目,该项目构建了两个库:

  • libuniverse - 一个包含两个C++类的示例库。

  • Universe - 生成的Python扩展模块,包含对上述库的绑定。

项目文件的结构使得用户可以将其复制粘贴到自己的项目中,并且只需进行最少的修改即可构建。

描述

libuniverse库声明了两个类:IcecreamTruck

Icecream 对象有一个口味,以及一个用于返回口味的访问器。

Truck 实例存储了一个 Icecream 对象的向量,并且有各种方法用于添加新口味、打印可用口味、配送冰淇淋等。

从C++的角度来看,Icecream实例被视为对象类型(指针语义),因为该类声明了虚方法。

相比之下,Truck 没有定义虚方法,因此被视为值类型(复制语义)。

因为Truck是一个值类型,并且它存储了一个Icecream指针的向量,所以必须考虑五法则(实现复制构造函数、赋值运算符、移动构造函数、移动赋值运算符和析构函数)。

并且由于Icecream对象是可复制的,该类型必须定义clone()方法的实现,以避免类型切片问题。

这两种类型及其方法将通过生成CPython代码暴露给Python。代码由shiboken生成,并放置在以每个C++类型命名的单独的.cpp文件中。然后代码被编译并链接到一个共享库中。共享库是一个CPython扩展模块,由Python解释器加载。

因为C++语言与Python有不同的语义,shiboken需要帮助来弄清楚如何生成绑定代码。这是通过指定一个称为类型系统文件的特殊XML文件来完成的。

在类型系统文件中,您可以指定如下内容:

  • 哪些C++类应该具有绑定(Icecream, Truck)以及使用何种语义(值/对象)

  • 所有权规则(谁删除C++对象,C++还是Python)

  • 代码注入(适用于shiboken不知道的各种特殊情况)

  • 包名称(从Python导入的包的名称)

在这个例子中,我们将Icecream声明为对象类型,将Truck声明为值类型。clone()addIcecreamFlavor(Icecream*)在跨语言边界传递参数对象时需要额外的信息来确定谁拥有这些参数对象(在这种情况下,C++将删除这些对象)。

Truck 有用于字符串 arrivalMessage 的 getter 和 setter 方法。 在类型系统文件中,我们将其声明为 Python 中的一个属性:

<property type="std::string" name="arrivalMessage" get="getArrivalMessage" set="setArrivalMessage"/>

然后可以用更Pythonic的方式使用它:

special_truck.arrivalMessage = "A new SPECIAL icecream truck has arrived!\n"

在shiboken生成C++代码并且CMake从代码中创建扩展模块后,可以通过使用原始的C++名称导入这些类型来在Python中访问它们。

from Universe import Icecream, Truck

构建C++封装对象与在Python中相同

icecream = Icecream("vanilla")
truck = Truck()

实际的C++构造函数被映射到Python的__init__方法。

class VanillaChocolateIcecream(Icecream):
    def __init__(self, flavor=""):
        super().__init__(flavor)

C++ 方法可以作为常规的 Python 方法使用 C++ 名称访问

truck.addIcecreamFlavor(icecream)

继承的工作方式与常规的Python类相同,虚拟的C++方法可以通过在C++类中定义同名的方法来简单地覆盖。

class VanillaChocolateIcecream(Icecream):
    # ...
    def getFlavor(self):
        return "vanilla sprinked with chocolate"

main.py 脚本展示了这些类型的用法。

CMake项目文件包含许多注释,解释了所有构建规则,供对构建过程感兴趣的人参考。

构建项目

此示例只能使用CMake构建。 需要满足以下要求:

  • 一个 PySide 包已安装到当前活动的 Python 环境(系统或虚拟环境)中

  • 足够新版本的CMake(3.16+)。

  • 忍者

对于Windows用户,您还需要:

  • 在您的终端中激活一个Visual Studio环境

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

  • 确保您的Python解释器和绑定项目构建配置相同(全部为Release,这更有可能,或者全部为Debug)。

构建使用pyside_config.py文件来配置项目,使用当前的PySide/Shiboken安装。

使用CMake

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

在macOS/Linux上运行CMake:

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

在Windows上运行CMake:

cd C:\pyside-setup\examples\samplebinding
mkdir build
cd build
cmake .. -B. -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl.exe

构建:

ninja
ninja install
cd ..

使用Python模块

最终示例可以通过以下方式运行:

python main.py

main.py脚本中,从Universe模块导入类后,从Icecream派生出两种类型以表示不同的“口味”。然后,创建了一个truck来运送一些常规口味的冰淇淋和两种特殊口味的冰淇淋。

如果配送失败,将创建一个新的truck,并将旧的口味复制过来,同时添加一个新的神奇口味,确保满足所有客户的需求。

尝试运行它,看看冰淇淋是否被送达。

Windows 故障排除

有可能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"

如果使用了-G "Visual Studio 14 Win64"选项,将会生成一个sln文件,并且可以使用MSBuild而不是ninja。在这种情况下,最简单的方法是使用cmake可执行文件来构建和安装:

cmake --build . --target install --config Release

请注意,使用"Ninja"生成器比MSBuild生成器更受推荐,因为MSBuild生成器会同时生成Debug和Release的配置,如果你不小心至少一次构建了错误的配置,这可能会导致构建错误。

Virtualenv 支持

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

Linux共享库笔记

出于本示例的目的,我们链接到依赖共享库的绝对路径 libshiboken,因为库的安装是通过 wheel 完成的,并且没有干净的解决方案可以在 wheel 包中包含符号链接(以便将 -lshiboken 传递给链接器可以工作)。

Windows 笔记

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

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

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

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

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

下载 这个 示例

Shiboken生成器需要一个包含我们感兴趣类型的头文件:

// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef BINDINGS_H
#define BINDINGS_H

#include "icecream.h"
#include "truck.h"

#endif // BINDINGS_H

Shiboken需要一个基于XML的类型系统文件,该文件定义了C++和Python类型之间的关系。

它声明了上述两个类。其中一个作为“对象类型”,另一个作为“值类型”。主要区别在于,对象类型在生成的代码中作为指针传递,而值类型则是复制的(值语义)。

通过在类型系统文件中指定这些类的名称,Shiboken 会自动尝试为这些类的所有方法生成绑定。除非你想修改这些方法,否则不需要在 XML 文件中手动提及所有方法。

对象所有权规则

Shiboken 不知道是 Python 还是 C++ 负责释放 Python 代码中分配的 C++ 对象,假设这一点可能会导致错误。在某些情况下,当 Python 对象的引用计数变为零时,Python 应该释放 C++ 内存,但它不应该仅仅因为假设底层 C++ 库不会删除它,或者它可能被父对象(如 QWidgets)所拥有,就删除底层的 C++ 对象。

在我们的案例中,clone() 方法仅在 C++ 库内部调用, 我们假设 C++ 代码负责释放克隆的对象。

至于addIcecreamFlavor(),我们知道Truck拥有Icecream对象,并且一旦Truck被销毁,它将被移除。这就是为什么在类型系统文件中将所有权设置为“c++”,以便在相应的Python名称超出范围时不会删除C++对象。

<?xml version="1.0"?>
<!--
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
-->
<typesystem package="Universe">

    <object-type name="Icecream">
        <!-- By default the ownership of an object created in Python is tied
             to the Python name pointing to it. In order for the underlying
             C++ object not to get deleted when the Python name goes out of
             scope, we have to transfer ownership to C++.
             -->
        <modify-function signature="clone()">
            <modify-argument index="0">
                <define-ownership owner="c++"/>
            </modify-argument>
        </modify-function>
    </object-type>

    <value-type name="Truck">
        <!-- Same ownership caveat applies here. -->
        <property type="std::string" name="arrivalMessage" get="getArrivalMessage" set="setArrivalMessage"/>
        <modify-function signature="addIcecreamFlavor(Icecream*)">
            <modify-argument index="1">
                <define-ownership owner="c++"/>
            </modify-argument>
        </modify-function>
    </value-type>

</typesystem>
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include "icecream.h"

#include <iostream>

Icecream::Icecream(const std::string &flavor) : m_flavor(flavor) {}

Icecream::~Icecream() = default;

std::string Icecream::getFlavor() const
{
    return m_flavor;
}

Icecream *Icecream::clone()
{
    return new Icecream(*this);
}

std::ostream &operator<<(std::ostream &str, const Icecream &i)
{
    str << i.getFlavor();
    return str;
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef ICECREAM_H
#define ICECREAM_H

#include "macros.h"

#include <iosfwd>
#include <string>

class BINDINGS_API Icecream
{
public:
    explicit Icecream(const std::string &flavor);
    virtual Icecream *clone();
    virtual ~Icecream();
    virtual std::string getFlavor() const;

private:
    std::string m_flavor;
};

std::ostream &operator<<(std::ostream &str, const Icecream &i);

#endif // ICECREAM_H
// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef MACROS_H
#define MACROS_H

#if defined _WIN32 || defined __CYGWIN__
    // Export symbols when creating .dll and .lib, and import them when using .lib.
    #if BINDINGS_BUILD
        #define BINDINGS_API __declspec(dllexport)
    #else
        #define BINDINGS_API __declspec(dllimport)
    #endif
    // Disable warnings about exporting STL types being a bad idea. Don't use this in production
    // code.
    #pragma warning( disable : 4251 )
#else
    #define BINDINGS_API
#endif

#endif // MACROS_H
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""An example showcasing how to use bindings for a custom non-Qt C++ library"""

from Universe import Icecream, Truck


class VanillaChocolateIcecream(Icecream):
    def __init__(self, flavor=""):
        super().__init__(flavor)

    def clone(self):
        return VanillaChocolateIcecream(self.getFlavor())

    def getFlavor(self):
        return "vanilla sprinked with chocolate"


class VanillaChocolateCherryIcecream(VanillaChocolateIcecream):
    def __init__(self, flavor=""):
        super().__init__(flavor)

    def clone(self):
        return VanillaChocolateCherryIcecream(self.getFlavor())

    def getFlavor(self):
        base_flavor = super(VanillaChocolateCherryIcecream, self).getFlavor()
        return f"{base_flavor} and a cherry"


if __name__ == '__main__':
    leave_on_destruction = True
    truck = Truck(leave_on_destruction)

    flavors = ["vanilla", "chocolate", "strawberry"]
    for f in flavors:
        icecream = Icecream(f)
        truck.addIcecreamFlavor(icecream)

    truck.addIcecreamFlavor(VanillaChocolateIcecream())
    truck.addIcecreamFlavor(VanillaChocolateCherryIcecream())

    truck.arrive()
    truck.printAvailableFlavors()
    result = truck.deliver()

    if result:
        print("All the kids got some icecream!")
    else:
        print("Aww, someone didn't get the flavor they wanted...")

    if not result:
        special_truck = Truck(truck)
        del truck

        print("")
        special_truck.arrivalMessage = "A new SPECIAL icecream truck has arrived!\n"
        special_truck.arrive()
        special_truck.addIcecreamFlavor(Icecream("SPECIAL *magical* icecream"))
        special_truck.printAvailableFlavors()
        special_truck.deliver()
        print("Now everyone got the flavor they wanted!")
        special_truck.leave()
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#include <iostream>
#include <random>

#include "truck.h"

Truck::Truck(bool leaveOnDestruction) : m_leaveOnDestruction(leaveOnDestruction) {}

Truck::Truck(const Truck &other)
{
    assign(other);
}

Truck &Truck::operator=(const Truck &other)
{
    if (this != &other) {
        m_flavors.clear();
        assign(other);
    }
    return *this;
}

Truck::Truck(Truck &&other) = default;

Truck& Truck::operator=(Truck &&other) = default;

Truck::~Truck()
{
    if (m_leaveOnDestruction)
        leave();
}

void Truck::addIcecreamFlavor(Icecream *icecream)
{
    m_flavors.push_back(IcecreamPtr(icecream));
}

void Truck::printAvailableFlavors() const
{
    std::cout << "It sells the following flavors: \n";
    for (const auto &flavor : m_flavors)
        std::cout << "  * "  << *flavor << '\n';
    std::cout << '\n';
}

void Truck::arrive() const
{
    std::cout << m_arrivalMessage;
}

void Truck::leave() const
{
    std::cout << "The truck left the neighborhood.\n";
}

void Truck::setLeaveOnDestruction(bool value)
{
    m_leaveOnDestruction = value;
}

void Truck::setArrivalMessage(const std::string &message)
{
    m_arrivalMessage = message;
}

std::string Truck::getArrivalMessage() const
{
    return m_arrivalMessage;
}

void Truck::assign(const Truck &other)
{
    m_flavors.reserve(other.m_flavors.size());
    for (const auto &f : other.m_flavors)
        m_flavors.push_back(IcecreamPtr(f->clone()));
}

bool Truck::deliver() const
{
    std::random_device rd;
    std::mt19937 mt(rd());
    std::uniform_int_distribution<int> dist(1, 2);

    std::cout << "The truck started delivering icecream to all the kids in the neighborhood.\n";
    bool result = false;

    if (dist(mt) == 2)
        result = true;

    return result;
}
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause

#ifndef TRUCK_H
#define TRUCK_H

#include "icecream.h"
#include "macros.h"

#include <memory>
#include <vector>

class BINDINGS_API Truck
{
public:
    explicit Truck(bool leaveOnDestruction = false);
    Truck(const Truck &other);
    Truck& operator=(const Truck &other);
    Truck(Truck &&other);
    Truck& operator=(Truck &&other);

    ~Truck();

    void addIcecreamFlavor(Icecream *icecream);
    void printAvailableFlavors() const;

    bool deliver() const;
    void arrive() const;
    void leave() const;

    void setLeaveOnDestruction(bool value);

    void setArrivalMessage(const std::string &message);
    std::string getArrivalMessage() const;

private:
    using IcecreamPtr = std::shared_ptr<Icecream>;

    void assign(const Truck &other);

    bool m_leaveOnDestruction = false;
    std::string m_arrivalMessage = "A new icecream truck has arrived!\n";
    std::vector<IcecreamPtr> m_flavors;
};

#endif // TRUCK_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 not use RPATH settings for install_name on macOS.
if(POLICY CMP0068)
  cmake_policy(SET CMP0068 NEW)
endif()

# Consider changing the project name to something relevant for you.
project(SampleBinding)

# ================================ General configuration ======================================

# Set CPP standard to C++17 minimum.
set(CMAKE_CXX_STANDARD 17)

# The sample library for which we will create bindings. You can change the name to something
# relevant for your project.
set(sample_library "libuniverse")

# The name of the generated bindings module (as imported in Python). You can change the name
# to something relevant for your project.
set(bindings_library "Universe")

# The header file with all the types and functions for which bindings will be generated.
# Usually it simply includes other headers of the library you are creating bindings for.
set(wrapped_header ${CMAKE_SOURCE_DIR}/bindings.h)

# The typesystem xml file which defines the relationships between the C++ types / functions
# and the corresponding Python equivalents.
set(typesystem_file ${CMAKE_SOURCE_DIR}/bindings.xml)

# Specify which C++ files will be generated by shiboken. This includes the module wrapper
# and a '.cpp' file per C++ type. These are needed for generating the module shared
# library.
set(generated_sources
    ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/universe_module_wrapper.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/icecream_wrapper.cpp
    ${CMAKE_CURRENT_BINARY_DIR}/${bindings_library}/truck_wrapper.cpp)


# ================================== Shiboken detection ======================================
# 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 and paths.
# Uses the not entirely supported utils/pyside_config.py file.
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 shiboken generator 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(--python-include-path python_include_dir)
pyside_config(--shiboken-generator-include-path shiboken_include_dir 1)
pyside_config(--shiboken-module-shared-libraries-cmake shiboken_shared_libraries 0)
pyside_config(--python-link-flags-cmake python_linking_data 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()


# ==================================== RPATH configuration ====================================


# =============================================================================================
# !!! (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 built shared libraries find their dependencies.
set(CMAKE_SKIP_BUILD_RPATH FALSE)
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH ${shiboken_module_path} ${CMAKE_CURRENT_SOURCE_DIR})
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================


# =============================== CMake target - sample_library ===============================


# Define the sample shared library for which we will create bindings.
set(${sample_library}_sources icecream.cpp truck.cpp)
add_library(${sample_library} SHARED ${${sample_library}_sources})
set_property(TARGET ${sample_library} PROPERTY PREFIX "")

# Needed mostly on Windows to export symbols, and create a .lib file, otherwise the binding
# library can't link to the sample library.
target_compile_definitions(${sample_library} PRIVATE BINDINGS_BUILD)


# ====================== Shiboken target for generating binding C++ files  ====================


# Set up the options to pass to shiboken.
set(shiboken_options --generator-set=shiboken --enable-parent-ctor-heuristic
    --enable-return-value-heuristic --use-isnull-as-nb-bool
    --avoid-protected-hack
    -I${CMAKE_SOURCE_DIR}
    -T${CMAKE_SOURCE_DIR}
    --output-directory=${CMAKE_CURRENT_BINARY_DIR}
    )

set(generated_sources_dependencies ${wrapped_header} ${typesystem_file})

# Add custom target to run shiboken to generate the binding cpp files.
add_custom_command(OUTPUT ${generated_sources}
                    COMMAND ${shiboken_path}
                    ${shiboken_options} ${wrapped_header} ${typesystem_file}
                    DEPENDS ${generated_sources_dependencies}
                    IMPLICIT_DEPENDS CXX ${wrapped_header}
                    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
                    COMMENT "Running generator for ${typesystem_file}.")


# =============================== CMake target - bindings_library =============================


# Set the cpp files which will be used for the bindings library.
set(${bindings_library}_sources ${generated_sources})

# Define and build the bindings library.
add_library(${bindings_library} MODULE ${${bindings_library}_sources})

# Apply relevant include and link flags.
target_include_directories(${bindings_library} PRIVATE ${python_include_dir})
target_include_directories(${bindings_library} PRIVATE ${shiboken_include_dir})
target_include_directories(${bindings_library} PRIVATE ${CMAKE_SOURCE_DIR})

target_link_libraries(${bindings_library} PRIVATE ${shiboken_shared_libraries})
target_link_libraries(${bindings_library} PRIVATE ${sample_library})

# Adjust the name of generated module.
set_property(TARGET ${bindings_library} PROPERTY PREFIX "")
set_property(TARGET ${bindings_library} PROPERTY OUTPUT_NAME
             "${bindings_library}${PYTHON_EXTENSION_SUFFIX}")
if(WIN32)
    if("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
        set_property(TARGET ${bindings_library} PROPERTY SUFFIX "_d.pyd")
    else()
        set_property(TARGET ${bindings_library} PROPERTY SUFFIX ".pyd")
    endif()
endif()

# Make sure the linker doesn't complain about not finding Python symbols on macOS.
if(APPLE)
  set_target_properties(${bindings_library} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif(APPLE)

# Find and link to the python import library only on Windows.
# On Linux and macOS, the undefined symbols will get resolved by the dynamic linker
# (the symbols will be picked up in the Python executable).
if (WIN32)
    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(${bindings_library} PRIVATE ${python_link_flags})
endif()


# ================================= Dubious deployment section ================================

set(windows_shiboken_shared_libraries)

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(${bindings_library}
                           PROPERTIES LINK_FLAGS "${python_additional_link_flags}")

    # Compile a list of shiboken shared libraries to be installed, so that
    # the user doesn't have to set the PATH manually to point to the PySide6 package.
    foreach(library_path ${shiboken_shared_libraries})
        string(REGEX REPLACE ".lib$" ".dll" library_path ${library_path})
        file(TO_CMAKE_PATH ${library_path} library_path)
        list(APPEND windows_shiboken_shared_libraries "${library_path}")
    endforeach()
    # =========================================================================================
    # !!! End of dubious section.
    # =========================================================================================
endif()

# =============================================================================================
# !!! (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).
# =============================================================================================
# Install the library and the bindings module into the source folder near the main.py file, so
# that the Python interpeter successfully imports the used module.
install(TARGETS ${bindings_library} ${sample_library}
        LIBRARY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
        RUNTIME DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}
        )
install(FILES ${windows_shiboken_shared_libraries} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR})
# =============================================================================================
# !!! End of dubious section.
# =============================================================================================
# 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)