python – 在linux上编译py文件为【.so】文件部署项目运行

python – 在linux上编译py文件为【.so】文件,可通过主文件直接执行

一. 前言

在Python中,通常不直接将Python代码编译为.so(共享对象)文件来执行,因为.so文件是编译后的二进制代码,通常用于C或C++等语言,并且它们被设计为可以被Python(通过C API)或其他语言(如C或C++)动态加载和执行。
前言
.so文件的特点是看不到源码,但是依然能被其他.py文件调用。将py文件编译为so文件可以在一定程度上减小源码泄露的可能性。这篇文章以.py编译为.so为例,记录完整的加密流程。

二.打包编译项目

准备工作

环境

  • Linux环境:CentOS 8.5、gcc:yum install gcc
  • Python环境:Cython:pip install cython
  • 一般linux上都会有GCC编译器,如若没有请先安装

    1.安装Cython

    pip install cython
    

    2.将以下的脚本放在deploy目录下,项目所有文件放在project下面即可

    1.编译为.c文件的代码

    创建一个setup_cmd.py文件

    import logging
    import os
    
    from setuptools import setup
    from Cython.Build import cythonize
    
    # ============================== 配置日志 ===============================
    # 定义日志文件的名称
    log_filename = 'setup_cmd.log'
    
    # 创建一个日志记录器
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)  # 设置日志级别为 INFO
    
    # 创建一个文件处理器,并设置级别为 INFO
    file_handler = logging.FileHandler(log_filename, encoding='utf-8')  # 指定编码为 utf-8
    file_handler.setLevel(logging.INFO)
    
    # 创建一个流处理器(控制台输出),并设置级别为 INFO
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    
    # 创建日志格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    stream_handler.setFormatter(formatter)
    
    # 将处理器添加到日志记录器
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    
    # ============================== 配置日志 ===============================
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 设置 BASE_DIR
    logger.info(f"BASE_DIR -> {BASE_DIR}\n")
    
    
    # 获取需要编译的文件列表
    def get_py_files(directory, exclude_folders, exclude_file_list):
        py_files = []
        for root, dirs, files in os.walk(directory):
            for d in dirs:
                if d in exclude_folders:
                    dirs.remove(d)
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"不需要编译文件:[{file}] 在 {exclude_file_list} 中!")
                    continue
    
                if file.endswith('.py') and '-' not in file:
                    py_files.append(os.path.join(root, file))
        logger.info(f'py_files -> {py_files}')
        logger.info(f'py_files count -> {len(py_files)}')
        return py_files
    
    
    exclude_folders = ['tests', '.git', '.idea', '__pycache__', 'a-deploy', 'deploy']
    exclude_file_list = ['app.py']
    
    # 1.编译打包
    file_list = get_py_files(BASE_DIR, exclude_folders, exclude_file_list)
    # 单个文件编译或多个文件
    # file_list = ['/opt/pkg/project/dev/service/sessionService/service_impl/nw_session_history.py']
    setup(
        ext_modules=cythonize(file_list, language_level=3),  # 使用 Python 3 的语言级别
    )
    

    1.在linux环境下可直接执行文件

    python3 setup_cmd.py
    

    2.windows上直接使用命令执行

    python3 setup_cmd.py build_ext --inplace
    
    2.将.c文件转化为.so文件

    创建setup_compile_file.py文件
    主要是将以下命令拆解执行

    gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/usr/local/include/python3.11 -c /opt/pkg/project/dev/base/utils/handle_tasks.c -o build/temp.linux-x86_64-cpython-311/opt/pkg/project/dev/base/utils/handle_tasks.o
    gcc -pthread -shared build/temp.linux-x86_64-cpython-311/opt/pkg/project/dev/base/utils/handle_tasks.o -o build/lib.linux-x86_64-cpython-311/handle_tasks.cpython-311-x86_64-linux-gnu.so
    

    代码如下,参考函数:compile_c_files

    # -*- coding: utf-8 -*-
    import logging
    import os
    import shutil
    import subprocess
    
    from setuptools import setup
    from Cython.Build import cythonize
    
    import os
    
    from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
    
    executor = ThreadPoolExecutor(max_workers=50)
    
    # ============================== 配置日志 ===============================
    # 定义日志文件的名称
    log_filename = 'setup_compile_file.log'
    
    # 创建一个日志记录器
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)  # 设置日志级别为 INFO
    
    # 创建一个文件处理器,并设置级别为 INFO
    file_handler = logging.FileHandler(log_filename, encoding='utf-8')  # 指定编码为 utf-8
    file_handler.setLevel(logging.INFO)
    
    # 创建一个流处理器(控制台输出),并设置级别为 INFO
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    
    # 创建日志格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    stream_handler.setFormatter(formatter)
    
    # 将处理器添加到日志记录器
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    # ============================== 配置日志 ===============================
    
    
    # BASE_DIR = os.getcwd()
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 设置 BASE_DIR
    logger.info(f"BASE_DIR -> {BASE_DIR}\n")
    
    executor = ThreadPoolExecutor(max_workers=10)  # 根据需要调整线程数
    
    
    # 获取需要编译的文件列表
    def get_py_files(directory, exclude_folders, exclude_file_list):
        py_files = []
        for root, dirs, files in os.walk(directory):
            for d in dirs:
                if d in exclude_folders:
                    dirs.remove(d)
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"不需要编译文件:[{file}] 在 {exclude_file_list} 中!")
                    continue
    
                if file.endswith('.py') and '-' not in file:
                    py_files.append(os.path.join(root, file))
        logger.info(f'py_files -> {py_files}')
        logger.info(f'py_files count -> {len(py_files)}')
        return py_files
    
    
    # 编译扩展模块
    def compile_pkg(file_list):
        setup(
            ext_modules=cythonize(file_list, language_level=3),  # 使用 Python 3 的语言级别
        )
        subprocess.run(['python3', 'setup_compile_file.py', 'build_ext', '--inplace'])
    
    
    #
    def copy_compiled_files(source_dir, target_dir, exclude_folders, exclude_file_list):
        """ 拷贝处理编译好的文件 """
    
        if os.path.exists(target_dir):
            shutil.rmtree(target_dir)
            logger.info(f"已删除旧的目标文件夹: {target_dir}")
    
        os.makedirs(target_dir)
        logger.info(f"已创建新的目标文件夹: {target_dir}")
        ans = 0
        # 遍历源文件夹  
        for root, dirs, files in os.walk(source_dir):
            # 过滤掉要排除的文件夹  
            dirs[:] = [d for d in dirs if d not in exclude_folders]
            # 遍历当前文件夹中的文件
            for file in files:
                try:
                    if file in exclude_file_list:
                        logger.info(f"不拷贝文件:[{file}] 在 exclude_file_list:{exclude_file_list} 中!")
                        continue
    
                    # 构造源和目标文件路径
                    src_file = os.path.join(root, file)
                    rel_path = os.path.relpath(root, source_dir)
                    dst_file = os.path.join(target_dir, rel_path, file)
    
                    # 确保目标文件夹存在
                    os.makedirs(os.path.dirname(dst_file), exist_ok=True)
    
                    # 拷贝文件
                    shutil.copy2(src_file, dst_file)
                    ans += 1
                    logger.info(f'文件拷贝成功[src_file]:{src_file} -> [dst_file]:{dst_file}')
                except Exception as e:
                    logger.info(f'文件拷贝异常:file:{file} -> error:{e}')
        logger.info(f"已成功拷贝编译文件,数量:{ans}")
    
    
    def delete_specific_files(folder, extensions, exclude_file_list):
        """
        在指定文件夹中删除所有以extensions中指定的扩展名结尾的文件。
    
        :param folder: 文件夹路径
        :param extensions: 要删除的文件扩展名列表
        """
        # 遍历文件夹
        logger.info(f"")
        ans = 0
        for root, dirs, files in os.walk(folder):
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"不删除文件:[{file}] 在 exclude_file_list:{exclude_file_list} 中!")
                    continue
                if file.endswith(tuple(extensions)):
                    os.remove(os.path.join(root, file))
                    logger.info(f"已删除含有 {extensions} 的文件: {os.path.join(root, file)}")
                    ans += 1
        logger.info(f"已删除以{extensions}结尾的文件数量:{ans}")
    
    
    def compile_c_files(target_dir, extensions, exclude_folders, exclude_file_list):
        """
        编译文件(linux下的命令)
        gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/usr/local/include/python3.11 -c /opt/pkg/project/dev/base/utils/handle_tasks.c -o build/temp.linux-x86_64-cpython-311/opt/pkg/project/dev/base/utils/handle_tasks.o
        gcc -pthread -shared build/temp.linux-x86_64-cpython-311/opt/pkg/project/dev/base/utils/handle_tasks.o -o build/lib.linux-x86_64-cpython-311/handle_tasks.cpython-311-x86_64-linux-gnu.so
        """
        # 遍历文件夹
        ans = 0
        futures = []
        for root, dirs, files in os.walk(target_dir):
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"文件:{file} 在 exclude_file_list:{exclude_file_list} 中,不需要编译!")
                    continue
                if file.endswith(tuple(extensions)):
                    c_path = os.path.join(root, file)
                    file_name = file.split('.')[0]
                    # o_path = root + '/' + file_name + '.o'
                    o_path = os.path.join(root, file_name + '.o')
                    # so_path = root + '/' + file_name + '.cpython-311-x86_64-linux-gnu.so'
                    so_path = os.path.join(root, file_name + '.cpython-311-x86_64-linux-gnu.so')
                    logger.info(f'c_path - > {c_path}')
                    logger.info(f'o_path - > {o_path}')
                    logger.info(f'so_path - > {so_path}')
                    future = executor.submit(compile_file_to_so, c_path, o_path, so_path)
                    futures.append(future)
                    ans += 1
                    logger.info(f'complied count -----> {ans}')
        # 等待所有任务完成
        wait(futures, return_when=ALL_COMPLETED)
        logger.info(f"已编译文件数量:{ans}")
    
    
    def compile_file_to_so(c_path, o_path, so_path):
        os.system(
            f"gcc -pthread -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/usr/local/include/python3.11 -c {c_path} -o {o_path}")
        os.system(
            f"gcc -pthread -shared {o_path} -o {so_path}")
        logger.info(f'File success compiled to - > {so_path}')
    
    
    def main():
        exclude_folders = ['tests', '.git', '.idea', '__pycache__', 'a-deploy', 'deploy']
        exclude_file_list = ['app.py', 'app.c']
        logger.info(f'项目编译打包开始,源码文件路劲:{BASE_DIR}\n')
        # try:
        #     # 单个文件编译或多个文件
        #     # file_list = ['/opt/pkg/project/dev/service/sessionService/service_impl/nw_session_history.py']
        #
        #     # 1.编译打包
        #     file_list = get_py_files(BASE_DIR, exclude_folders, exclude_file_list)
        #     compile_pkg(file_list)
        #     ...
        # except Exception as e:
        #     logger.info(f'Exception:{e}')
    
        source_dir = BASE_DIR
        target_dir = os.path.join(BASE_DIR, 'deploy', 'dist')
    
        # 清除['.c']文件
        extensions = ['.c']
        logger.info(f'开始清除项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
        delete_specific_files(target_dir, extensions, exclude_file_list)
        logger.info(f'结束清除项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
    
        # 2.复制编译好的文件
        logger.info(f'复制拷贝项目开始 [source_dir]:{source_dir} -> [target_dir]:{target_dir}\n')
        copy_compiled_files(source_dir, target_dir, exclude_folders, ['app.c'])
        logger.info(f'复制拷贝项目结束 [source_dir]:{source_dir} -> [target_dir]:{target_dir}\n')
    
        # # 3.删除py文件
        # extensions = ['.py']
        # logger.info(f'开始删除拷贝项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
        # delete_specific_files(target_dir, extensions, exclude_file_list)
        # logger.info(f'结束删除拷贝项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
    
        # 4.编译项目['.c']文件 -> ['.so', 'pyd']
        compile_extensions = ['.c']
        logger.info(f"开始编译项目{compile_extensions}文件 ->  ['.so', 'pyd']\n")
        compile_c_files(target_dir, compile_extensions, exclude_folders, exclude_file_list)
        logger.info(f"结束编译项目{compile_extensions}文件 ->  ['.so', 'pyd']\n")
    
        logger.info(f'项目编译处理完成,源码文件路劲:{source_dir}\n')
        logger.info(f'项目编译处理完成,编译打包文件路劲:{target_dir}\n')
    
    
    if __name__ == '__main__':
        main()
    
    
    3.清理编译后的项目文件

    创建clean_compile_file.py文件

    # -*- coding: utf-8 -*-
    import logging
    
    import os
    
    from concurrent.futures import ThreadPoolExecutor
    
    executor = ThreadPoolExecutor(max_workers=50)
    
    # ============================== 配置日志 ===============================
    # 定义日志文件的名称
    log_filename = 'setup_compile_file.log'
    
    # 创建一个日志记录器
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)  # 设置日志级别为 INFO
    
    # 创建一个文件处理器,并设置级别为 INFO
    file_handler = logging.FileHandler(log_filename, encoding='utf-8')  # 指定编码为 utf-8
    file_handler.setLevel(logging.INFO)
    
    # 创建一个流处理器(控制台输出),并设置级别为 INFO
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    
    # 创建日志格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    stream_handler.setFormatter(formatter)
    
    # 将处理器添加到日志记录器
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    # ============================== 配置日志 ===============================
    
    
    # BASE_DIR = os.getcwd()
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 设置 BASE_DIR
    logger.info(f"BASE_DIR -> {BASE_DIR}\n")
    
    
    def delete_specific_files(folder, extensions, exclude_file_list):
        """
        在指定文件夹中删除所有以extensions中指定的扩展名结尾的文件。
    
        :param folder: 文件夹路径
        :param extensions: 要删除的文件扩展名列表
        """
        # 遍历文件夹
        logger.info(f"")
        ans = 0
        for root, dirs, files in os.walk(folder):
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"不删除文件:[{file}] 在 exclude_file_list:{exclude_file_list} 中!")
                    continue
                if file.endswith(tuple(extensions)):
                    os.remove(os.path.join(root, file))
                    logger.info(f"已删除含有 {extensions} 的文件: {os.path.join(root, file)}")
                    ans += 1
        logger.info(f"已删除以{extensions}结尾的文件数量:{ans}")
    
    
    def main():
        exclude_folders = ['tests', '.git', '.idea', '__pycache__', 'a-deploy', 'deploy']
        exclude_file_list = ['app.py', 'app.c']
        logger.info(f'========= 项目编译文件清理完成开始 ========\n')
        target_dir = os.path.join(BASE_DIR, 'deploy', 'dist')
    
        # 删除py文件
        extensions = ['.py', '.o', '.pyd', '.c']
        logger.info(f'开始删除拷贝项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
        delete_specific_files(target_dir, extensions, exclude_file_list)
        logger.info(f'结束删除拷贝项目中含有{extensions}结尾的文件 [target_dir]:{target_dir}\n')
    
        # # 删除源码下的['.so', 'pyd', '.c']文件
        # src_extensions = ['.so', 'pyd', '.c']
        # logger.info(f"开始删除源码下的{src_extensions}文件\n")
        # delete_specific_files(source_dir, src_extensions)
        # logger.info(f"结束删除源码下的{src_extensions}文件\n")
    
        logger.info(f'========= 项目编译文件清理完成!=========\n')
    
    
    if __name__ == '__main__':
        main()
    
    

    其他操作

    清理源代码下的.c文件(可选)
    clean_code_c_file.py

    # -*- coding: utf-8 -*-
    import logging
    
    import os
    
    from concurrent.futures import ThreadPoolExecutor
    
    executor = ThreadPoolExecutor(max_workers=50)
    
    # ============================== 配置日志 ===============================
    # 定义日志文件的名称
    log_filename = 'setup_compile_file.log'
    
    # 创建一个日志记录器
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)  # 设置日志级别为 INFO
    
    # 创建一个文件处理器,并设置级别为 INFO
    file_handler = logging.FileHandler(log_filename, encoding='utf-8')  # 指定编码为 utf-8
    file_handler.setLevel(logging.INFO)
    
    # 创建一个流处理器(控制台输出),并设置级别为 INFO
    stream_handler = logging.StreamHandler()
    stream_handler.setLevel(logging.INFO)
    
    # 创建日志格式
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    stream_handler.setFormatter(formatter)
    
    # 将处理器添加到日志记录器
    logger.addHandler(file_handler)
    logger.addHandler(stream_handler)
    # ============================== 配置日志 ===============================
    
    
    # BASE_DIR = os.getcwd()
    BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 设置 BASE_DIR
    logger.info(f"BASE_DIR -> {BASE_DIR}\n")
    
    
    def delete_specific_files(folder, extensions, exclude_file_list):
        """
        在指定文件夹中删除所有以extensions中指定的扩展名结尾的文件。
    
        :param folder: 文件夹路径
        :param extensions: 要删除的文件扩展名列表
        """
        # 遍历文件夹
        logger.info(f"")
        ans = 0
        for root, dirs, files in os.walk(folder):
            for file in files:
                if file in exclude_file_list:
                    logger.info(f"不删除文件:[{file}] 在 exclude_file_list:{exclude_file_list} 中!")
                    continue
                if file.endswith(tuple(extensions)):
                    os.remove(os.path.join(root, file))
                    logger.info(f"已删除含有 {extensions} 的文件: {os.path.join(root, file)}")
                    ans += 1
        logger.info(f"已删除以{extensions}结尾的文件数量:{ans}")
    
    
    def main():
        exclude_folders = ['tests', '.git', '.idea', '__pycache__', 'a-deploy', 'deploy']
        exclude_file_list = ['app.py', 'app.c']
        logger.info(f'========= 项目编译文件清理完成开始 ========\n')
        source_dir = BASE_DIR
    
        # 删除源码下的['.so', 'pyd', '.c']文件
        src_extensions = ['.c']
        logger.info(f"开始删除源码下的{src_extensions}文件\n")
        delete_specific_files(source_dir, src_extensions, exclude_file_list)
        logger.info(f"结束删除源码下的{src_extensions}文件\n")
    
        logger.info(f'========= 项目源码文件清理完成!=========\n')
    
    
    if __name__ == '__main__':
        main()
    
    

    shell脚本

    start_setup.sh

    #!/bin/bash
    
    echo "开始执行 setup_cmd.py ..."
    python3 setup_cmd.py
    echo "setup_cmd.py 执行完成!"
    
    echo "开始执行 setup_compile_file.py ..."
    python3 setup_compile_file.py
    if [ $? -eq 0 ]; then
        echo "setup_compile_file.py 中的所有任务执行成功完成!"
    else
        echo "setup_compile_file.py 执行失败或任务未完成,请检查错误。"
        exit 1
    fi
    
    # 等待 setup_compile_file.py 中的所有线程完成后,执行 clean_compile_file.py
    echo "开始执行 clean_compile_file.py ..."
    python3 clean_compile_file.py
    if [ $? -eq 0 ]; then
        echo "clean_compile_file.py 执行成功!"
    else
        echo "clean_compile_file.py 执行失败,请检查错误。"
        exit 1
    fi
    
    echo "所有脚本执行完毕。"
    
    

    注意事项

    这里有一个小坑需要注意一下。如果你有写函数参数类型声明的习惯,在编译之后,可能项目就无法运行了。

    具体来说,当你写了类似如下的类型声明时:

    错误写法
    def fn(numbers: list[int]) -> int:
    	pass
    

    在运行编译之后的项目时,会出现报错:

    typeerror: type object is not subscriptable
    

    解决方案: 把所有类型声明中,带有中括号的部分删掉,不然编译成c语言后,程序会以为这个是索引。

    正确写法
    def fn(numbers: list) -> int:
    	pass
    

    总结

    Cython只是帮助我们将Python代码(或Python风格的代码)转换成了C代码,然后编译成了二进制形式。这个过程并不是传统意义上的“编译Python代码为机器码执行”,而是利用了C的编译效率和Python的易用性之间的平衡。

    本文介绍到此结束,希望对你有所帮助!

    作者:天下·第二

    物联沃分享整理
    物联沃-IOTWORD物联网 » python – 在linux上编译py文件为【.so】文件部署项目运行

    发表回复