def _precheck(conf: TConf): assert not ospath.exists(conf['build']['dist_dir']) # from .global_dirs import curr_dir # if not ospath.exists(f'{curr_dir}/template/pytransform'): # from os import popen # popen('pyarmor runtime -O "{}"'.format(f'{curr_dir}/template')).read() if conf['build']['venv']['enable_venv']: from .embed_python import EmbedPythonManager builder = EmbedPythonManager( pyversion=conf['build']['venv']['python_version'] ) # try to get a valid embed python path, if failed, raise an error. builder.get_embed_python_dir() mode = conf['build']['venv']['mode'] if mode == 'source_venv': if venv_path := conf['build']['venv']['options'][mode]['path']: if venv_path.startswith(src_path := conf['build']['proj_dir']): lk.logt('[C2015]', '请勿将虚拟环境放在您的源代码文件夹下! 这将导致虚拟' '环境中的第三方库代码也被加密, 通常情况下这会导致不' '可预测的错误发生. 您可以选择将虚拟环境目录放到与源' '代码同级的目录, 这是推荐的做法.\n' f'\t虚拟环境目录: {venv_path}\n' f'\t建议迁移至: {ospath.dirname(src_path)}/venv') if input('是否仍要继续运行? (y/n): ').lower() != 'y': raise SystemExit
def save(self, alt=False, open_after_saved=False): from xlsxwriter.exceptions import FileCreateError try: self.book.close() except FileCreateError as e: # 注意: 此错误在 macOS 平台无法捕获! if alt: # 尝试对文件改名后保存 from time import strftime self.book.filename = self.filepath = self.filepath.replace( '.xlsx', strftime('_%Y%m%d_%H%M%S.xlsx') ) self.book.close() elif input( '\tPermission denied while saving excel: \n' '\t\t"{}:0"\n' '\tPlease close the opened file manually and input "Y" to ' 'retry to save.'.format(self.filepath) ).lower() == 'y': self.book.close() else: raise e if open_after_saved: import os os.startfile(os.path.abspath(self.filepath)) else: from lk_logger import lk lk.logt( '[ExcelWriter][D1139]', f'\n\tExcel saved to "{self.filepath}:0"', h=self.__h )
def build(self, *args, **kwargs): self._comp = self._func(*args, **kwargs) if self._comp is None: lk.logt('[W0314]', 'The wrapped function should return its ' 'component. (Here we use a workaround to fix ' 'this issue.)', h='parent') self._comp = _pointer return self._comp
def compile_one(self, src_file, dst_file): """ Compile `src_file` and generate `dst_file`. Args: src_file dst_file References: `cmd:pyarmor obfuscate -h` Results: the `dst_file` has the same content structure: from pytransform import pyarmor_runtime pyarmor_runtime() __pyarmor__(__name__, __file__, b'\\x50\\x59\\x41...') `pytransform` comes from `{dist}/lib`, it will be added to `sys.path` in the startup (see `pyportable_installer/template/ bootloader.txt` and `pyportable_installer/no3_build_pyproject.py:: func:_create_launcher`). Notes: table of `pyarmor obfuscate --bootstrap {0~4}` | command | result | | ================== | =========================================== | | `pyarmor obfuscate | each obfuscated file has a header of | | --bootstrap 0` | `from .pytransform import pyarmor_runtime` | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | only `__init__.py` has a header of | | --bootstrap 1` | `from .pytransform import pyarmor_runtime` | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | each obfuscated file has a header of | | --bootstrap 2` | `from pytransform import pyarmor_runtime` | | | **this is what we want** | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | *unknown* | | --bootstrap 3` | | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | *unknown* | | --bootstrap 4` | | """ lk.loga('compiling', ospath.basename(src_file)) if self._liscense == 'trial': # The limitation of content size is 32768 bytes (i.e. 32KB) in # pyarmor trial version. if (size := ospath.getsize(src_file)) > 32768: lk.logt( '[W0357]', f'该文件: "{src_file}" 的体积 ({size}) 超出了 pyarmor 试用' f'版的限制, 请购买个人版或商业版许可后重新编译! (本文件谨以' f'源码形式打包)') copyfile(src_file, dst_file) return
def handle_only_folders(dir_i, dir_o): global _excludes out = [] for dp, dn in filesniff.findall_dirs( dir_i, fmt='zip', exclude_protected_folders=False # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # set this param to False, we will use our own exclusion rule # (i.e. `globals:_excludes.is_protected`) instead. ): if _excludes.is_protected(dn, dp): lk.logt('[D1409]', 'skip making dir', dp) continue subdir_i, subdir_o = dp, dp.replace(dir_i, dir_o, 1) os.mkdir(subdir_o) out.append((subdir_i, subdir_o)) return out
def main(pyproj_file: TPath, misc: TMisc) -> TConf: """ 几个关键目录的区分和说明: `../docs/devnote/difference-between-roots.md` """ if misc.get('log_verbose', False) is False: lk.lite_mode = True prj_conf = step1(pyproj_file) ________ = step2(prj_conf) # 下划线只是为了让代码看起来整齐, 无实际意义 dst_root = step3(prj_conf['app_name'], **prj_conf['build'], **misc) m, n = ospath.split(dst_root) lk.logt("[I2501]", f'See distributed project at \n\t"{m}:0" >> {n}') # this path link is clickable via pycharm console ^-----^ if misc.get('do_aftermath', True): from .aftermath import main as do_aftermath do_aftermath(prj_conf, dst_root) return prj_conf
def _trans(self, word: str, trim_symbols=True) -> str: word = re.sub(self._reg1, ' ', word) for k, v in self.cedilla_dict.items(): word = word.replace(k, v) word = self.strip_accents(word) if trim_symbols: for k, v in self.symbols_dict.items(): word = word.replace(k, v) if self._strict_mode: if x := self._reg3.findall(word): # uniq: unique; unreg: unregisted from lk_logger import lk lk.logt('[DiacriticalMarksCleaner][W2557]', 'found uniq and unreg symbol', word, x, h='parent') word = re.sub(self._reg3, '', word)
def _build_venv_by_pip(requirements: TRequirements, pypi_url, local, offline, pyversion, quiet=False): """ Args: requirements: pypi_url: str. suggest list: official https://pypi.python.org/simple/ tsinghua https://pypi.tuna.tsinghua.edu.cn/simple/ aliyun http://mirrors.aliyun.com/pypi/simple/ ustc https://pypi.mirrors.ustc.edu.cn/simple/ douban http://pypi.douban.com/simple/ local: offline: pyversion: quiet: Warnings: 请勿随意更改本函数的参数名, 这些名字与 `typehint.py:_TPipOptions` 的成员 名有关系, 二者名字需要保持一致. """ if not requirements: # `requirements` is empty string or empty list return if offline is False: assert pypi_url host = pypi_url \ .removeprefix('http://') \ .removeprefix('https://') \ .split('/', 1)[0] else: host = '' find_links = f'--find-links={local}' if local else '' no_index = '--no-index' if offline else '' pypi_url = f'--index-url {pypi_url}' if not offline else '' quiet = '--quiet' if quiet else '' trusted_host = f'--trusted-host {host}' if host else '' if isinstance(requirements, str): assert ospath.exists(requirements) # see cmd help: `pip download -h` send_cmd( f'pip download' f' -r "{requirements}"' f' --disable-pip-version-check' f' --python-version {pyversion}' f' {find_links}' f' {no_index}' f' {pypi_url}' f' {quiet}' f' {trusted_host}', ignore_errors=True ) else: for pkg in requirements: try: send_cmd(pkg) except: lk.logt('[W0835]', 'Pip installing failed', pkg)
def create_venv( embed_py_mgr: EmbedPythonManager, venv_options: TVenvBuildConf, root_dir: TPath, mode: THowVenvCreated ): if mode == 'empty_folder': mkdirs(root_dir, 'venv', 'site-packages') return # 这里的 `src_venv_dir` 无实际意义, 只是为了让代码看起来整齐. 我们把它作为参 # 数传到 `_copy_site_packages:[params]kwargs` 也无实际意义, 因为我们最终用到 # 的是由 `venv_options` 提供的路径信息. src_venv_dir = '' dst_venv_dir = f'{root_dir}/venv' embed_py_dir = embed_py_mgr.get_embed_python_dir() if mode == 'copy': _copy_python_interpreter(embed_py_dir, dst_venv_dir) _copy_site_packages( venv_options, pyversion=embed_py_mgr.pyversion, # `embed_py_mgr.pyversion` is equivalent to # `venv_options['python_version']` src_venv_dir=src_venv_dir, dst_venv_dir=dst_venv_dir, embed_py_dir=embed_py_dir, ) return if mode == 'symbolink': def _is_the_same_driver(a: TPath, b: TPath): return a.lstrip('/').split('/', 1)[0] == \ b.lstrip('/').split('/', 1)[0] if _is_the_same_driver(embed_py_dir, dst_venv_dir): mklink(embed_py_dir, dst_venv_dir, exist_ok=True) else: # noinspection PyUnusedLocal mode = 'copy' _copy_python_interpreter(embed_py_dir, dst_venv_dir) if _is_the_same_driver(src_venv_dir, dst_venv_dir): mklink(f'{src_venv_dir}/lib/site-packages', f'{dst_venv_dir}/site-packages') else: # noinspection PyUnusedLocal mode = 'empty_folder' lk.logt( '[W3359]', ''' cannot create symbol link acrossing different drivers: {} --x--> {} will do nothing instead. you need to copy them by manual after installer finished. '''.format( f'{src_venv_dir}/lib/site-packages', f'{dst_venv_dir}/site-packages' ) ) return raise Exception('Unexpected error, this code should be unreachable.', mode)
class PyArmorCompiler(BaseCompiler): # noinspection PyMissingConstructor def __init__(self, python_interpreter=''): """ Args: python_interpreter: 如果启用了虚拟环境, 则这里传入 `.venv_builder: VEnvBuilder:get_embed_python:returns`; 如果没使用虚拟环境, 则留 空, 它将使用默认 (全局的) python 解释器和 pyarmor. 此时请确保该 解释器和 pyarmor 都可以在 cmd 中访问 (测试命令: `python --version`, `pyarmor --version`). 参考: https://pyarmor.readthedocs.io/zh/latest/advanced.html 子章节: "使用不同版本 Python 加密脚本" Warnings: Currently only supports Windows platform. """ self._liscense = 'trial' # TODO: see https://pyarmor.readthedocs.io/zh/latest/license.html if python_interpreter: # warnings: `docs/devnote/warnings-about-embed-python.md` self._interpreter = python_interpreter.replace('/', '\\') # create pyarmor file copy origin_file = self._locate_pyarmor_script() backup_file = origin_file.removesuffix('.py') + '_copy.py' if not ospath.exists(backup_file): from lk_utils.read_and_write import loads, dumps content = loads(origin_file) dumps( '\n'.join([ 'from os.path import dirname', 'from sys import path as syspath', 'syspath.append(dirname(__file__))', content ]), backup_file) self._pyarmor = backup_file self._head = f'"{self._interpreter}" "{self._pyarmor}"' else: self._interpreter = 'python' self._pyarmor = 'pyarmor' self._head = 'pyarmor' @staticmethod def _locate_pyarmor_script(): try: import pyarmor except ImportError as e: print( 'Error: could\'t find pyarmor lib! Please make sure you have ' 'installed pyarmor (by `pip install pyarmor`).') raise e file = ospath.abspath(f'{pyarmor.__file__}/../pyarmor.py') assert ospath.exists(file) return file def generate_runtime(self, local_dir: str, lib_dir: str): from shutil import copytree dir_i = f'{local_dir}/pytransform' dir_o = f'{lib_dir}/pytransform' if not ospath.exists(dir_i): send_cmd(f'{self._head} runtime -O "{local_dir}"') # note the target dir is `local_dir`, not `dir_i` # see `cmd:pyarmor runtime -h` copytree(dir_i, dir_o) def compile_all(self, pyfiles: list[str]): """ References: docs/devnote/how-does-pytransform-work.md """ for src_file in pyfiles: dst_file = global_dirs.to_dist(src_file) self.compile_one(src_file, dst_file) @new_thread def compile_one(self, src_file, dst_file): """ Compile `src_file` and generate `dst_file`. Args: src_file dst_file References: `cmd:pyarmor obfuscate -h` Results: the `dst_file` has the same content structure: from pytransform import pyarmor_runtime pyarmor_runtime() __pyarmor__(__name__, __file__, b'\\x50\\x59\\x41...') `pytransform` comes from `{dist}/lib`, it will be added to `sys.path` in the startup (see `pyportable_installer/template/ bootloader.txt` and `pyportable_installer/no3_build_pyproject.py:: func:_create_launcher`). Notes: table of `pyarmor obfuscate --bootstrap {0~4}` | command | result | | ================== | =========================================== | | `pyarmor obfuscate | each obfuscated file has a header of | | --bootstrap 0` | `from .pytransform import pyarmor_runtime` | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | only `__init__.py` has a header of | | --bootstrap 1` | `from .pytransform import pyarmor_runtime` | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | each obfuscated file has a header of | | --bootstrap 2` | `from pytransform import pyarmor_runtime` | | | **this is what we want** | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | *unknown* | | --bootstrap 3` | | | ------------------ | ------------------------------------------- | | `pyarmor obfuscate | *unknown* | | --bootstrap 4` | | """ lk.loga('compiling', ospath.basename(src_file)) if self._liscense == 'trial': # The limitation of content size is 32768 bytes (i.e. 32KB) in # pyarmor trial version. if (size := ospath.getsize(src_file)) > 32768: lk.logt( '[W0357]', f'该文件: "{src_file}" 的体积 ({size}) 超出了 pyarmor 试用' f'版的限制, 请购买个人版或商业版许可后重新编译! (本文件谨以' f'源码形式打包)') copyfile(src_file, dst_file) return # 注意: 虽然我们已经事先判断了, 如果文件体积大于试用版 pyarmor 的限 # 制就不用 pyarmor 编译, 但是事实上仍会发生极少数 py 文件体积未超过 # 限制却被 pyarmor 报超出限制的错误. 该现象目前在编译虚拟环境下的 # gb2312.py 文件遇到过一次. 为了处理该报错, 我们需要从 subprocess 中 # 获取到错误, 并转为拷贝源码的方式. 详见下面的处理. cmd = (f'{self._head} --silent obfuscate' f' -O "{ospath.dirname(dst_file)}"' f' --bootstrap 2' f' --exact' f' --no-runtime' f' "{src_file}"') # arguments: # --silent do not print normal info # --output output path, pass `dst_file`'s dirname, it will # generate a compiled file under and has the same # name with `src_file` # --bootstrap 2 see `docstring:notes:table` # --exact only obfuscate the listed script(s) (here we # only obfuscate `src_file`) # --no-runtime do not generate runtime files (cause we have # generated runtime files in `{dst}/lib`) try: send_cmd(cmd) except Exception as e: lk.logt('[E1747]', e) from os import remove remove(dst_file) copyfile(src_file, dst_file)
def main(file_i, file_o, qtdoc_dir: str): """ Args: file_i: '~/resources/no4_all_qml_types.json'. see `no2_all_qml_types.py` file_o: '~/resources/no5_all_qml_widgets.json' qtdoc_dir: 请传入您的 Qt 安装程序的 Docs 目录. 例如: 'D:/Programs/Qt /Docs/Qt-5.14.2' (该路径须确实存在) """ reader = loads(file_i) # type: dict writer = defaultdict(lambda: defaultdict(lambda: { 'parent': '', 'props': {}, })) assert exists(qtdoc_dir), ('The Qt Docs directory doesn\'t exist!', qtdoc_dir) for module, qmltype, file_i in _get_files(reader, qtdoc_dir): lk.logax(qmltype) if not exists(file_i): lk.logt('[I3924]', 'file not found', qmltype) continue soup = BeautifulSoup(loads(file_i), 'html.parser') # 以 '{qtdoc_dir}/qtquick/qml-qtquick-rectangle.html' 为例分析 (请在 # 浏览器中查看此 html, 打开开发者工具. try: # get parent ''' <table class="alignedsummary"> <tr>...</tr> # 目标可能在第二个 tr, 也可能在第三个 tr. 例如 Rectangle 和 # Button 的详情页. 有没有其他情况不太清楚 (没有做相关测试). 安全 # 起见, 请逐个 tr 进行检查. <tr> <td class="memItemLeft rightAlign topAlign"> Inherits:</td> <td class="memItemRight bottomAlign"> <p> <a href="qml-qtquick-item.html">Item</a> </p> </td> </tr> ... </table> ''' parent = '' e = soup.find('table', 'alignedsummary') for tr in e.find_all('tr'): if tr.td.text.strip() == 'Inherits:': td = tr.find('td', 'memItemRight bottomAlign') parent = td.text.strip() break except AttributeError: parent = '' try: # props e = soup.find(id='properties') e = e.find_next_sibling('ul') props = {} for li in e.find_all('li'): ''' <li class="fn"> ... <a href=...>border</a> # this is `prop` # border 的值的类型是空, 我们用 'group' 替代 ... <ul> <li class="fn"> ... <a href=...>border.color</a> # this is `prop` ... " : color" # this is `type` </li> ... </ul> </li> References: https://blog.csdn.net/Kwoky/article/details/82890689 ''' # `p` and `t` means 'property' and 'type' p = li.a.text t = li.contents[-1].strip(' :').strip() # 后一个 strip 是为了去除未知的空白符, 比如换行符或者其他看不 # 见的字符 (后者通常是 html 数据不规范引起的) assert isinstance(t, str) if t == '': t = 'group' props[p] = t except AttributeError: props = {} writer[module][qmltype]['parent'] = parent writer[module][qmltype]['props'].update(props) del soup dumps(writer, file_o)
def _mkdir(dir_): if dir_ not in existed: existed.add(dir_) if not ospath.exists(dir_): lk.logt('[D0604]', 'creat empty folder', dir_) mkdir(dir_)