py2app recogiendo el subdirectorio .git de un paquete durante la comstackción

Utilizamos py2app ampliamente en nuestras instalaciones para producir paquetes .app autocontenidos para una fácil implementación interna sin problemas de dependencia. Algo que noté recientemente, y no tengo idea de cómo comenzó, es que al crear un .app, py2app comenzó a incluir el directorio .git de nuestra biblioteca principal.

commonLib, por ejemplo, es nuestro paquete de bibliotecas de python raíz, que es un repository git. Debajo de este paquete se encuentran los diversos subpaquetes, como la base de datos, la utilidad, etc.

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

En un proyecto dado, digamos Foo, haremos importaciones como from commonLib import xyz para usar nuestros paquetes comunes. La construcción a través de py2app se ve algo como: python setup.py py2app

Así que el problema reciente que estoy viendo es que al crear una aplicación para el proyecto Foo, veré que incluya todo en commonLib / .git / en la aplicación, lo cual es una gran cantidad de información. py2app tiene una opción de exclusión, pero eso parece ser solo para los módulos de Python. No puedo descifrar lo que se necesitaría para excluir el subdirector .git o, de hecho, lo que hace que se incluya en primer lugar.

¿Alguien ha experimentado esto al usar una importación de paquetes python que es un repository git? Nada ha cambiado en nuestros archivos setup.py para cada proyecto, y commonLib siempre ha sido un repository git. Así que lo único que puedo pensar de ser una variable es la versión de py2app y sus funciones que, obviamente, se han actualizado con el tiempo.

Editar

Estoy usando el último py2app 0.6.4 a partir de ahora. Además, mi setup.py se generó por primera vez desde py2applet hace un tiempo, pero se ha configurado manualmente desde entonces y se ha copiado como una plantilla para cada nuevo proyecto. Estoy usando PyQt4 / sip para cada uno de estos proyectos, por lo que también me pregunto si es un problema con una de las recetas.

Actualizar

Desde la primera respuesta, traté de arreglar esto usando varias combinaciones de configuraciones exclude_package_data . Nada parece forzar al directorio .git a quedar excluido. Aquí hay una muestra de cómo se ven mis archivos setup.py en general:

 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'], ) 

He intentado cosas como:

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

Actualización # 2

He publicado mi propia respuesta que hace un monkeypatch en distutils. Es feo y no preferido, pero hasta que alguien me pueda ofrecer una mejor solución, supongo que esto es lo que tengo.

Estoy agregando una respuesta a mi propia pregunta, para documentar lo único que he encontrado para trabajar hasta ahora. Mi enfoque fue hacer perpuntes parásitos para ignorar ciertos patrones al crear un directorio o copiar un archivo. Esto realmente no es lo que quería hacer, pero como he dicho, es lo único que funciona hasta ahora.

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

Esperemos que alguien comente sobre esto y me diga un enfoque de nivel superior para evitar hacer esto. Pero a partir de ahora, probablemente lo exclude_data_patterns(re_pattern) en un método de utilidad como exclude_data_patterns(re_pattern) para reutilizarlo en mis proyectos.

Puedo ver dos opciones para excluir el directorio .git.

  1. Cree la aplicación a partir de una verificación “limpia” del código. Al implementar una nueva versión, siempre comstackmos a partir de una nueva svn export basada en una etiqueta para asegurarnos de que no detectamos cambios / archivos espurios. Puedes probar el equivalente aquí, aunque el equivalente de git parece algo más complicado .

  2. Modifique el archivo setup.py para masajear los archivos incluidos en la aplicación. Esto se puede hacer usando la funcionalidad exclude_package_data como se describe en los documentos , o compile la lista de data_files de data_files y data_files a la setup .

En cuanto a por qué comenzó a suceder repentinamente, conocer la versión de py2app que está utilizando podría ayudar, al igual que conocer el contenido de su setup.py y tal vez cómo se hizo (a mano o usando py2applet).

Tengo una experiencia similar con Pyinstaller, así que no estoy seguro de que se aplique directamente.

Pyinstaller crea un “manifiesto” de todos los archivos que se incluirán en la distribución, antes de ejecutar el proceso de exportación. Podría “masajear” este manifiesto, según la segunda sugerencia de Mark, para excluir cualquier archivo que desee. Incluyendo cualquier cosa dentro de .git o .git mismo.

Al final, me limité a revisar mi código antes de crear un binario, ya que había algo más que .git inflado (como documentos UML y archivos de recursos en bruto para Qt). Una comprobación garantizó un resultado limpio y no experimenté problemas al automatizar ese proceso junto con el proceso de creación del instalador para el binario.

Hay una buena respuesta a esto, pero tengo una respuesta más elaborada para resolver el problema mencionado aquí con un enfoque de lista blanca. Para que el parche de mono también funcione para paquetes fuera de site-packages.zip Tuve que parchar parche también copy_tree (porque importa copy_file dentro de su función), esto ayuda a hacer una aplicación independiente.

Además, creo una receta en lista blanca para marcar ciertos paquetes zip-unsafe. El enfoque facilita la adición de filtros distintos de la lista blanca.

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