Вы дорабатываете Python-проект в Windows, а сборку пакета с нативными расширениями поручаете облачному Mac. В журнале уже появилась запись об успешной сборке wheel, но после установки у коллеги возникает ошибка несовместимой архитектуры или не находится динамическая библиотека. Обычно дело не в том, удалось ли создать архив, а в его содержимом, путях к зависимостям и том, не использовался ли незаметно каталог исходников во время тестирования.
Возьмём существующий проект с pyproject.toml и выстроим три этапа приёмки: проверку тегов, нативных библиотек и установки в изолированное окружение. Публикацию в репозитории пакетов здесь не рассматриваем, а успешную локальную проверку не считаем доказательством совместимости со всеми системами.
1. Зафиксируйте интерпретатор и границы проверки
Выполните следующие команды в корне проекта на удалённом Mac. Используйте уже установленный Python, соответствующий требованиям проекта; не обновляйте интерпретатор по ходу проверки.
python3 -m venv .wheel-build
.wheel-build/bin/python -m pip --version
.wheel-build/bin/python -c \
'import platform, sys; print(sys.version); print(platform.machine()); print(sys.executable)'
sw_vers
xcodebuild -version
Сохраните вывод вместе с идентификатором коммита. Версия интерпретатора, архитектура процесса и версия системы — исходные данные для проверки. Сам по себе Mac на Apple Silicon ещё не означает, что текущий процесс Python работает в режиме arm64.
Определите, какую версию Python и какую минимальную версию macOS вы собираетесь поддерживать, а также содержит ли пакет нативные расширения. Даже пакет на чистом Python может зависеть от нативных пакетов, поэтому проверять только сам проект недостаточно.
Тег платформы в имени wheel-файла — это заявление о совместимости, а не средство преобразования. Если переименовать файл arm64 в universal2, в бинарном файле не появится вторая архитектура.
2. Начните с однозначно определённой сборки
Сначала убедитесь, что в dist не осталось пакетов от предыдущей сборки, затем создайте файл для текущей проверки. Зафиксируйте бэкенд сборки и его версию с помощью принятого в проекте механизма ограничений. Изоляция сборки, включённая по умолчанию, не фиксирует автоматически все сборочные зависимости.
mkdir -p dist
find dist -maxdepth 1 -name '*.whl' -print
.wheel-build/bin/python -m pip wheel \
--no-deps --wheel-dir dist .
Если предварительная проверка вывела старые файлы, сначала перенесите их в отдельный архивный каталог. Не смешивайте их с проверяемой сборкой. Параметр --no-deps означает лишь, что wheel не будут собираться для зависимостей времени выполнения; скачивание зависимостей на этапе сборки он не запрещает.
Следующая проверка требует, чтобы в каталоге находился ровно один wheel, и выводит его внутренние метаданные:
.wheel-build/bin/python - <<'PY'
from pathlib import Path
from zipfile import ZipFile
wheels = list(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("Expected exactly one wheel")
wheel = wheels[0]
print(wheel.name)
with ZipFile(wheel) as archive:
metadata = [
name for name in archive.namelist()
if name.endswith(".dist-info/WHEEL")
]
if len(metadata) != 1:
raise SystemExit("Invalid wheel metadata")
print(archive.read(metadata[0]).decode("utf-8"))
PY
Убедитесь, что значения Tag и совместимость, заявленная в имени файла, соответствуют целевой среде поставки. Тег py3-none-any обычно используется для платформонезависимых пакетов. Если внутри пакета есть нативные исполняемые файлы, нужно пересмотреть способ упаковки, а не просто довериться тегу.
3. Проверьте, не привязаны ли нативные библиотеки к машине сборки
Распакуйте wheel во временный каталог и проверьте модули расширения и динамические библиотеки. Следующий код только исследует файлы и не запускает программы из пакета:
inspect_dir="$(mktemp -d)"
.wheel-build/bin/python - "$inspect_dir" <<'PY'
from pathlib import Path
from zipfile import ZipFile
import sys
wheel, = Path("dist").glob("*.whl")
with ZipFile(wheel) as archive:
archive.extractall(sys.argv[1])
PY
find "$inspect_dir" -type f \
\( -name '*.so' -o -name '*.dylib' \) \
-exec file {} \; -exec otool -L {} \;
Используйте этот способ распаковки для доверенных артефактов собственного проекта. Не распаковывайте архивы из неизвестных источников прямо в рабочий каталог.
В первую очередь ищите ссылки на домашние каталоги пользователей, рабочие каталоги сборки и префиксы локальных пакетных менеджеров. На другой машине таких абсолютных путей часто нет. Пути к системным библиотекам нельзя безусловно считать ошибкой. Наличие @rpath или @loader_path тоже само по себе не гарантирует правильность: необходимо проверить, какие библиотеки действительно включены в пакет и как настроены пути их загрузки.
После изменений обязательно пересоберите wheel и повторите проверку. Нельзя исправить файлы только в распакованном каталоге, а пользователю передать исходный wheel без этих изменений. По завершении проверки удалите созданный временный каталог.
4. Выполните настоящую установку вне каталога исходников
Сначала соберите wheel и все зависимости времени выполнения в отдельном каталоге. На этом этапе может потребоваться доступ к сети. Версии зависимостей должны определяться ограничениями, принятыми в проекте для выпуска; итоговый список скачанных пакетов необходимо сохранить.
mkdir -p wheelhouse
.wheel-build/bin/python -m pip download \
--only-binary=:all: --dest wheelhouse dist/*.whl
Если для какой-либо зависимости нет подходящего бинарного пакета, этот шаг должен завершиться ошибкой. Не переходите незаметно к сборке из исходников: это уже меняет предмет проверки.
Следующие команды создают окружение вне дерева исходников. При запросе введите настоящее имя для импорта модуля проекта: оно может отличаться от имени дистрибутива.
read -r -p "Import module: " module
wheelhouse="$(pwd)/wheelhouse"
wheel="$(find "$(pwd)/dist" -maxdepth 1 -name '*.whl' -print)"
sandbox="$(mktemp -d)"
python3 -m venv "$sandbox/venv"
(
cd "$sandbox"
./venv/bin/python -m pip install \
--no-index --find-links "$wheelhouse" "$wheel"
./venv/bin/python -m pip check
./venv/bin/python -I - "$module" <<'PY'
import importlib
import sys
module = importlib.import_module(sys.argv[1])
print(getattr(module, "__file__", None))
print(list(getattr(module, "__path__", [])))
PY
)
Убедитесь, что модуль импортируется из нового окружения, а не из дерева исходников. pip check проверяет только соответствие объявленных зависимостей; загрузку нативных библиотек эта команда не проверяет и функции с отложенной загрузкой не вызывает. Поэтому дополнительно выполните прикладную операцию, которая действительно задействует модуль расширения: например, обработайте небольшой фиксированный набор данных и проверьте результат с помощью утверждения.
5. Включите критерии приёмки в запись о выпуске
Для каждого прогона сохраняйте как минимум идентификатор коммита, сведения об интерпретаторе, имя wheel-файла, контрольную сумму, список зависимостей и результаты прикладного smoke-теста. Запишите контрольную сумму командой shasum -a 256 dist/*.whl, чтобы убедиться, что передаёте именно тот файл, который только что проверили.
Разбирайте ошибки на соответствующем уровне: при несоответствии тегов вернитесь к целевым параметрам сборки, при отсутствии динамических библиотек — к сбору зависимостей, а если импорт работает только из каталога исходников — проверьте правила обнаружения пакетов и объявления ресурсов. Не маскируйте отсутствие файлов расширением путей поиска.
В завершение повторите установку и запуск на минимальной заявленной версии системы и с другими целевыми интерпретаторами. Один успешный запуск на более новой системе подтверждает работоспособность только этой конкретной комбинации. Явно указать границы проверки надёжнее, чем присвоить пакету более широкий тег совместимости.
Часто задаваемые вопросы
Сборка на Apple Silicon автоматически означает universal2?
Нет. Нативные бинарные файлы должны содержать arm64 и x86_64; выполнение нужно проверить для обеих архитектур. Переименование wheel этого не обеспечивает.
Зачем проверять импорт вне каталога исходников?
Локальные модули могут скрыть отсутствие файлов в wheel. Установите пакет в новое виртуальное окружение во временном каталоге и проверьте изолированный импорт.
Поручите задачи выделенному облачному Mac
Арендуйте выделенный физический Mac mini и выберите локацию и срок аренды под свой график. После активации данные для подключения появятся в консоли; если возникнут вопросы, отправьте заявку в поддержку через консоль.
Выбрать облачный Mac