Python加密方案

Pyside6软件加密方案

​ 针对Python开发的PySide6应用进行封装,并进行加密。目标是把应用封装成可执行文件,并让反向工程变得尽可能困难/昂贵**。**任何本地分发的软件都不可能做到“绝对不可逆向”,只能提高门槛、增加成本和耗时。由于目标客户Windows平台不支持联网,因此敏感的许可证以及密钥逻辑判断都不支持服务器端授权验证以及token校验。

1 针对软件反逆向加密

1.1 加密逻辑

  • 核心逻辑拆分 — 把敏感/关键算法、许可证检查、秘钥、设备认证逻辑放到单独模块或单独包里,便于单独编译为二进制扩展或单独保护。
  • 用编译器把关键模块变成二进制扩展 — 用 CythonNuitka 将关键模块编译为 .pyd,比纯 Python 字节码难逆向得多。
  • 整体打包为单文件/目录 — 用 NuitkaPyInstaller / PyOxidizer 生成最终可执行文件
  • 对 Python 代码进行混淆/加壳 — 对剩余 Python 代码用 PyArmor 等工具混淆;把资源/配置做加密并在运行时解密。
  • 资源保护 & 完整性校验 — 把配置等资源进行 AES 加密存储,运行时解密。在程序启动时(在 .pyd 内)检查 sys.executable(主 exe)和若干关键 .pyd 的 SHA256;若不匹配则拒绝运行。
  • 反调试/抗注入(谨慎使用) — 能增加逆向复杂度,但容易引起误报或稳定性问题。

1.2 加密风险

  • 修改/补丁程序:攻击者直接修改 exe / .pyd,修改许可证校验函数总是返回 True,或跳过某些判定。
  • 替换公钥或校验逻辑:如果公钥是个外部文件或可写位置,攻击者把公钥替换为自己控制的公钥,再用自己的私钥签发 license。
  • 内存补丁 / 动态劫持:运行时用调试器 / 注入修改进程内验证结果。
  • 重放/替换元数据:删除/修改 time-meta 或 license_meta,使时间回退检测失效。
  • 提取公钥与反向分析:能提取公钥本身,但公钥公开并不能伪造签名。主要风险是反向分析后去掉验证逻辑。

2 针对用户使用权限加密

​ 不联网的 Windows 环境下做本地授权管理,包括使用期限限制(到期失效)、设备码绑定(只能在指定机器运行)、硬件加密狗 / U 盘绑定等限制方式。

2.1 使用期限限制

实现思路
  • 软件启动时,读取系统时间(datetime.now())。
  • 结合配置文件或编译时内置的到期日期,判断是否超期。
  • 为了防止用户改系统时间,可以记录首次运行时间到本地隐藏文件/注册表、每次运行检查「当前时间 ≥ 首次运行时间」,并且如果时间倒退(说明用户篡改系统时间),直接报错拒绝运行。

2.2 设备码限制

实现思路
  • 读取设备的唯一硬件 ID(CPU 序列号 / 主板序列号 / 硬盘序列号 / MAC 地址等)。(注意涉及到联网用户的设备码变更问题)
  • 使用 hash 算法生成「设备码」,在交付时生成一个许可证文件(含设备码 + 签名)。
  • 软件启动时比对本机设备码与许可证文件是否一致。

此方式需要用户配合生成设备码,即需要现在用户Windows平台上运行设备码生成脚本,将得到的设备码利用私钥加密后写入文件,后在软件运行时进行license校验。

2.3 第三方加密狗

实现思路
  • 利用硬件厂商提供的 API 进行加密狗验证。
  • 运行时软件会检测加密狗是否插入、是否有合法授权。
  • 即使用户复制 exe,没有加密狗也不能运行。

目前基本存在两种加密方式,一个为调用加密狗的API接口库,第二个为对封装好的可执行文件加壳,两种方式可同时使用。

3 实现方法总结

  • 针对软件的反逆向,用 Cython 把 application.pymodules/utils/ 等核心模块编译成 .pyd。写一个 run.py 作为启动入口,并用 Nuitka或 PyInstaller 打包成单 exe,最后用 pyarmor 混淆剩余 .py 文件。打包好的 exe + config/data 资源文件 = 可交付版本,
  • 若想限制用户的使用权限,可使用私钥分发许可证(但需要使用者配合提供设备码)
  • 若想要更安全或成本支持,可选择加密狗。
  • 目前任何不联网的本地分发软件都不可能做到“绝对不可逆向”。

4 关键实现步骤

4.1 pyd是什么

  • .pyd = Windows 平台下的 Python 动态链接库,本质和 .dll 一样。

  • Python 导入 .pyd 的方式和 .py 完全相同:

    import utils.data.xxxxxxxxx

    不需要改调用代码。

哪些要编译成 .pyd?

  • 建议:只把 核心逻辑/算法/关键验证.py 转换为 .pyd
  • 比如 utils/data/*.py, utils/xxx/*.py, modules/*.py, 以及 application.py(主入口可以也编译)。
  • 资源文件(csv/json/icon/ui 等)不要编进去,否则打包麻烦。

对应目录里生成 .pyd 文件(例如 application.cp312-win_amd64.pyd)。

  • .py 可以删掉或只留一个空壳(避免泄漏源码)。
  • 其它文件导入 .pyd 时方式不变。

4.2 主程序调用

  • 如果 application.py 也编译成 .pyd,就不能直接 python application.py 启动了。
  • 解决方法:写一个很小的 run.py 作为入口:
import application

if __name__ == "__main__":
    application.main()   # 这里调用主入口函数
  • 这样实际运行的是编译过的 application.pyd,而源码不会暴露。

4.3 封装成 exe

两种方式:

  • PyInstaller(常用,兼容性最好)

    pyinstaller --noconfirm --onefile --windowed --icon=config/icon.ico run.py
  • Nuitka(更难逆向,推荐)

    nuitka --standalone --onefile --enable-plugin=pyside6 run.py
  • 打包时 .pyd 会和 exe 一起放进 dist 目录,最终用户无法直接反编译出 py 源码。

4.4 设备码RSA加密

获取硬件 ID 的方法(Windows)

import subprocess

def get_cpu_id():
    cmd = 'wmic cpu get ProcessorId'
    return subprocess.check_output(cmd, shell=True).decode().split("\n")[1].strip()

def get_disk_serial():
    cmd = 'wmic diskdrive get SerialNumber'
    return subprocess.check_output(cmd, shell=True).decode().split("\n")[1].strip()

可以组合多个硬件信息(如 CPU+主板+硬盘),并对其做 SHA256,减少伪造风险。由于实际运行时设备无法联网,因此仅根据硬盘、CPU、主板等硬件信息进行运算即可,以防止由于动态IP变化导致设备码变化的情况。

4.5 私钥签发 license

生成 RSA 密钥(一次性),并用私钥对 license JSON 签名,输出 license.jsoncryptography(签名/验证),可通过 pip install cryptography 安装。

# license_generator.py
# 运行环境: Windows 或其他平台 (用于生成签名的离线工具)
# 依赖: pip install cryptography

import json, base64, datetime
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.backends import default_backend
import argparse
from pathlib import Path

def gen_rsa_keys(priv_path="private_key.pem", pub_path="public_key.pem", bits=2048):
    priv = rsa.generate_private_key(public_exponent=65537, key_size=bits, backend=default_backend())
    priv_bytes = priv.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    pub = priv.public_key()
    pub_bytes = pub.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    Path(priv_path).write_bytes(priv_bytes)
    Path(pub_path).write_bytes(pub_bytes)
    print(f"Generated keys: {priv_path}, {pub_path}")

def sign_license(priv_key_path, license_data: dict):
    # canonical JSON: sorted keys, no whitespace
    message = json.dumps(license_data, separators=(",", ":"), sort_keys=True).encode("utf-8")
    with open(priv_key_path, "rb") as f:
        priv = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
    sig = priv.sign(message, padding.PKCS1v15(), hashes.SHA256())
    return base64.b64encode(sig).decode("ascii")

def create_license_file(priv_key_path, out_path, device_code, expire_date_str, extra=None):
    # expire_date_str: "YYYY-MM-DD"
    now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
    license_data = {
        "device": device_code,
        "expire": expire_date_str,
        "issued": now,
    }
    if extra:
        license_data["extra"] = extra
    signature = sign_license(priv_key_path, license_data)
    license_data["signature"] = signature
    Path(out_path).write_text(json.dumps(license_data, indent=2, ensure_ascii=False))
    print(f"License written to {out_path}")

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--gen-keys", action="store_true", help="Generate RSA keypair")
    parser.add_argument("--priv", default="private_key.pem")
    parser.add_argument("--pub", default="public_key.pem")
    parser.add_argument("--create-license", action="store_true")
    parser.add_argument("--device", help="Device code (设备码,hex)")
    parser.add_argument("--expire", help="Expire date YYYY-MM-DD")
    parser.add_argument("--out", default="license.json")
    args = parser.parse_args()

    if args.gen_keys:
        gen_rsa_keys(args.priv, args.pub)
    if args.create_license:
        if not args.device or not args.expire:
            raise SystemExit("请提供 --device 和 --expire")
        create_license_file(args.priv, args.out, args.device, args.expire)

给客户签发 license:

python license_generator.py --create-license --priv my_priv.pem --device A1B2C3D4E5 --expire 2025-12-31 --out license_for_customer.json

license_for_customer.json 交给客户(连同可执行文件和公钥已经嵌入的程序)。

4.6 公钥验证 license

在程序启动时调用 check_license(),完成:设备码比对、签名验证、到期验证、时间回退检测(首次运行/上次运行记录)。cryptography。若做成 .pyd,把这个模块编译进二进制将更安全。

# license_checker.py
# 应放入主程序中(或编译为 .pyd 嵌入)
import json, base64, hashlib, subprocess, datetime, os, ctypes
from pathlib import Path
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend

# 配置项(按需调整)
APP_NAME = "MyApp"
LICENSE_PATHS = [
    Path(os.getenv("PROGRAMDATA", "C:\\ProgramData")) / APP_NAME / "license.json",
    Path.cwd() / "license.json"
]
META_STORE = Path(os.getenv("PROGRAMDATA", "C:\\ProgramData")) / APP_NAME / "license_meta.json"
# 把公钥嵌入此文件(或放到已编译的 .pyd)
PUBLIC_KEY_PEM = b"""-----BEGIN PUBLIC KEY-----
... 把你生成的 public_key.pem 内容粘贴到这里 ...
-----END PUBLIC KEY-----
"""

# ---------------- 设备码生成 ----------------
def _run_cmd(cmd):
    try:
        out = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL)
        return out.decode().strip().splitlines()
    except Exception:
        return []

def get_cpu_id():
    lines = _run_cmd('wmic cpu get ProcessorId')
    if len(lines) >= 2:
        return lines[1].strip()
    return ""

def get_mb_serial():
    lines = _run_cmd('wmic baseboard get SerialNumber')
    if len(lines) >= 2:
        return lines[1].strip()
    return ""

def get_disk_serial():
    lines = _run_cmd('wmic diskdrive get SerialNumber')
    if len(lines) >= 2:
        # 取第一个非空项
        for l in lines[1:]:
            s = l.strip()
            if s:
                return s
    return ""

def get_mac():
    # 备用:取第一个物理 MAC
    try:
        import uuid
        mac = uuid.getnode()
        return format(mac, '012x')
    except:
        return ""

def make_device_code():
    parts = []
    for f in (get_cpu_id, get_mb_serial, get_disk_serial, get_mac):
        try:
            v = f() or ""
            parts.append(v)
        except Exception:
            parts.append("")
    raw = "|".join(parts)
    h = hashlib.sha256(raw.encode("utf-8")).hexdigest().upper()
    # 取前16或其它长度作为设备码
    return h[:32]  # 32 hex chars

# ----------------- 签名验证 -----------------
def load_public_key(pem_bytes):
    return serialization.load_pem_public_key(pem_bytes, backend=default_backend())

def verify_signature(pubkey, license_dict):
    sig_b64 = license_dict.get("signature", "")
    if not sig_b64:
        return False
    sig = base64.b64decode(sig_b64)
    # 构造签名原文(canonical JSON)
    payload = {k: v for k, v in license_dict.items() if k != "signature"}
    message = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8")
    try:
        pubkey.verify(sig, message, padding.PKCS1v15(), hashes.SHA256())
        return True
    except Exception:
        return False

# ----------------- 时间回退与元数据保护 -----------------
def _ensure_meta_dir():
    META_STORE.parent.mkdir(parents=True, exist_ok=True)

def _read_meta():
    _ensure_meta_dir()
    if META_STORE.exists():
        try:
            return json.loads(META_STORE.read_text(encoding="utf-8"))
        except Exception:
            return {}
    return {}

def _write_meta(d):
    _ensure_meta_dir()
    META_STORE.write_text(json.dumps(d), encoding="utf-8")

def is_time_rollback(now_date, meta):
    # meta: {first_run: "YYYY-MM-DD", last_run: "YYYY-MM-DD"}
    if "first_run" in meta:
        first = datetime.date.fromisoformat(meta["first_run"])
        if now_date < first:
            return True
    if "last_run" in meta:
        last = datetime.date.fromisoformat(meta["last_run"])
        if now_date < last:
            return True
    return False

def update_meta(now_date, meta):
    if "first_run" not in meta:
        meta["first_run"] = now_date.isoformat()
    meta["last_run"] = now_date.isoformat()
    _write_meta(meta)

# ----------------- 主检查函数 -----------------
class LicenseError(Exception):
    pass

def find_license_file():
    for p in LICENSE_PATHS:
        if p.exists():
            try:
                return p
            except:
                pass
    return None

def check_license():
    # 1. 读取 license 文件
    lic_path = find_license_file()
    if lic_path is None:
        raise LicenseError("未找到 license 文件")
    lic = json.loads(lic_path.read_text(encoding="utf-8"))

    # 2. 验证签名
    pubkey = load_public_key(PUBLIC_KEY_PEM)
    if not verify_signature(pubkey, lic):
        raise LicenseError("license 签名无效或已被篡改")

    # 3. 设备码验证(可选,若 license.device == "*" 表示不绑定)
    device_code_local = make_device_code()
    device_expected = lic.get("device", "")
    if device_expected and device_expected != "*" and device_expected.upper() != device_code_local.upper():
        raise LicenseError("设备与 license 不匹配")

    # 4. 到期验证
    expire = lic.get("expire", "")
    if expire:
        today = datetime.date.today()
        exp_date = datetime.date.fromisoformat(expire)
        if today > exp_date:
            raise LicenseError("license 已过期")

    # 5. 时间回退检测(防止修改系统时间)
    now_date = datetime.date.today()
    meta = _read_meta()
    if is_time_rollback(now_date, meta):
        raise LicenseError("检测到系统时间回退,拒绝运行")
    update_meta(now_date, meta)

    # 6. 可选:额外字段(例如并发数/功能开关)
    # extra = lic.get("extra", {})

    # 全部通过
    return True

# 方便嵌入应用(抛出异常则拒绝运行)
if __name__ == "__main__":
    try:
        ok = check_license()
        print("License OK:", ok)
    except Exception as e:
        print("License check failed:", e)

license_checker.py 放入项目(例如 modules/license_checker.py),并将 PUBLIC_KEY_PEM 替换为厂商公钥文本。在 application.py 启动入口处最开始调用:

from modules.license_checker import check_license, LicenseError

try:
    check_license()
except LicenseError as e:
    # 提示用户、写日志并退出
    print("License 错误:", e)
    sys.exit(1)

把敏感的 license_checker.py 编译成 .pyd(Cython)或把整个 modules/utils 编译,最后用 Nuitka/PyInstaller 打包为 exe。将 license.json 放在安装目录或 %ProgramData%\MyApp\license.json。你可以选择把 LICENSE_PATHS 修改成你希望的多个查找路径(例如注册表、隐藏分区等)

4.7 自校验完整性

在程序启动时(在 .pyd 内)检查 sys.executable(主 exe)和若干关键 .pyd 的 SHA256;若不匹配则拒绝运行。在源码模板里放占位符 __EXPECTED_HASHES__,构建脚本计算哈希并把真实哈希写入模板生成 license_checker.pyx,再 cythonize。攻击者如果篡改 exe 或关键 .pyd,会触发校验失败。本地完整性校验能显著提高逆向/篡改成本,但不能做到绝对防护;高级攻击者可通过内存补丁、在运行时跳过检查或替换 .pyd 来绕过。建议把校验逻辑也做混淆/编译,并把公钥等敏感信息嵌入 native 二进制。

因为 EXPECTED_HASHES 包含主 exe 的哈希,因此需要先获得最终 exe(或在构建中按阶段生成 exe),才能计算 exe 的哈希并写入 .pyx。常见流程是:

  1. 先生成 exe(例如 PyInstaller 输出到 dist\MyApp.exe)。
  2. 运行 build_license_checker.py --exe dist\MyApp.exe --targets ... 生成 license_checker.pyx
  3. cythonize 生成 license_checker.pyd
  4. license_checker.pyd 放回 dist 并替换,同时把 license_checker.pyd 也打包入最终 installer(或用一条额外命令把 dist 里文件重新打包为最终分发文件)。

如果用 Nuitka 直接把 Python 转成 exe(且包含了模块),则需要在 Nuitka 打包前把 .pyd 生成好并放到项目中,Nuitka 会把它一起打包进去。流程上要确保 license_checker.pydexe 的哈希一致 — 因此通常的做法是先构建 exe(不把 license_checker 校验的哈希硬编码),然后“固化哈希并编译 license_checker”,然后把 license_checker.pyd 包回到最终发行包里。

4.8 加密狗

把加密狗接入进来能够显著提升离线授权的安全性。

  1. 物理强绑定:要求运行时必须插入加密狗,否则拒绝运行。
  2. 狗内存储/验证:把关键授权数据(设备 ID / 许可证数据 / 到期时间 / 使用次数)写入狗内部(加密),并在运行时用狗自身的 crypto / clock / counter 进行验证,而不是把全部数据放在可替换的 license.json。
  3. 狗 + 本地 license 双校验(可选):同时校验狗与本地签名 license,增加攻击成本。
  4. 把验证逻辑编译为 native(.pyd / Nuitka),并做完整性校验,防止被直接跳过

由于硬件加密狗在购买时将提供使用文档,在此不做介绍,仅作提示:

  • 尽早校验:在 GUI 显示之前,先做狗检测与验证。若失败,用 QMessageBoxMessageBoxW 弹窗提示并提供“打开 license 目录 / 联系客服 / 硬件售后”选项。
  • 错误处理:对常见错误给出明确提示(未检测到狗 / 狗未登录 / license 过期 / 本机设备不匹配 / 计数器为 0)。记录日志(建议放在 %ProgramData%\MyApp\license_error.log)。
  • 降级策略:若希望在某些特殊客户允许有限降级(比如测试模式),内置“应急授权码”机制(仅在非常受控情况下使用,并嵌入在你保管的私钥中)。
  • 找不到 DLL:打包后 DLL 未被包含,导致 ImportError。把厂商 DLL 放到项目根或用 nuitka 的 --include-plugin-directory / PyInstaller 的 --add-binary 明确包含。
  • 权限问题:写入狗/管理需要管理员权限,Provisioning 工具应以管理员运行。运行时只需用户登录/普通权限即可读取/使用(取决厂商实现)。
  • 串口/USB 热插拔:若用户在运行时拔掉狗,应检测并给出友好提示。
  • 加密算法与密钥管理:千万不要把狗的管理员密码/ApiKey 写入分发包;ApiKey 由厂商工具生成并设置在狗上。
  • 如果预算允许且你想强保护:把授权写入狗(发货时 Provision),程序启动仅通过狗内数据校验与狗内时钟判断到期。把校验逻辑编译成 .pyd 并做完整性校验。
  • 预算中等/兼顾 UX:采用狗 + 已签名本地 license 的混合模式(用 challenge-response 增强),并在程序 UI 提供清晰的“未检测到狗 / 插入狗后重试”的提示。
  • 实施优先级:先把狗检测/登录/读取/时间读取的代码做通,再做写入/加密与挑战-应答流程,最后把这些逻辑编译为 native 并加入完整性校验。

4.9 Challenge-Response

启动时做一个 challenge-response 流程:

  1. 主机调用 ViKeyRandom 得到随机 challenge。
  2. 主机把 challenge 传到狗内,狗用内置密钥对 challenge 做 MAC(或用狗内的私钥做签名/加密),返回结果。
  3. 主机验证返回值是否与预期(使用公钥或本地算法验证)。
  4. 这样即使攻击者拷贝了狗内的静态数据,无法用静态 blob 通用地通过 challenge-response。

示例伪码(借用 ViKeyRandom):

rnd = (c_uint * 4)()
ViKeyRandom(index, byref(rnd[0]), byref(rnd[1]), byref(rnd[2]), byref(rnd[3]))
# 把 rnd 打包后调用狗内 HMAC 接口或让狗返回签名并验证

5 实现注意事项

5.1 Nuitka封装

Nuitka追求速度,且不被反编译直接看到源码(优点:性能更快;缺点:打包资料少,成功率低,需要一些稳定的版本)Nuitka打包需要c环境,mingw64版本要求比较严格,如果不知道该如何下载合适的版本,使用Nuitka自带的源即可。

5.2 打包成功日志

(base) PS C:\Users\users\Desktop\project> python -m nuitka --standalone --mingw64 --enable-plugins=pyside6 --remove-output --output-dir=o application.py
Nuitka-Options: Used command line options:
Nuitka-Options:   --standalone --mingw64 --enable-plugins=pyside6 --remove-output --output-dir=o application.py
Nuitka: Starting Python compilation with:
Nuitka:   Version '2.7.16' on Python 3.12 (flavor 'Anaconda Python') commercial grade 'not installed'.
Nuitka-Plugins:pyside6: Unwanted import of 'PyQt5' that conflicts with 'PySide6' encountered, preventing its inclusion. As a result an "ImportError" might be given at run time. Uninstall the module
Nuitka-Plugins:pyside6: it for fully compatible behavior with the uncompiled code.
Nuitka-Plugins:pyside6: Unwanted import of 'PyQt6' that conflicts with 'PySide6' encountered, preventing its inclusion. As a result an "ImportError" might be given at run time. Uninstall the module
Nuitka-Plugins:pyside6: it for fully compatible behavior with the uncompiled code.
Nuitka-Plugins:anti-bloat: Not including 'pandas.core._numba.extensions' automatically in order to avoid bloat, but this may cause: no numba acceleration.
Nuitka-Plugins:matplotlib: Using configuration file or default backend 'qtagg'.
Nuitka: Completed Python level compilation and optimization.
Nuitka: Generating source code for C backend compiler.
Nuitka: Running data composer tool for optimal constant value handling.                                                   
Nuitka: Running C compilation via Scons.
Nuitka-Scons: Non downloaded winlibs-gcc 'C:\Users\users\Programs\mingw64\mingw64\bin\gcc.exe' is being ignored, Nuitka is very dependent on the precise one.
Nuitka-Scons: Backend C compiler: gcc (gcc 14.2.0).
Nuitka-Scons: Backend C linking with 3010 files (no progress information available for this stage).
Nuitka-Scons: Compiled 2984 C files using ccache.
Nuitka-Scons: Cached C files (using ccache) with result 'cache hit': 2982
Nuitka-Scons: Cached C files (using ccache) with result 'cache miss': 2
Nuitka-Plugins:pyside6: Including Qt plugins 'iconengines,imageformats,platforms,styles,tls' below 'PySide6\qt-plugins'.
Nuitka-Plugins:dll-files: Found 2 files DLLs from cv2 installation.
Nuitka-Plugins:dll-files: Found 29 files DLLs from mkl installation.
Nuitka-Plugins:dll-files: Found 29 files DLLs from numpy installation.
Nuitka-Plugins:dll-files: Found 4 files DLLs from shiboken6 installation.
Nuitka-Plugins:data-files: Included 803 data files due to package data directory 'locale-data' for 'babel'.                                 
Nuitka-Plugins:data-files: Included data file 'babel\global.dat' due to package data for 'babel'.
Nuitka-Plugins:data-files: Included 1656 data files due to package data directory 'data' for 'botocore'.
Nuitka-Plugins:data-files: Included data file 'botocore\cacert.pem' due to package data for 'botocore'.
Nuitka-Plugins:data-files: Included data file 'certifi\cacert.pem' due to package data for 'certifi'.
Nuitka-Plugins:data-files: Included data file 'jaraco\text\Lorem ipsum.txt' due to package data for 'jaraco.text'.
Nuitka-Plugins:matplotlib: Included 182 data files due to package data for 'matplotlib.
Nuitka-Plugins:matplotlib: Included data file 'matplotlib\mpl-data\matplotlibrc' due to updated matplotlib config file with backend to use.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\html.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\html_style.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\html_table.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\latex.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\latex_longtable.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\latex_table.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included data file 'pandas\io\formats\templates\string.tpl' due to package data directory 'templates' for 'pandas.io.formats'.
Nuitka-Plugins:data-files: Included 603 data files due to package data directory 'zoneinfo' for 'pytz'.
Nuitka-Plugins:data-files: Included data file 'scipy\stats\_sobol_direction_numbers.npz' due to package data for 'scipy'.
Nuitka-Plugins:data-files: Included data file 'sklearn\utils\_estimator_html_repr.css' due to package data for 'sklearn.utils'.
Nuitka: Removing build directory 'o\application.build'.
Nuitka: Successfully created '~\Desktop\project\o\application.dist\application.exe'.