Перейти к содержанию

412 уроков, 25 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 14 из 20 Средний 15 мин 100 XP

py2exe и cx_Freeze: альтернативы сборки exe

Сборка exe без PyInstaller: py2exe — старейший упаковщик Windows, cx_Freeze — кросс-платформенный. Общий принцип, recipe на setup.py и правила выбора инструмента.

Редакция Питоники

PyInstaller — самый популярный упаковщик, но не единственный. Два ветерана жанра: py2exe — старейший сборщик для Windows, и cx_Freeze — кросс-платформенный инструмент с тем же типом рецепта. Урок показывает, что у всех упаковщиков внутри общее, чем различаются рецепты и как выбирать инструмент осознанно, а не по привычке.

Общий принцип у всех упаковщиков

Несмотря на разные команды и файлы настройки, результат у всех один: интерпретатор рядом с вашим кодом и зависимостями. Убедимся, собрав «бандлы» от имени трёх упаковщиков и заглянув внутрь каждого:

три упаковщика — одинаковый бандл
import os, zipfile

def build(packager):
    bundle = packager + '.exe'
    with zipfile.ZipFile(bundle, 'w') as z:
        z.writestr('runtime.bin', 'interpreter')
        z.writestr('__main__.py', "ARG = '" + packager + "'\nprint('Приложение собрано:', ARG)\n")
    return bundle

for packager in ('pyinstaller', 'py2exe', 'cx_freeze'):
    bundle = build(packager)
    with zipfile.ZipFile(bundle) as z:
        names = ', '.join(sorted(z.namelist()))
        entry = z.read('__main__.py').decode('utf-8')
    print(packager + ':', names)
    exec(compile(entry, '__main__.py', 'exec'), {'__name__': '__main__'})
    os.remove(bundle)
Вывод
pyinstaller: __main__.py, runtime.bin
Приложение собрано: pyinstaller
py2exe: __main__.py, runtime.bin
Приложение собрано: py2exe
cx_freeze: __main__.py, runtime.bin
Приложение собрано: cx_freeze

Разница упаковщиков не в содержимом бандла — внутри у всех интерпретатор, байткод и данные, — а в рецепте сборки и удобстве сопровождения. Настоящие бандлы сложнее: бутстрап, платформенные библиотеки, архивы байткода, — но принцип общий.

py2exe: старейший упаковщик Windows

py2exe появился в начале 2000-х и живёт до сих пор. Его ниша — Windows-приложения: консольные утилиты, GUI-программы и, что уникально среди упаковщиков, Windows-службы. Рецепт сборки — файл setup.py, как у библиотек:

setup.py для py2exe
# setup.py для py2exe
from distutils.core import setup
import py2exe

setup(console=['main.py'])
Файл настройки, а не запускаемый скрипт: его исполняет сам py2exe. Параметр console собирает консольное приложение; windowed — без окна консоли.
терминал: сборка py2exe
python setup.py py2exe
Команда терминала, в песочнице не выполняется: py2exe — нативный инструмент и существует только для Windows. Результат появляется в папке dist.

Границы py2exe честные: Windows-only — на других системах его нет вовсе; сообщество скромнее, чем у PyInstaller, поэтому готовых хуков под экзотические библиотеки меньше, и несовместимости разбирать придётся руками.

cx_Freeze: кросс-платформенный рецепт

cx_Freeze собирает под Windows и Linux из одного рецепта, на macOS — с оговорками. Стиль тот же: setup.py с описанием приложения:

setup.py для cx_Freeze
# setup.py для cx_Freeze
from cx_Freeze import setup, Executable

setup(
    name='mytool',
    version='1.0',
    executables=[Executable('main.py')],
)
Файл настройки; его исполняет cx_Freeze командой сборки. Это минимальный рецепт: имя, версия, точка входа.
терминал: установка и сборка cx_Freeze
pip install cx_Freeze
python setup.py build
Команды терминала, в песочнице не выполняются. Результат — папка build с подпапкой, имя которой включает платформу и версию Python, поэтому точное имя не приводим: внутри лежит запускаемый файл и библиотеки.

Настройка: excludes, includes и данные

В подробных рецептах опции сборки передают словарём options. Ключевые: excludes убирает ненужные модули, includes добавляет невидимые анализатору, include_files копирует данные рядом с exe:

setup.py с опциями build_exe
build_exe_options = {
    'packages': ['os'],
    'excludes': ['tkinter'],
    'include_files': ['config.json'],
}
setup(
    name='mytool',
    options={'build_exe': build_exe_options},
    executables=[Executable('main.py', base='Win32GUI')],
)
Рецепт выполняется cx_Freeze на реальной машине, в песочнице не запускается. base='Win32GUI' собирает приложение без окна консоли — аналог флага --noconsole у PyInstaller.

Логика excludes и includes легко проверяется на чистом Python: убираем исключённое из списка, добавляем пропущенное в конец:

excludes и includes на чистом Python
def resolve_bundle(modules, options):
    excluded = set(options.get('excludes', []))
    included = sorted(options.get('includes', []))
    result = []
    for name in modules:
        if name in excluded:
            continue
        result.append(name)
    for name in included:
        if name not in result:
            result.append(name)
    return result

modules = ['os', 'sys', 'json', 'tkinter', 'unittest']
options = {'excludes': ['tkinter'], 'includes': ['json']}
print('В бандл попадут:', ', '.join(resolve_bundle(modules, options)))
Вывод
В бандл попадут: os, sys, json, unittest

Модули, которых нет в списке анализа — например, импортируемые по вычисленному имени, — добавляются через includes. Тот же приём, что hiddenimports у PyInstaller: динамика невидима любому текстовому анализатору. Данные копируются включением в include_files:

данные рядом с exe
import os, shutil

os.makedirs('src', exist_ok=True)
os.makedirs('build', exist_ok=True)
with open('src/config.json', 'w', encoding='utf-8') as f:
    f.write('{"mode": "fast"}')

shutil.copyfile('src/config.json', 'build/config.json')
print('config рядом с exe:', os.path.isfile('build/config.json'))
with open('build/config.json', encoding='utf-8') as f:
    print('Содержимое:', f.read())
shutil.rmtree('src')
shutil.rmtree('build')
Вывод
config рядом с exe: True
Содержимое: {"mode": "fast"}

Консоль или окно: цена GUI-режима

Параметр base у Executable решает, будет ли у приложения консоль. Заодно он решает, куда пойдёт вывод программы — потоки существуют только у консольного варианта:

потоки у console и Win32GUI
def launch(base):
    streams = ['stdin', 'stdout', 'stderr']
    if base == 'Win32GUI':
        return []
    return streams

print('console:', ', '.join(launch('console')))
print('Win32GUI:', ', '.join(launch('Win32GUI')) or 'нет стандартных потоков')
Вывод
console: stdin, stdout, stderr
Win32GUI: нет стандартных потоков

В GUI-режиме print уходит в никуда, и падения невидимы: логирование в файл обязательно — то же правило, что и для --noconsole у PyInstaller.

Как упаковщик отличает stdlib от пакетов

Всем упаковщикам надо решить: что класть из стандартной библиотеки, а что копировать из окружения сборки. Граница описана в самом Python — списке имён stdlib:

граница стандартной библиотеки
import sys

candidates = ['os', 'json', 'sqlite3', 'requests', 'numpy']
for name in candidates:
    if name in sys.stdlib_module_names:
        kind = 'стандартная библиотека'
    else:
        kind = 'внешний пакет'
    print(name, '-', kind)
Вывод
os - стандартная библиотека
json - стандартная библиотека
sqlite3 - стандартная библиотека
requests - внешний пакет
numpy - внешний пакет

Стандартная библиотека приходит из рантайма бандла; внешние пакеты упаковщик копирует из окружения, в котором шла сборка. Поэтому окружение сборки делают чистым: устанавливают только то, что нужно приложению, иначе бандл разрастается чужими зависимостями.

Сравнение трёх упаковщиков

КритерийPyInstallerpy2execx_Freeze
ПлатформыWindows, macOS, LinuxТолько WindowsWindows, Linux (macOS с оговорками)
Рецепт сборкиКоманда плюс spec-файлsetup.pysetup.py
Windows-службыНетДаНет
Готовые хуки библиотекМногоМеньшеСредне
СообществоКрупнейшееСкромноеСреднее

Упаковщики разобраны все. Следующий урок — про то, что видно снаружи: декомпиляция pyc показывает, почему упаковка не защищает код и что именно восстанавливают декомпиляторы из байткода.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

def resolve_bundle(modules, options):
    excluded = set(options.get('excludes', []))
    included = sorted(options.get('includes', []))
    result = []
    for name in modules:
        if name in excluded:
            continue
        result.append(name)
    for name in included:
        if name not in result:
            result.append(name)
    return result

modules = ['os', 'sys', 're']
options = {'includes': ['yaml']}
print(', '.join(resolve_bundle(modules, options)))
def launch(base):
    streams = ['stdin', 'stdout', 'stderr']
    if base == 'Win32GUI':
        return []
    return streams

print(len(launch('console')), len(launch('Win32GUI')))
import sys

for name in ('re', 'flask'):
    if name in sys.stdlib_module_names:
        kind = 'из рантайма'
    else:
        kind = 'копируется из окружения'
    print(name, '-', kind)
Проверь себя
0 / 7

1. Что общего у бандлов PyInstaller, py2exe и cx_Freeze?

2. Где живут рецепты сборки py2exe и cx_Freeze?

3. На каких платформах работает py2exe?

4. Что делает опция excludes в рецепте cx_Freeze?

5. Что меняет base='Win32GUI' у Executable?

6. Каким будет Python внутри бандла, собранного cx_Freeze?

7. Как в cx_Freeze добавить модуль, невидимый текстовому анализу?

Карточки терминов
Запомнено: 0 / 7
Практика

Собери бандлы двумя упаковщиками: функция build(packager) создаёт zip-файл <packager>.exe с runtime.bin и точкой входа __main__.py. Проверь, что содержимое бандлов py2exe и cx_freeze совпадает, и выведи отсортированный список. Ожидаемый вывод: True и список.

practice.py
Вопросы и ответы по уроку

Что такое py2exe и когда его стоит выбрать?

Старейший упаковщик Python-приложений для Windows: собирает exe из setup.py-рецепта. Выбор оправдан, когда целевая платформа строго Windows и нужны редкие возможности вроде сборки Windows-служб. В остальных случаях проще PyInstaller.

Урок 10: PyInstaller — сборка exe

Чем cx_Freeze отличается от PyInstaller?

Рецептом и платформами: cx_Freeze описывает сборку декларативно в setup.py и собирает под Windows и Linux из одного рецепта; PyInstaller — командной строкой и spec-файлом, с самым большим набором готовых хуков под сторонние библиотеки.

Почему у py2exe и cx_Freeze рецепт setup.py, а не команды?

Оба выросли из инфраструктуры сборки библиотек, где setup.py — стандарт. Декларативный рецепт хранится в репозитории вместе с кодом, версионируется и повторяется командой одной строкой — это удобно для долгоживущих проектов.

Какой упаковщик выбрать по умолчанию?

PyInstaller: крупнейшее сообщество, готовые хуки под сотни библиотек, сборка одной командой. Альтернативы — для ниш: службы Windows и строгий Windows — py2exe; один рецепт под Windows и Linux — cx_Freeze; скорость исполнения — компилятор Nuitka.

Урок 12: Nuitka — компиляция в C

Скрывает ли упаковщик мой код?

Нет: внутри любого из них — байткод, из которого декомпиляторы восстанавливают читаемый исходник. Упаковка решает проблему распространения, не защиты. Сокрытие даёт нативная компиляция в машинный код.

Урок 15: декомпиляция pyc

Понравился урок? Сошлитесь на него

«Разница упаковщиков не в содержимом бандла — внутри у всех интерпретатор, байткод и данные, — а в рецепте сборки и удобстве сопровождения.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Что дальше

Осталось в разделе: 20 из 20. Прогресс хранится в вашем браузере.

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.