py2app picking up .git subdir of a package during build
我们在工厂广泛使用py2app来生成独立的.app包,以便于内部部署,而不存在依赖关系问题。我最近注意到了一件事,不知道它是如何开始的,那就是当构建一个.app时,py2app开始包含我们主库的.git目录。
例如,commonlib是我们的根python库包,它是一个git repo。在这个包下面是各种子包,如数据库、实用程序等。
1 2 3 4 5 6 7 8 | commonLib/ |- .git/ # because commonLib is a git repo |- __init__.py |- database/ |- __init__.py |- utility/ |- __init__.py # ... etc |
在一个给定的项目中,比如foo,我们将像
所以我最近看到的一个问题是,在为项目foo构建应用程序时,我会看到它将commonlib/.git/中的所有内容都包含到应用程序中,这是一个额外的膨胀。py2app有一个excludes选项,但这似乎只适用于python模块。我不太明白排除.git子目录需要什么,或者实际上,是什么导致它首先被包括在其中。
有人在使用Git repo的python包导入时遇到过这种情况吗?在每个项目的setup.py文件中没有任何变化,commonlib始终是一个git repo。所以我唯一能想到的是PY2APP及其DEP的版本,它们显然是随着时间的推移而升级的。
编辑
我现在使用的是最新的PY2AP0.6.4。另外,我的setup.py是前一段时间从py2applet生成的,但从那时起就被手工配置,并作为每个新项目的模板复制过来。我对这些项目中的每一个都使用了pyqt4/sip,所以这也让我怀疑这是否是其中一个配方的问题?
更新从第一个答案开始,我尝试使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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'], ) |
号
我尝试过以下方法:
1 2 3 4 5 6 7 8 | setup( ... exclude_package_data = { 'commonLib': ['.git'] }, #exclude_package_data = { '': ['.git'] }, #exclude_package_data = { 'commonLib/.git/': ['*'] }, #exclude_package_data = { '.git': ['*'] }, ... ) |
更新2
我发布了自己的答案,在distutils上做monkeypatch。它很难看,也不受欢迎,但在有人能给我一个更好的解决方案之前,我想这就是我所拥有的。
我正在为自己的问题添加一个答案,以记录到目前为止我唯一找到的工作。我的方法是使用monkeypatch distuils在创建目录或复制文件时忽略某些模式。这真的不是我想做的,但正如我所说,这是目前唯一可行的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ## 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 |
希望有人会对此发表评论,并告诉我一个更高级别的方法来避免这样做。但到目前为止,我可能会把它包装成一个实用方法,比如
有一个很好的答案,但是我有一个更详细的答案来用白名单的方法解决这里提到的问题。为了让monkey补丁也适用于
此外,我还创建了一个白名单配方来标记某些包zip不安全。这种方法使得添加白名单以外的过滤器变得容易。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 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)) |
号
我在PyInstaller方面也有类似的经验,所以我不确定它是否直接适用。
在运行导出过程之前,pyinstaller创建要包含在分发中的所有文件的"清单"。根据马克的第二个建议,你可以"按摩"这个清单,以排除你想要的任何文件。包括.git或.git中的任何内容。
最后,在生成二进制文件之前,我一直在检查代码,因为不仅仅是.git膨胀(比如UML文档和qt的原始资源文件)。签出保证了一个干净的结果,并且在为二进制文件创建安装程序的过程中,我没有遇到使该过程自动化的问题。
我可以看到两个排除.git目录的选项。
从代码的"干净"签出中生成应用程序。在部署新版本时,我们总是基于一个标签从一个新的
修改
至于它突然开始发生的原因,了解您正在使用的py2app的版本可能会有所帮助,了解setup.py的内容,也许了解如何制作(手工或使用py2applet)。