py2app在构建期间拾取包的.git子目录

时间:2012-03-23 19:45:36

标签: python macos setuptools distutils py2app

我们在我们的工厂广泛使用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有一个排除选项,但似乎只适用于python模块。我无法弄清楚如何排除.git子目录,或者实际上是什么原因导致它被包含在第一位。

有没有人在使用git repo的python包导入时遇到过这种情况? 我们在每个项目的setup.py文件中没有任何变化,而且commonLib一直是一个git repo。所以我唯一可以想到的变量就是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上做了一个monkeypatch。它的丑陋而不是首选,但在有人能为我提供更好的解决方案之前,我想这就是我所拥有的。

4 个答案:

答案 0 :(得分: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)这样的实用程序方法,以便在我的项目中重用。

答案 1 :(得分:1)

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

  1. 从“干净”的代码检查中构建应用程序。在部署新版本时,我们始终基于标记从新的svn export构建,以确保我们不会收集虚假的更改/文件。你可以在这里尝试相应的 - 尽管git等价似乎是somewhat more involved

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

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

答案 2 :(得分:1)

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

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

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

答案 3 :(得分:1)

对此有一个很好的答案,但我有一个更精细的答案,用白名单方法解决这里提到的问题。为了让猴子补丁也适用于site-packages.zip以外的包,我不得不修补补丁copy_tree(因为它在其函数中导入copy_file),这有助于创建一个独立的应用程序。

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

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))