我们在我们的工厂广泛使用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': ['*'] },
...
)
我已经发布了自己的答案,在distutils上做了一个monkeypatch。它的丑陋而不是首选,但在有人能为我提供更好的解决方案之前,我想这就是我所拥有的。
答案 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目录的选项。
从“干净”的代码检查中构建应用程序。在部署新版本时,我们始终基于标记从新的svn export
构建,以确保我们不会收集虚假的更改/文件。你可以在这里尝试相应的 - 尽管git等价似乎是somewhat more involved。
修改setup.py
文件以按摩应用程序中包含的文件。这可以使用docs中所述的exclude_package_data
功能完成,也可以构建data_files
列表并将其传递给setup
。
至于为什么它突然开始发生,知道你正在使用的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))