py2app собирает .git подкаталог пакета во время сборки

Мы широко используем py2app на нашем предприятии для создания автономных пакетов .app для простого внутреннего развертывания без проблем с зависимостями. Что-то, что я недавно заметил и понятия не имею, как это началось, заключается в том, что при создании .app py2app начал включать каталог .git нашей основной библиотеки.

Например, commonLib — это наш корневой пакет библиотеки Python, который представляет собой репозиторий git. В этом пакете находятся различные подпакеты, такие как база данных, утилита и т. д.

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 имеет опцию exclude, но, похоже, это только для модулей python. Я не могу понять, что нужно, чтобы исключить подкаталог .git, или, на самом деле, что заставляет его быть включенным в первую очередь.

Кто-нибудь сталкивался с этим при использовании импорта пакета python, который является репозиторием git? В наших файлах setup.py для каждого проекта ничего не изменилось, а commonLib всегда был репозиторием git. Таким образом, единственное, что я могу представить как переменную, — это версия py2app и ее приложений, которые, очевидно, со временем обновлялись.

Изменить

На данный момент я использую последнюю версию 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. Это уродливо и нежелательно, но пока кто-то не предложит мне лучшее решение, я думаю, это то, что у меня есть.


person jdi    schedule 23.03.2012    source источник


Ответы (4)


Я добавляю ответ на свой вопрос, чтобы задокументировать единственное, что я нашел работающим до сих пор. Мой подход заключался в том, чтобы обезвреживать 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), для повторного использования в моих проектах.

person jdi    schedule 30.03.2012
comment
Я думаю, что это единственный вариант здесь, за исключением того, что ваш скрипт делает чистую (.git-менее) копию репо перед его упаковкой. - person Etienne Perot; 01.04.2012
comment
@EtiennePerot: Спасибо за подтверждение. Я надеялся, что просто не упустил какое-то другое очевидное, более элегантное решение. Поскольку я ссылаюсь на более чем одно репо (без подмодулей) в разных проектах для моего импорта, было бы огромной головной болью, если бы пришлось экспортировать их все в одно место и делать env на лету для сборки. - person jdi; 01.04.2012
comment
спасибо, вы не знаете, так ли это по-прежнему (нет лучшего способа исключить определенные шаблоны)? Я хочу исключить из него в основном то, что у меня есть в моем .gitignore. - person dashesy; 26.09.2015
comment
@dashesy Вероятно, это все еще относится к вашей проблеме. Но вместо того, чтобы предоставлять жестко закодированный шаблон, как я, вам нужно будет прочитать файл игнорирования git и применить каждый шаблон. - person jdi; 26.09.2015
comment
Последний py2app автоматически фильтрует .git, '.svn', ... но это решение помогло мне создать белый список. - person dashesy; 28.09.2015

Я вижу два варианта исключения каталога .git.

  1. Создайте приложение из «чистой» проверки кода. При развертывании новой версии мы всегда строим из нового svn export на основе тега, чтобы гарантировать, что мы не получим ложные изменения/файлы. Вы можете попробовать эквивалент здесь, хотя эквивалент git кажется несколько более участие.

  2. Измените файл setup.py, чтобы указать файлы, включенные в приложение. Это можно сделать с помощью функции exclude_package_data, как описано в документах, или создайте список data_files и передайте его setup.

Что касается того, почему это внезапно начало происходить, может помочь знание используемой вами версии py2app, а также знание содержимого вашего setup.py и, возможно, того, как это было сделано (вручную или с помощью py2applet).

person Mark Streatfield    schedule 29.03.2012
comment
Спасибо за ответ. Первый вариант кажется громоздким слоем, который мне пришлось бы добавить ко всем нашим процедурам py2app. Проект будет одним репозиторием, а затем он будет ссылаться на нашу стандартную библиотеку в месте выпуска объекта. В миксе может быть несколько репозиториев, и они не являются подмодулями. Так что мне пришлось бы экспортировать и консолидировать их все. Что касается второго предложения, я рассмотрю это завтра и сообщу и обновлю! - person jdi; 29.03.2012
comment
Я обновил свой вопрос информацией после тестирования подхода № 2. До сих пор не получается исключить :-( - person jdi; 30.03.2012

У меня есть аналогичный опыт работы с Pyinstaller, поэтому я не уверен, что это применимо напрямую.

Pyinstaller создает «манифест» всех файлов, которые будут включены в дистрибутив, перед запуском процесса экспорта. Вы можете «массировать» этот манифест, согласно второму предложению Марка, чтобы исключить любые файлы, которые вы хотите. Включая что-либо внутри .git или самого .git.

В конце концов, я остановился на проверке своего кода перед созданием двоичного файла, поскольку раздувался не только .git (например, документы UML и необработанные файлы ресурсов для Qt). Проверка гарантировала чистый результат, и у меня не возникло проблем с автоматизацией этого процесса вместе с процессом создания установщика для двоичного файла.

person Marcus Ottosson    schedule 09.01.2014
comment
Спасибо, Маркус. Pyinstaller не использует подход distutils, такой как py2app/py2exe, поэтому решение будет другим. Я уверен, что это сработает, подобно тому, что вы сказали, вместо того, чтобы выполнять проверку git, фактически выполнить экспорт git из вашей текущей проверки во временное местоположение, а затем построить оттуда. экспорт git позволит вам контролировать точные шаблоны, но это был подход, которого я также пытался избежать, если бы его можно было решить просто из сценария установки distutils. - person jdi; 09.01.2014

На это есть хороший ответ, но у меня есть более сложный ответ, чтобы решить упомянутую здесь проблему с подходом белого списка. Чтобы патч для обезьяны также работал для пакетов вне site-packages.zip, мне пришлось также установить патч для обезьяны copy_tree (поскольку он импортирует copy_file внутри своей функции), это помогает в создании отдельного приложения.

Кроме того, я создаю рецепт белого списка, чтобы пометить определенные пакеты как небезопасные для zip. Этот подход упрощает добавление фильтров, отличных от белого списка.

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))
person dashesy    schedule 28.09.2015