7

我们在我们的设施中广泛使用 py2app 来生成自包含的 .app 包,以便于内部部署而不会出现依赖性问题。我最近注意到但不知道它是如何开始的,那就是在构建 .app 时,py2app 开始包含我们主库的 .git 目录。

例如,commonLib 是我们的根 python 库包,它是一个 git repo。在这个包下是各种子包,如数据库、实用程序等。

commonLib/
    |- .git/ # because commonLib is a git repo
    |- __init__.py
    |- database/
        |- __init__.py
    |- utility/
        |- __init__.py
    # ... etc

在一个给定的项目中,比如说 Foo,我们会像from commonLib import xyz使用我们的通用包一样进行导入。通过 py2app 构建看起来像:python setup.py py2app

所以我最近看到的问题是,在为项目 Foo 构建应用程序时,我会看到它包含 commonLib/.git/ 中的所有内容到应用程序中,这是额外的膨胀。py2app 有一个 excludes 选项,但这似乎只适用于 python 模块。我无法完全弄清楚排除 .git 子目录需要什么,或者实际上是什么导致它首先被包含在内。

有没有人在使用作为 git repo 的 python 包导入时遇到过这种情况?每个项目的 setup.py 文件没有任何变化,commonLib 一直是一个 git 存储库。所以我唯一能想到的变量是 py2app 的版本及其明显随着时间的推移而升级的 deps。

编辑

截至目前,我正在使用最新的 py2app 0.6.4。此外,我的 setup.py 是不久前从 py2applet 生成的,但此后一直手动配置并复制为每个新项目的模板。我对这些项目中的每一个都使用 PyQt4/sip,所以这也让我想知道它是否与其中一个食谱有问题?

更新

从第一个答案开始,我尝试使用各种exclude_package_data设置组合来解决此问题。似乎没有什么可以强制 .git 目录被排除在外。以下是我的 setup.py 文件的一般示例:

from setuptools import setup
from myApp import VERSION

appname = 'MyApp'
APP = ['myApp.py']
DATA_FILES = []
OPTIONS = {
    'includes': 'atexit, sip, PyQt4.QtCore, PyQt4.QtGui',
    'strip': True, 
    'iconfile':'ui/myApp.icns', 
    'resources':['src/myApp.png'], 
    'plist':{
        'CFBundleIconFile':'ui/myApp.icns',
        'CFBundleIdentifier':'com.company.myApp',
        'CFBundleGetInfoString': appname,
        'CFBundleVersion' : VERSION,
        'CFBundleShortVersionString' : VERSION
        }
    }

setup(
    app=APP,
    data_files=DATA_FILES,
    options={'py2app': OPTIONS},
    setup_requires=['py2app'],
)

我试过这样的事情:

setup(
    ...
    exclude_package_data = { 'commonLib': ['.git'] },
    #exclude_package_data = { '': ['.git'] },
    #exclude_package_data = { 'commonLib/.git/': ['*'] },
    #exclude_package_data = { '.git': ['*'] },
    ...
)

更新#2

我已经发布了我自己的答案,它在 distutils 上做了一个猴子补丁。它丑陋且不受欢迎,但在有人可以为我提供更好的解决方案之前,我想这就是我所拥有的。

4

4 回答 4

3

我正在为我自己的问题添加一个答案,以记录迄今为止我发现的唯一可行的方法。我的方法是monkeypatch distutils 在创建目录或复制文件时忽略某些模式。这真的不是我想做的,但就像我说的,它是迄今为止唯一有效的东西。

## setup.py ##

import re

# file_util has to come first because dir_util uses it
from distutils import file_util, dir_util

def wrapper(fn):
    def wrapped(src, *args, **kwargs):
        if not re.search(r'/\.git/?', src):
            fn(src, *args, **kwargs) 
    return wrapped       

file_util.copy_file = wrapper(file_util.copy_file)
dir_util.mkpath = wrapper(dir_util.mkpath)

# now import setuptools so it uses the monkeypatched methods
from setuptools import setup

希望有人对此发表评论并告诉我更高级别的方法来避免这样做。但到目前为止,我可能会将其包装成一个实用方法exclude_data_patterns(re_pattern),以便在我的项目中重用。

于 2012-03-30T19:37:29.647 回答
1

我可以看到排除 .git 目录的两个选项。

  1. 从代码的“干净”签出构建应用程序。部署新版本时,我们始终svn export基于标签从新版本构建,以确保我们不会拾取虚假的更改/文件。您可以在此处尝试等效项 - 尽管 git 等效项似乎 涉及更多

  2. 修改setup.py文件以按摩应用程序中包含的文件。这可以使用文档exclude_package_data中描述的功能来完成,或者构建列表并将其传递给.data_filessetup

至于为什么它突然开始发生,知道你正在使用的 py2app 版本可能会有所帮助,知道你的 setup.py 的内容以及它是如何制作的(手工或使用 py2applet)也会有所帮助。

于 2012-03-29T05:08:13.527 回答
1

我对 Pyinstaller 有类似的经验,所以我不确定它是否直接适用。

Pyinstaller 在运行导出过程之前创建要包含在分发中的所有文件的“清单”。根据 Mark 的第二个建议,您可以“按摩”此清单,以排除您想要的任何文件。包括 .git 或 .git 本身中的任何内容。

最后,我坚持在生成二进制文件之前检查我的代码,因为不仅仅是 .git 膨胀(例如 UML 文档和 Qt 的原始资源文件)。结帐保证了一个干净的结果,我在自动化该过程以及为二进制文件创建安装程序的过程中没有遇到任何问题。

于 2014-01-09T12:09:52.270 回答
1

对此有一个很好的答案,但我有一个更详细的答案来解决这里提到的问题,使用白名单方法。为了让猴子补丁也适用于外部的包,site-packages.zip我还必须猴子补丁copy_tree(因为它copy_file在其函数内部导入),这有助于制作独立的应用程序。

此外,我创建了一个白名单配方来标记某些包 zip-unsafe。该方法可以轻松添加除白名单之外的过滤器。

import pkgutil
from os.path import join, dirname, realpath
from distutils import log

# file_util has to come first because dir_util uses it
from distutils import file_util, dir_util
# noinspection PyUnresolvedReferences
from py2app import util


def keep_only_filter(base_mod, sub_mods):
    prefix = join(realpath(dirname(base_mod.filename)), '')
    all_prefix = [join(prefix, sm) for sm in sub_mods]
    log.info("Set filter for prefix %s" % prefix)

    def wrapped(mod):
        name = getattr(mod, 'filename', None)
        if name is None:
            # ignore anything that does not have file name
            return True
        name = join(realpath(dirname(name)), '')
        if not name.startswith(prefix):
            # ignore those that are not in this prefix
            return True
        for p in all_prefix:
            if name.startswith(p):
                return True
        # log.info('ignoring %s' % name)
        return False
    return wrapped

# define all the filters we need
all_filts = {
    'mypackage': (keep_only_filter, [
        'subpackage1', 'subpackage2',
    ]),
}


def keep_only_wrapper(fn, is_dir=False):
    filts = [(f, k[1]) for (f, k) in all_filts.iteritems()
             if k[0] == keep_only_filter]
    prefixes = {}
    for f, sms in filts:
        pkg = pkgutil.get_loader(f)
        assert pkg, '{f} package not found'.format(f=f)
        p = join(pkg.filename, '')
        sp = [join(p, sm, '') for sm in sms]
        prefixes[p] = sp

    def wrapped(src, *args, **kwargs):
        name = src
        if not is_dir:
            name = dirname(src)
        name = join(realpath(name), '')
        keep = True
        for prefix, sub_prefixes in prefixes.iteritems():
            if name == prefix:
                # let the root pass
                continue
            # if it is a package we have a filter for
            if name.startswith(prefix):
                keep = False
                for sub_prefix in sub_prefixes:
                    if name.startswith(sub_prefix):
                        keep = True
                        break
        if keep:
            return fn(src, *args, **kwargs)
        return []

    return wrapped

file_util.copy_file = keep_only_wrapper(file_util.copy_file)
dir_util.mkpath = keep_only_wrapper(dir_util.mkpath, is_dir=True)
util.copy_tree = keep_only_wrapper(util.copy_tree, is_dir=True)


class ZipUnsafe(object):
    def __init__(self, _module, _filt):
        self.module = _module
        self.filt = _filt

    def check(self, dist, mf):
        m = mf.findNode(self.module)
        if m is None:
            return None

        # Do not put this package in site-packages.zip
        if self.filt:
            return dict(
                packages=[self.module],
                filters=[self.filt[0](m, self.filt[1])],
            )
        return dict(
            packages=[self.module]
        )

# Any package that is zip-unsafe (uses __file__ ,... ) should be added here 
# noinspection PyUnresolvedReferences
import py2app.recipes
for module in [
        'sklearn', 'mypackage',
]:
    filt = all_filts.get(module)
    setattr(py2app.recipes, module, ZipUnsafe(module, filt))
于 2015-09-28T18:49:27.050 回答