├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── jmpy ├── __init__.py ├── cmdline.py ├── encrypt_py.py └── log.py ├── requirements.txt ├── setup.py └── tests └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | venv/ 3 | __pycache__/ 4 | dist/ 5 | encrypt_py.egg-info/ 6 | build/ 7 | jmpy3.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Modifications: 4 | 5 | Copyright (c) 2020 Boris 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | 4 | global-exclude __pycache__ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jmpy 2 | 3 | ![](https://img.shields.io/badge/python-3.0-brightgreen) 4 | 5 | ## 简介 6 | 7 | 将python代码一键加密为so或pyd。支持单个文件加密,整个项目加密。 8 | 9 | Git仓库地址: https://github.com/Boris-code/jmpy.git 10 | 11 | ## 安装 12 | 13 | pip install jmpy3 14 | 15 | ## 使用方法 16 | 17 | jmpy -i "xxx project dir" [-o output dir] 18 | 19 | 加密后的文件默认存储在 dist/project_name/ 下 -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.6 2 | -------------------------------------------------------------------------------- /jmpy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Created on 2020/6/17 6:41 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | ''' -------------------------------------------------------------------------------- /jmpy/cmdline.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/6/17 6:55 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | """ 9 | import getopt 10 | import sys 11 | 12 | from jmpy.encrypt_py import start_encrypt 13 | 14 | 15 | def usage(): 16 | """ 17 | python代码 加密|加固 18 | 参数说明: 19 | -i | --input_file_path 待加密文件或文件夹路径,可是相对路径或绝对路径 20 | -o | --output_file_path 加密后的文件输出路径,默认在input_file_path下创建dist文件夹,存放加密后的文件 21 | -I | --ignore_files 不需要加密的文件或文件夹,逗号分隔 22 | -m | --except_main_file 不加密包含__main__的文件(主文件加密后无法启动), 值为0、1。 默认为1 23 | """ 24 | 25 | 26 | def execute(): 27 | try: 28 | options, args = getopt.getopt( 29 | sys.argv[1:], 30 | "hi:o:I:m:", 31 | [ 32 | "help", 33 | "input_file_path=", 34 | "output_file_path=", 35 | "ignore_files=", 36 | "except_main_file=", 37 | ], 38 | ) 39 | input_file_path = output_file_path = ignore_files = "" 40 | except_main_file = 1 41 | 42 | for name, value in options: 43 | if name in ("-h", "--help"): 44 | print(usage.__doc__) 45 | sys.exit() 46 | 47 | elif name in ("-i", "--input_file_path"): 48 | input_file_path = value 49 | 50 | elif name in ("-o", "--output_file_path"): 51 | output_file_path = value 52 | 53 | elif name in ("-I", "--ignore_files"): 54 | ignore_files = value.split(",") 55 | 56 | elif name in ("-m", "--except_main_file"): 57 | except_main_file = int(value) 58 | 59 | if not input_file_path: 60 | print("需指定-i 或 input_file_path") 61 | print(usage.__doc__) 62 | sys.exit() 63 | 64 | start_encrypt(input_file_path, output_file_path, ignore_files, except_main_file) 65 | 66 | except getopt.GetoptError: 67 | print(usage.__doc__) 68 | sys.exit() 69 | -------------------------------------------------------------------------------- /jmpy/encrypt_py.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2018-07-18 18:24 4 | --------- 5 | @summary: 加密python代码为pyd/so 6 | --------- 7 | @author: Boris 8 | """ 9 | import os 10 | import re 11 | import shutil 12 | import tempfile 13 | from distutils.command.build_py import build_py 14 | from distutils.core import setup 15 | from typing import Union, List 16 | 17 | from Cython.Build import cythonize 18 | 19 | from jmpy.log import logger 20 | 21 | 22 | def get_package_dir(*args, **kwargs): 23 | return "" 24 | 25 | 26 | # 重写get_package_dir, 否者生成的so文件路径有问题 27 | build_py.get_package_dir = get_package_dir 28 | 29 | 30 | class TemporaryDirectory(object): 31 | def __enter__(self): 32 | self.name = tempfile.mkdtemp() 33 | return self.name 34 | 35 | def __exit__(self, exc_type, exc_value, traceback): 36 | shutil.rmtree(self.name) 37 | 38 | 39 | def search(content, regexs): 40 | if isinstance(regexs, str): 41 | return re.search(regexs, content) 42 | 43 | for regex in regexs: 44 | if re.search(regex, content): 45 | return True 46 | 47 | 48 | def walk_file(file_path): 49 | if os.path.isdir(file_path): 50 | for current_path, sub_folders, files_name in os.walk(file_path): 51 | for file in files_name: 52 | file_path = os.path.join(current_path, file) 53 | yield file_path 54 | 55 | else: 56 | yield file_path 57 | 58 | 59 | def copy_files(src_path, dst_path): 60 | if os.path.isdir(src_path): 61 | if os.path.exists(dst_path): 62 | shutil.rmtree(dst_path) 63 | 64 | def callable(src, names: list): 65 | if search(src, dst_path): 66 | return names 67 | return ["dist", ".git", "venv", ".idea", "__pycache__"] 68 | 69 | shutil.copytree(src_path, dst_path, ignore=callable) 70 | else: 71 | if not os.path.exists(dst_path): 72 | os.makedirs(dst_path) 73 | shutil.copyfile(src_path, os.path.join(dst_path, os.path.basename(src_path))) 74 | 75 | 76 | def get_py_files(files, ignore_files: Union[List, str, None] = None): 77 | """ 78 | @summary: 79 | --------- 80 | @param files: 文件列表 81 | #param ignore_files: 忽略的文件,支持正则 82 | --------- 83 | @result: 84 | """ 85 | for file in files: 86 | if file.endswith(".py"): 87 | if ignore_files and search(file, regexs=ignore_files): # 该文件是忽略的文件 88 | pass 89 | else: 90 | yield file 91 | 92 | 93 | def filter_cannot_encrypted_py(files, except_main_file): 94 | """ 95 | 过滤掉不能加密的文件,如 log.py __main__.py 以及包含 if __name__ == "__main__": 的文件 96 | Args: 97 | files: 98 | 99 | Returns: 100 | 101 | """ 102 | _files = [] 103 | for file in files: 104 | if search(file, regexs="__.*?.py"): 105 | continue 106 | 107 | if except_main_file: 108 | with open(file, "r", encoding="utf-8") as f: 109 | content = f.read() 110 | if search(content, regexs="__main__"): 111 | continue 112 | 113 | _files.append(file) 114 | 115 | return _files 116 | 117 | 118 | def encrypt_py(py_files: list): 119 | encrypted_py = [] 120 | 121 | with TemporaryDirectory() as td: 122 | total_count = len(py_files) 123 | for i, py_file in enumerate(py_files): 124 | try: 125 | dir_name = os.path.dirname(py_file) 126 | file_name = os.path.basename(py_file) 127 | 128 | os.chdir(dir_name) 129 | 130 | logger.debug("正在加密 {}/{}, {}".format(i + 1, total_count, file_name)) 131 | 132 | setup( 133 | ext_modules=cythonize([file_name], quiet=True, language_level=3), 134 | script_args=["build_ext", "-t", td, "--inplace"], 135 | ) 136 | 137 | encrypted_py.append(py_file) 138 | logger.debug("加密成功 {}".format(file_name)) 139 | 140 | except Exception as e: 141 | logger.exception("加密失败 {} , error {}".format(py_file, e)) 142 | temp_c = py_file.replace(".py", ".c") 143 | if os.path.exists(temp_c): 144 | os.remove(temp_c) 145 | 146 | return encrypted_py 147 | 148 | 149 | def delete_files(files_path): 150 | """ 151 | @summary: 删除文件 152 | --------- 153 | @param files_path: 文件路径 py 及 c 文件 154 | --------- 155 | @result: 156 | """ 157 | try: 158 | # 删除python文件及c文件 159 | for file in files_path: 160 | os.remove(file) # py文件 161 | os.remove(file.replace(".py", ".c")) # c文件 162 | 163 | except Exception as e: 164 | pass 165 | 166 | 167 | def rename_excrypted_file(output_file_path): 168 | files = walk_file(output_file_path) 169 | for file in files: 170 | if file.endswith(".pyd") or file.endswith(".so"): 171 | new_filename = re.sub("(.*)\..*\.(.*)", r"\1.\2", file) 172 | os.rename(file, new_filename) 173 | 174 | 175 | def start_encrypt( 176 | input_file_path, 177 | output_file_path: str = None, 178 | ignore_files: Union[List, str, None] = None, 179 | except_main_file: int = 1, 180 | ): 181 | assert input_file_path, "input_file_path cannot be null" 182 | 183 | assert ( 184 | input_file_path != output_file_path 185 | ), "output_file_path must be diffent with input_file_path" 186 | 187 | if output_file_path and os.path.isfile(output_file_path): 188 | raise ValueError("output_file_path need a dir path") 189 | 190 | input_file_path = os.path.abspath(input_file_path) 191 | if not output_file_path: # 无输出路径 192 | if os.path.isdir( 193 | input_file_path 194 | ): # 如果输入路径是文件夹 则输出路径为input_file_path/dist/project_name 195 | output_file_path = os.path.join( 196 | input_file_path, "dist", os.path.basename(input_file_path) 197 | ) 198 | else: 199 | output_file_path = os.path.join(os.path.dirname(input_file_path), "dist") 200 | else: 201 | output_file_path = os.path.abspath(output_file_path) 202 | 203 | # 拷贝原文件到目标文件 204 | copy_files(input_file_path, output_file_path) 205 | 206 | files = walk_file(output_file_path) 207 | py_files = get_py_files(files, ignore_files) 208 | 209 | # 过滤掉不需要加密的文件 210 | need_encrypted_py = filter_cannot_encrypted_py(py_files, except_main_file) 211 | 212 | encrypted_py = encrypt_py(need_encrypted_py) 213 | 214 | delete_files(encrypted_py) 215 | rename_excrypted_file(output_file_path) 216 | 217 | logger.debug( 218 | "加密完成 total_count={}, success_count={}, 生成到 {}".format( 219 | len(need_encrypted_py), len(encrypted_py), output_file_path 220 | ) 221 | ) 222 | -------------------------------------------------------------------------------- /jmpy/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/6/17 12:17 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | """ 9 | import logging 10 | import sys 11 | 12 | # set up logging 13 | logger = logging.getLogger("encrypt-py") 14 | 15 | format_string = ( 16 | "%(asctime)s|%(filename)s|%(funcName)s|line:%(lineno)d|%(levelname)s| %(message)s" 17 | ) 18 | formatter = logging.Formatter(format_string, datefmt="%Y-%m-%dT%H:%M:%S") 19 | handler = logging.StreamHandler() 20 | handler.setFormatter(formatter) 21 | handler.stream = sys.stdout 22 | 23 | logger.addHandler(handler) 24 | logger.setLevel(logging.DEBUG) 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Cython==0.29.20 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/4/22 10:45 PM 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boris 8 | @email: boris@bzkj.tech 9 | """ 10 | 11 | from os.path import dirname, join 12 | from sys import version_info 13 | 14 | import setuptools 15 | 16 | if version_info < (3, 0, 0): 17 | raise SystemExit("Sorry! jmpy requires python 3.0.0 or later.") 18 | 19 | with open(join(dirname(__file__), "VERSION"), "rb") as f: 20 | version = f.read().decode("ascii").strip() 21 | 22 | with open("README.md", "r") as fh: 23 | long_description = fh.read() 24 | 25 | packages = setuptools.find_packages() 26 | 27 | setuptools.setup( 28 | name="jmpy3", 29 | version=version, 30 | author="Boris", 31 | license="MIT", 32 | author_email="boris@bzkj.tech", 33 | description="python代码一键加密", 34 | long_description=long_description, 35 | long_description_content_type="text/markdown", 36 | install_requires=["Cython==0.29.20"], 37 | entry_points={"console_scripts": ["jmpy = jmpy.cmdline:execute"]}, 38 | url="https://github.com/Boris-code/jmpy.git", 39 | packages=packages, 40 | include_package_data=True, 41 | classifiers=["Programming Language :: Python :: 3"], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on 2020/6/17 8:58 下午 4 | --------- 5 | @summary: 6 | --------- 7 | @author: Boirs 8 | """ 9 | 10 | from jmpy.encrypt_py import start_encrypt 11 | 12 | input_file_path = "test.py" 13 | start_encrypt(input_file_path=input_file_path, output_file_path=None, ignore_files=None) 14 | --------------------------------------------------------------------------------