---2019/09/13---
为了上pyd和用pyinstaller打包趟了些坑,过两天详细补上,然后弄个打包脚本(嗯一定)...
---2019/09/15---
先上个代码吧...
import os
import sys
import shutil
from time import time
from py_compile import compile as pyc_compile
from distutils.core import setup, Extension
from Cython.Distutils import build_ext
# 编译pyd时需要额外参数 放在这里 执行命令时就不用加额外参数了
sys.argv += ['build_ext', '--inplace']
project_name = "your_project_name"
spec_file_name = project_name + ".spec"
def compile_py_to(py_path, compile_type="pyd", pkgs=None, compile_name=None, compile_output_dir=None, export_func_name=None):
# py_path py文件完整路径
# compile_type py编译的类型 默认是输出pyd pyc可选 pyc下可以另行配置要不要保留注释 说明 文档
# compile_name py编译最终的文件名 不含后缀 后缀由compile_type确定 不指定则默认和文件名一样
# compile_output_dir 编译的最终输出目录 若不指定则输出目录和py同一位置
# export_func_name 对外的接口函数名称 pyd可能需要 不指定则默认和文件名一样
# py_dir py文件所在文件夹
# py_name py文件名 带后缀
# py_basename py文件名 不带后缀
# ext 文件后缀 万一传入的不是py文件 用来判断一下
py_dir, py_name = os.path.split(py_path)
py_basename, ext = os.path.splitext(py_name)
pyini = [py_path, py_dir, py_name, py_basename]
if ext != ".py":
print("{}\n{} 不是python文件".format(py_dir, py_name))
return False
if compile_type == "pyd":
compile_status = compile_py_to_pyd(pyini, pkgs=pkgs, compile_name=compile_name, compile_output_dir=compile_output_dir, export_func_name=export_func_name)
elif compile_type == "pyc":
compile_status = compile_py_to_pyc(pyini, compile_name=compile_name, compile_output_dir=compile_output_dir)
else:
compile_status = "未作任何处理"
print("{}处理为{}的结果是 --> {}".format(py_path, compile_type, compile_status))
def compile_py_to_pyd(pyini, pkgs=None, compile_name=None, compile_output_dir=None, export_func_name=None):
py_path, py_dir, py_name, py_basename = pyini
compile_c_name = py_basename + ".c" # 临时生成的c文件名
compile_c_path = os.path.join(py_dir, compile_c_name) # 完整c文件的路径
pyd_ends = ".cp35-win32" + ".pyd" # 前面部分由python版本和windows位数确定
# 如果没有指定最终输出名则使用py_basename作为最终输出文件名
# 编译的最终输出目录
if compile_name is None:
compile_name = py_basename
if compile_output_dir is None:
compile_output_dir = py_dir
# export_func_name 如果不指定则和py文件名不带后缀一致
if export_func_name is None:
export_func_name = py_basename
compile_pyd_name = export_func_name + pyd_ends # 生成的pyd文件名 注意这里用的是export_func_name
compile_pyd_name_needed = compile_name + ".pyd" # 最终需要的pyd文件名
compile_pyd_path = os.path.join(py_dir, compile_pyd_name) # 完整pyd文件的路径
compile_pyd_path_needed = os.path.join(compile_output_dir, compile_pyd_name_needed) # 完整pyd文件的路径
# 正常情况这里需要注释掉 不注释掉则将跳过已存在的pyd
# if os.path.isfile(compile_pyd_path_needed):
# return True
if pkgs:
export_func_name = ".".join(pkgs + [export_func_name])
# ext_modules 中Extension第二个参数实际上可以多个文件一起
# 但暂时采用单个文件依次处理的方式 熟悉了可以试试更多的方法
ext_modules = [
Extension(export_func_name, [py_path]),
]
# 注意 生成pyd文件命令
# python compile_to_pyc_or_pyd.py build_ext --inplace
setup(
name = py_basename,# 包名
version = "0.3.2",# 包版本
author = "weimo",# 作者
author_email = "[email protected]",# 作者邮件
url = "https://blog.weimo.info",# 包的链接
description = "build pyd file by {} file.".format(py_name), # 简单描述
# long_description = "", # 详细描述
cmdclass = {'build_ext': build_ext},
ext_modules = ext_modules
)
# 删除临时c文件并重命名pyd文件名
if os.path.isfile(compile_c_path):
os.remove(compile_c_path)
if os.path.isfile(compile_pyd_path):
# 输出目录已存在pyd文件 先删除再重命名
if os.path.isfile(compile_pyd_path_needed):
os.remove(compile_pyd_path_needed)
os.rename(compile_pyd_path, compile_pyd_path_needed)
# 至此 默认情况下 py文件编译为pyd文件 两者位于同一目录位置
return True
def compile_py_to_pyc(pyini, compile_name=None, compile_output_dir=None):
py_path, py_dir, py_name, py_basename = pyini
if compile_name is None:
compile_name = py_basename
if compile_output_dir is None:
compile_output_dir = py_dir
compile_pyc_name = compile_name + ".pyc"
compile_pyc_path = os.path.join(compile_output_dir, compile_pyc_name)
# 预先删除掉原来的pyc文件
if os.path.exists(compile_pyc_path):
os.remove(compile_pyc_path)
print("删除了已存在的pyc文件 --> {}".format(compile_pyc_path))
# optimize 1是去除注释 2是去除注释和文档
compile_result = pyc_compile(py_path, cfile=compile_pyc_path, optimize=2)
print("compile_result --> {}".format(compile_result))
print("编译了 {} 为 {}".format(py_path, compile_pyc_path))
return True
def console_structure(project_dir=os.getcwd()):
# project_dir 项目目录
# 遍历project_dir 输出所有可能需要编译的文件路径等
# 手动选择调整 然后进行编译打包一把梭
# 一些通用的需要排除的通用设定
# methods = "exclude"
flist = []
methods = "include"
include_ends = [".py"]
exclude_starts = [".", "__"]
exclude_ends = [".log", ".json", ".exe", ".txt", ".ico", ".ini", ".html", ".h", ".ui", ".c", ".cpp", ".spec"]
exclude_dir = ["build", "dist", "program", "testscript"]
exclude_file = ["__init__.py", "py_file_you_want_to_exclude"]
tmp_exclude_file = ["tmp_py_file_you_want_to_exclude"]
exclude_file += tmp_exclude_file
for root, dirs, files in os.walk(project_dir, topdown=True):
if root == project_dir:
for subdir in dirs:
needed_continue = False
for starts in exclude_starts:
if subdir.startswith(starts):
needed_continue = True
break
if needed_continue:
continue
if subdir in exclude_dir:
continue
flist += console_structure(project_dir=os.path.join(project_dir, subdir))
for file in files:
if methods == "exclude":
needed_continue = False
for ends in exclude_ends:
if file.endswith(ends):
needed_continue = True
break
if needed_continue:
continue
if file in exclude_file:
continue
if file.endswith(".py"):
py_path = os.path.join(project_dir, file)
print("r'" + py_path + "',")
flist.append(py_path)
else:
break
return flist
def copy_to_zip(project_tmp_compile_dir):
mv_exclude_files = ["some_files_dont_need_to_move_such_as_main_pyd", spec_file_name]
mv_exclude_folders = ["some_folders_dont_need_to_move_such_as_build_and_dist", "dist", "__pycache__"]
for root, dirs, files in os.walk(project_tmp_compile_dir, topdown=True):
if root == project_tmp_compile_dir:
for subdir in dirs:
if subdir in mv_exclude_folders:
continue
path_old = os.path.join(project_tmp_compile_dir, subdir)
path_new = os.path.join(project_tmp_compile_dir, "dist", project_name)
shutil.move(path_old, path_new)
for file in files:
if file in mv_exclude_files:
continue
path_old = os.path.join(project_tmp_compile_dir, file)
path_new = os.path.join(project_tmp_compile_dir, "dist", project_name)
shutil.move(path_old, path_new)
def copy_binaries(project_tmp_compile_dir, project_dir=os.getcwd()):
dirs_need_to_copy = ["icon", "program"]
files_need_to_copy = [spec_file_name, "file_need_to_copy"]
for folder_need_copy in dirs_need_to_copy:
path_old = os.path.join(project_dir, folder_need_copy)
path_new = os.path.join(project_tmp_compile_dir, folder_need_copy)
if not os.path.exists(path_new):
shutil.copytree(path_old, path_new)
for file_need_copy in files_need_to_copy:
path_old = os.path.join(project_dir, file_need_copy)
path_new = os.path.join(project_tmp_compile_dir, file_need_copy)
if os.path.exists(path_new):
# pyinstaller 打包用的spec文件 和 另一个py文件需要覆盖掉
os.remove(path_new)
shutil.copyfile(path_old, path_new)
def compile_it(flist, project_dir=os.getcwd()):
project_top_level_dir, project_name = os.path.split(project_dir)
project_tmp_compile_dir = os.path.join(project_top_level_dir, "tmp_compile_for_" + project_name)
compile_config = {
"project_name":project_name,
"project_version":"0.3.2",
"project_tmp_compile_dir":project_tmp_compile_dir,
}
if not os.path.isdir(project_tmp_compile_dir):
os.mkdir(project_tmp_compile_dir)
pycs = [os.path.join(project_dir, "compact.py")]
check_path_to_make_dir_and_compile(pycs, project_dir, project_tmp_compile_dir)
pyds = flist
check_path_to_make_dir_and_compile(pyds, project_dir, project_tmp_compile_dir, compile_type="pyd")
return project_tmp_compile_dir
def check_path_to_make_dir_and_compile(pycs_or_pyds, project_dir, project_tmp_compile_dir, compile_type="pyc"):
for py__file_path in pycs_or_pyds:
# 先得到相对于项目的相对路径
# 然后分割 得到可能需要新建的文件夹 需要则新建 不需要则开始编译
pyc_file_relpath = os.path.relpath(py__file_path, project_dir)
pyc_file_path_pres = os.path.normpath(pyc_file_relpath)
dirs_need_to_make = pyc_file_path_pres.split(os.sep)[:-1] # 注意最后一个是文件 所以要去掉
# 检查有没有已经新建 没有的话就新建
base_dir = project_tmp_compile_dir
for dir_make in dirs_need_to_make:
dir_make_full_path = os.path.join(base_dir, dir_make)
if os.path.isdir(dir_make_full_path):
base_dir = dir_make_full_path # 注意这里是因为往后的话 基础目录在变化
continue
else:
# print("新建 --> {}".format(dir_make_full_path))
os.mkdir(dir_make_full_path)
compile_output_dir = os.path.join(project_tmp_compile_dir, *dirs_need_to_make)
if compile_type == "pyc":
compile_py_to(py__file_path, compile_type="pyc", compile_output_dir=compile_output_dir)
elif compile_type == "pyd":
py__dir, py_name = os.path.split(py__file_path)
py__basename, ext = os.path.splitext(py_name)
if py__basename == "main":
compile_py_to(py__file_path, compile_type="pyd", pkgs=dirs_need_to_make, compile_name="compile_name_you_want", compile_output_dir=compile_output_dir, export_func_name="export_func_name_you_want")
else:
compile_py_to(py__file_path, compile_type="pyd", pkgs=dirs_need_to_make, compile_output_dir=compile_output_dir)
def pyinstaller_build_tozip(project_tmp_compile_dir):
# 这里实际上没有打包成zip 因为还需要测试下 稳定了加上
copy_binaries(project_tmp_compile_dir)
path_log = os.getcwd()
os.chdir(project_tmp_compile_dir)
os.system("pyinstaller {}".format(spec_file_name))
os.chdir(path_log)
copy_to_zip(project_tmp_compile_dir)
if __name__ == "__main__":
flist = console_structure()
print("-*-" * 10)
compile_flag = input("continue to compile? (y/n)\n")
if compile_flag != "y":
print("action end.")
exit()
ts = time()
project_tmp_compile_dir = compile_it(flist)
pyinstaller_build_tozip(project_tmp_compile_dir)
print("打包完成!耗时{:.2f}s".format(time() - ts))