Start Debugging

.NET 11 RC 1 делает образы контейнеров из dotnet publish воспроизводимыми

SDK .NET 11 RC 1 учитывает SOURCE_DATE_EPOCH при публикации образов контейнеров, убирает идентификатор процесса из tar-заголовков слоёв и пропускает загрузку blob-объектов, если манифест уже есть в реестре. Один и тот же коммит на входе, один и тот же digest на выходе.

Опубликуйте один и тот же коммит дважды через dotnet publish /t:PublishContainer на .NET 10, и вы получите два разных digest образа. Код идентичен, но SDK записывал текущее время в каждую запись слоя и в конфигурацию образа. Кроме того, он записывал идентификатор процесса в tar каждого слоя. Реестр не может такое дедуплицировать, а GitOps-контроллер, следящий за тегом, видит новый релиз. .NET 11 RC 1, вышедший 2026-09-08, исправляет обе проблемы во встроенных в SDK инструментах для контейнеров (dotnet/sdk#55836).

Четыре места, где часы просачивались в digest

Исходный PR перечисляет их:

Одного pid хватало, чтобы изменить digest слоя при побайтно идентичном содержимом. Различались только 13 байт слоя, и все они были вызваны этим именем заголовка.

Включение через SOURCE_DATE_EPOCH

RC 1 следует соглашению reproducible-builds. Свойство MSBuild SOURCE_DATE_EPOCH или одноимённая переменная окружения, которую MSBuild подхватывает автоматически, разбирается один раз в единую метку времени. Затем это значение попадает в каждую tar-запись, в поле created конфигурации, в запись истории и в обе метки OCI:

dotnet publish -c Release -r linux-x64 /t:PublishContainer \
  -p:ContainerRegistry=registry.example.com \
  -p:SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct)"

Если использовать метку времени коммита, digest меняется только тогда, когда меняется коммит. Я проверил это на SDK RC 1 (11.0.100-rc.1.26425.128), трижды опубликовав консольное приложение в tar-архив:

pub() { dotnet publish -c Release -r linux-x64 /t:PublishContainer \
  -p:ContainerArchiveOutputPath=./$1.tar.gz "${@:2}"; }

pub a -p:SOURCE_DATE_EPOCH=1757635200   # config dc51dd30..., app layer 47af118e...
pub b -p:SOURCE_DATE_EPOCH=1757635200   # config dc51dd30..., app layer 47af118e...
pub c                                   # config 5f24aace..., app layer 2f4f68c1...

Запуски a и b побайтно идентичны, а поле created конфигурации содержит 2025-09-12T00:00:00.0000000Z. Запуск c по-прежнему получает системное время, так что воспроизводимость включается явно. Если значение некорректно, отрицательно или выходит за допустимый диапазон, SDK откатывается к текущему времени. Сборка при этом не падает.

Два побочных эффекта, о которых стоит знать до обновления. Запись слоёв теперь сортирует элементы по пути в контейнере, потому что порядок перечисления каталогов зависит от файловой системы. Имя заголовка pax всегда равно константе ./PaxHeaders/.. Оба изменения действуют при каждой публикации, даже без SOURCE_DATE_EPOCH, поэтому ваши digest один раз сдвинутся при переходе на RC 1.

Пропуск загрузок, которые уже есть в реестре

Сопутствующее изменение (dotnet/sdk#55838) опирается на это. Перед отправкой SDK выполняет запрос HEAD для вычисленного digest манифеста. Если манифест уже есть в реестре, SDK пропускает загрузку слоёв и конфигурации, всё равно применяет все запрошенные теги и записывает в журнал Manifest '...' already exists in repository '...'. Повторный запуск CI-задания или второй тег на неизменённом коммите сводятся к нескольким вызовам для метаданных.

В targets RC 1 это включено по умолчанию: ContainerPushNoCache по умолчанию равно false. Если реестр неверно сообщает о наличии манифеста, отключите проверку:

dotnet publish /t:PublishContainer -p:ContainerPushNoCache=true

Образ всё равно собирается локально, так как сначала нужно вычислить digest, поэтому экономится передача данных, а не время сборки. Кроме того, выигрыш есть только при стабильном digest, и именно поэтому оба PR вошли вместе.

Ещё одно изменение RC 1, которое убирает давний обходной путь, описано в статье сигналы и код завершения в Process. Полный список изменений SDK есть в примечаниях к выпуску SDK RC 1.

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад