Публикация и лок-файлы: wheel против sdist, пиннинг по потребителю и trusted publishing
Wheel распаковывается; sdist собирается — платформенные теги решают, что вы получите. Библиотеки объявляют диапазоны, приложения лочат граф с хешами; --require-hashes переживает угон релиза. Публикация: build, затем uv/twine; trusted publishing — короткоживущий OIDC.
Пятница, 18:40. Каждый свежий прогон CI краснеет с AttributeError в недрах HTTP-стека, который никто не трогал. Под подозрением — однострочная правка README; запушивший её инженер два часа бисектит коммиты, которые не могут быть виноваты. Локальные машины всё это время зелёные — от чего только хуже: отказ выглядит средовым, и раннеру больше никто не верит. Настоящее событие случилось часом раньше и тремя уровнями зависимостей ниже: транзитивная зависимость командного HTTP-клиента выпустила 2.0 с переименованным модулем. requirements.txt пиннил клиент, но не его зависимости; каждое свежее разрешение подхватывало новый мажор, а ноутбуки держали закешированный 1.x в существующих venv. В репозитории не изменилось ничего, поэтому ни один коммит не объяснял красноту — изменился вход, которым был сам индекс пакетов. Лок-файл, приземлившийся в следующем спринте — полный разрешённый граф, точные версии, хеши артефактов, — не сделал зависимости безопаснее. Он сделал установки детерминированными: индекс перестал быть рантайм-входом, и весь этот класс пятничных инцидентов закончился.
Лестница артефактов: sdist и wheel
Когда в следующий раз задумаетесь, почему установка пакета на Alpine заняла десять минут, хотя на ноутбуке она мгновенная, или почему pip install в CI неожиданно запустил компилятор — ответ лежит в том, какой тип артефакта был доступен и что кодирует его платформенный тег.
Релиз на PyPI — обычно два артефакта. sdist — исходники плюс рецепт сборки: чтобы его установить, установщик должен запустить бэкенд сборки на машине пользователя — что может означать компиляцию C, поиск заголовков и выполнение произвольного сборочного кода в момент установки. wheel — готовый продукт: zip, который установщик просто распаковывает в site-packages — без выполнения кода, без компилятора, установка измеряется миллисекундами. Какой wheel применим, закодировано в платформенных тегах: py3-none-any — чистый Python, один wheel для всех; cp312-cp312-manylinux_2_28_x86_64 — скомпилировано под CPython 3.12 против базовой glibc на x86-64. Установщик берёт самый специфичный совместимый wheel и откатывается к sdist, только если не подошёл ни один — ровно тогда вашему Alpine-контейнеру внезапно нужны gcc и десять минут, или он падает на отсутствующем заголовке. Два операционных следствия: публикуйте wheel под каждую платформу, которую заявляете (чистому Python это достаётся бесплатно одним any-wheel), и заметьте асимметрию безопасности — установка wheel не выполняет ни строки кода проекта, установка sdist исполняет бэкенд сборки и всё, что притянет рецепт.
Философия пиннинга: кто вы — так и пинните
Вопрос «пиннить ли?» не имеет ответа, пока не сказано, кто устанавливает. Библиотека разрешается вместе с чужаками — ваше ограничение на httpx обязано пересечься с ограничениями каждого другого пакета в приложении, которого вы никогда не увидите. Поэтому библиотеки объявляют честные широкие диапазоны: нижнюю границу, против которой вы реально тестируетесь, исключение заведомо плохого релиза (!=2.31.1) и никакого верхнего потолка без известной несовместимости. Механизм dependency hell от пере-пиннинга — арифметика, а не идеология: резолвер обязан выбрать ОДНУ версию пакета, удовлетворяющую всем ограничениям; каждый легкомысленный потолок сжимает допустимое множество, и два благонамеренных потолка из несвязанных библиотек могут сделать его пустым — разрешение падает либо откатывается в археологию, ставя версии многолетней давности, потому что те старше потолков. Приложение — противоположный случай: против вас никто не разрешается, а ваша работа — чтобы вторничный деплой равнялся пятничному. Приложения лочат всё — полный разрешённый граф, каждую транзитивную зависимость, точные версии с хешами (uv.lock или requirements.txt, сгенерированный с хешами). Диапазоны выражают намерение в pyproject.toml; лок-файл записывает решение.
Ваша библиотека объявляет httpx>=0.27,<0.28 «на всякий случай». Через месяцы приложения, использующие вашу библиотеку плюс другую, требующую httpx>=0.28, вовсе перестают разрешаться. Каков механизм?
Что на самом деле записывает лок-файл
Лок-файл — снимок разрешённого графа: каждый пакет, включая транзитивные, точная версия и sha256 каждого артефакта. Хеши — недооценённая часть: pip install --require-hashes (и поведение uv по умолчанию с uv.lock) сверяет каждый скачанный файл с записанным дайджестом и при несовпадении падает закрыто. Это превращает лок-файл из инструмента воспроизводимости в контроль цепочки поставок: атакующий, угнавший аккаунт мейнтейнера и перевыпустивший пакет, до вашего CI не дотянется — хеш зловредного артефакта не совпадает ни с чем записанным. Кроссплатформенная честность: pip freeze на маке — не лок для Linux: маркеры и платформенные wheel различаются; uv разрешает универсальный лок с пер-платформенными маркерами в одном файле, так что один и тот же uv.lock корректно ставится на ноутбуке разработчика и на Linux-раннере. Локи меняют и реагирование на инциденты: пятничный Хук становится не-событием, потому что свежий прогон CI ставит байт-в-байт те же входы — обновления происходят, когда вы перелочиваете, ревьюируемым диффом, а не когда у апстрим-мейнтейнера пятница.
Публикация: сборка, загрузка и trusted publishing
Поток — две команды: uv build (или python -m build) производит dist/*.tar.gz и dist/*.whl; uv publish (или twine upload) толкает их на PyPI. Сначала репетируйте на TestPyPI — отдельном индексе, где можно проверить метаданные, рендер README и пробную установку, пока имя не сожжено (версии PyPI неизменяемы: перезалить исправленный 1.4.0 нельзя, только выпустить 1.4.1). Современная история аутентификации — trusted publishing (публикация через доверие идентичности CI, без хранимых токенов): вместо долгоживущего API-токена в секретах CI — утекающего, ротируемого, скопированного в три места — PyPI настраивается доверять OIDC-идентичности (OpenID Connect — протокол аутентификации, где CI-провайдер чеканит короткоживущий токен-подпись) вашего CI-воркфлоу (этот репозиторий, этот файл воркфлоу, это окружение). На каждый публикующий прогон CI-провайдер чеканит короткоживущий токен идентичности, PyPI проверяет его и выдаёт токен загрузки на считаные минуты. Ничего долгоживущего, что можно украсть, не существует; утёкший креденшел истечёт раньше, чем заведут документ постмортема.
Аккаунт мейнтейнера транзитивной зависимости угнан, наверх уехал зловредный 1.4.7. Какая установка в CI выживает нетронутой: (а) диапазоны в requirements.txt, (б) ==1.4.6 только на ваших верхнеуровневых зависимостях, (в) установка по лок-файлу с обязательными хешами?
Гигиена цепочки поставок: yank, опечатки и радиус поражения
Три привычки закрывают большую часть оставшейся поверхности. Yank-релизы (yank — отзыв версии: пометить релиз как нежелательный без удаления с индекса): когда мейнтейнер янкает сломанную версию, резолверы пропускают её для диапазонных запросов, но точный пин == (или установка по хеш-локу) всё равно её получает, с предупреждением — yank это кнопка «отмена» для будущих разрешений, а не удаление; ваш залоченный CI продолжает работать, и мимо янка вы обновляетесь по собственному графику. Тайпсквоттинг: reqeusts и компания лежат на индексе в ожидании опечатки в pip install; лок-файл сжимает окно экспозиции до единственного момента, когда человек добавляет зависимость, — каждая последующая установка переигрывает отревьюенные имена из лока вместо повторного доверия пальцам. И общий принцип, вокруг которого кружил весь урок: хеши пиннят артефакты, а не имена и не версии. Имена сквотятся, версии перевыпускаются угнанными аккаунтами; sha256, записанный на ревью и сверенный при установке, — единственная идентичность, которую атакующий выше вас по течению не может тихо подменить. Вместе три привычки закрывают разные части поверхности: yank (отзыв версии) обрабатывает ошибки мейнтейнеров, лок обрабатывает перевыпуски и дрейф при свежем разрешении, а хеши замыкают круг — пиннируя идентичность, а не просто номер версии.
- 01Объясните sdist против wheel — что содержит каждый, что делает установка, что кодируют платформенные теги и в чём асимметрия безопасности.
- 02Дайте философию пиннинга по потребителю, механизм отказа от пере-пиннинга, что записывает лок-файл и как trusted publishing убивает долгоживущий токен.
Релиз на PyPI — это sdist: исходники плюс рецепт, собираемые бэкендом на машине потребителя, с компилятором и прочим, — и wheel: готовые артефакты, которые установщик просто распаковывает, выбранные по платформенному тегу с откатом к исходникам, лишь когда не подошло ничего; асимметрия и операционная (Alpine, которому нужен gcc), и относящаяся к безопасности (установка wheel не исполняет ничего). Пиннинг решается тем, кто устанавливает: библиотека разрешается рядом с чужаками — поэтому объявляет протестированный пол, исключает заведомо плохие релизы и избегает легкомысленных потолков: резолверу нужна одна версия под все ограничения, и каждый лишний потолок сжимает допустимое множество к отказу или археологии; приложение не отвечает ничьему резолверу и лочит весь граф — точные версии и sha256, — так что индекс перестаёт быть рантайм-входом. Именно этот детерминизм закончил пятничный инцидент из Хука: ломающий 2.0 пришёл тремя уровнями ниже, красный CI не объяснялся ни одним коммитом, а лок сделал свежие установки байт-идентичными; хеши вдобавок закрыто роняют угнанные перевыпуски — чего не может ни один верхнеуровневый пин ==. Публикация — сборка, репетиция на TestPyPI, загрузка — где trusted publishing меняет утекающий долгоживущий токен на чеканящийся OIDC на каждый прогон. Янкнутые версии исчезают из будущих диапазонных разрешений, но точным пинам отдаются; тайпсквоты ждут пальцев, а лок означает, что пальцам доверяют один раз — на ревью. Теперь, когда видите свежий прогон CI, покрасневший без единого виновного коммита, Alpine-контейнер, требующий компилятор, или вопрос о верхнем потолке версии, — вы знаете, какой тип артефакта, какое свойство лок-файла и какая философия пиннинга здесь работает.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.