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
Example #2
0
 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
         )
Example #3
0
 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
Example #4
0
    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
Example #6
0
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
Example #7
0
    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)
Example #10
0
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)
Example #11
0
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_)