Pull to refresh

Как Homebrew раздаёт 52 миллиона пакетов в месяц?

Level of difficultyMedium
Reading time12 min
Views3.3K
Original author: Dmitry Ustalov

Трудно представить разработчика, использующего macOS, но не использующего Homebrew. Это менеджер пакетов, позволяющий устанавливать сторонние программы (кстати, недавно он получил поддержку платформ Linux и Windows).

Логотип Homebrew (https://brew.sh/)
Логотип Homebrew (https://brew.sh/)

Первоначально Homebrew работал, загружая программное обеспечение с upstream и собирая его на том же компьютере, где и производится установка. При сборке он следует инструкциям, указанным в формулах (formulæ), написанных на языке программирования Ruby (см. терминологию).

Получив огромную популярность, Homebrew обзавёлся двумя важными возможностями.

Во‑первых, он стал управлять дистрибутивами приложений macOS от разных поставщиков, таких как Sublime Text или Firefox. Эта возможность называется casks. Во‑вторых, он начал скачивать готовые пакеты уже собранного программного обеспечения. Эта возможность называется «бутылки» (bottles).

Благодаря бутылкам не нужно компилировать программы и Homebrew достаточно скачать готовые пакеты под целевую систему. Однако эти пакеты надо где‑то хранить и раздавать. Как именно это работает для Homebrew?


Рассмотрим пример SQLite, встраиваемой реляционной базы данных. Если мы посмотрим на соответствующую формулу Homebrew, sqlite.rb, то заметим инструкции по сборке в методе install и несколько шестнадцатеричных идентификаторов в блоке bottle. Homebrew использует эти инструкции для сборки и загружает бутылки в Интернет, чтобы затем их скачать и избежать компиляции на вашем компьютере.

Если запустить команду brew install sqlite в любой системе, для которой доступна бутылка, Homebrew загрузит её и установит в систему.

За последний месяц Homebrew раздал более 52 миллионов пакетов. Это число может быть выше, поскольку некоторые пользователи отключают аналитику. Даже если каждая бутылка занимает один мегабайт места, Homebrew должен был раздать более 50 ТиБ трафика в месяц. Это запретительно дорого почти на всех популярных облачных хранилищах. Homebrew — некоммерческий проект, принимающий пожертвования от спонсоров. Маловероятно, что даже очень щедрые пожертвования покроют ежемесячный счет на тысячи долларов США для таких сервисов, как Amazon S3, при таком объёме трафика.


К счастью, команде Homebrew удалось найти хостинг. Первоначально они использовали недавно закрытую платформу Bintray. Позже Homebrew получил возможность размещения бутылок в GitHub Releases. Это довольно распространенная практика, используемая во многих открытых проектах, таких как Gensim (см., например, репозиторий gensim‑data). К сожалению, в этом случае нет стандартного способа указания метаданных и нет тривиального способа различить, под какую систему собрана одна и та же версия пакета. Недавно они перешли на GitHub Packages — те самые GitHub Packages, которые используются для хранения артефактов Maven и Docker‑образов.

Реестр контейнеров хорошо подходит для хранения файлов разного размера, версий и свойств. GitHub же щедро предлагает неограниченный трафик для открытых проектов. Итак, когда вы открываете бутылку в Homebrew, вы скачиваете файлы из GitHub Packages, также известного как GitHub Container Registry.


Но как именно Homebrew понимает, какие файлы загружать? Блок bottle в формуле содержит идентификаторы SHA-256, также известные как дайджесты архивов для конкретной платформы (см. снова sqlite.rb в качестве примера). На момент написания последней версией SQLite в Homebrew была 3.40.1.

Поскольку GitHub Packages — это такой же реестр контейнеров, как Docker Hub и многие другие, и следует одним и тем же спецификациям, можно вручную собрать ссылку для загрузки файла, поскольку мы знаем имя образа (homebrew/core/sqlite) и его версию (3.40.1). Таким образом, бутылки находятся по адресу ghcr.io/homebrew/core/sqlite:3.40.1. Например, бинарники для x86_64 Linux имеют дайджест 8d1bae…85bb06.

Для скачивания файла нам не хватает только токена аутентификации для реестра. Значение этого токена по умолчанию указано в Homebrew как QQ==. Однако мы можем запросить собственный токен с помощью одного простого запроса.

TOKEN=$(curl "https://ghcr.io/token?scope=repository:homebrew/core/sqlite:pull" | jq -r .token)
# или просто
TOKEN="QQ=="

Независимо от того, как мы получили токен, мы можем теперь скачать SQLite 3.40.1 для Linux (x86_64) в виде большого двоичного объекта (blob).

curl -I \
  -H "Authorization: Bearer $TOKEN" \
  "https://ghcr.io/v2/homebrew/core/sqlite/blobs/sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06"
Результат
HTTP/2 200 
content-length: 2682611
content-type: application/vnd.oci.image.layer.v1.tar+gzip
docker-content-digest: sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06
docker-distribution-api-version: registry/2.0
...

Не забудьте включить параметр --location в cURL (-L), чтобы следовать HTTP‑перенаправлениям при загрузке файла; мой текущий пример просто извлекает заголовки файлов (-I). Таким образом, мы можем достаточно легко вручную скачивать файлы из реестров контейнеров, будь то пакеты Homebrew или же обычные образы контейнеров.


Мы могли бы остановиться на скачивании файлов по уже известным дайджестам в формулах Homebrew, но давайте разберемся, откуда взялись все эти идентификаторы. Итак, нам известен идентификатор образа, ghcr.io/homebrew/core/sqlite:3.40.1, поэтому воспользуемся спецификациями образа Open Container Initiative (OCI), чтобы самостоятельно восстановить их.

Спецификация OCI Image Format: типы данных (https://github.com/opencontainers/image-spec/blob/main/img/media-types.png)
Спецификация OCI Image Format: типы данных (https://github.com/opencontainers/image-spec/blob/main/img/media-types.png)

Сущность верхнего уровня — это индекс образа (image index), который содержит информацию о вариантах операционной системы в формате JSON вместе с некоторыми другими метаданными. Давайте посмотрим на него.

curl \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/vnd.oci.image.index.v1+json" \
  "https://ghcr.io/v2/homebrew/core/sqlite/manifests/3.40.1"
Результат
{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:3b7ebf540cd60769c993131195e796e715ff4abc37bd9a467603759264360664",
      "size": 1977,
      "platform": {
        "architecture": "amd64",
        "os": "darwin",
        "os.version": "macOS 13.0"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.ventura",
        "sh.brew.bottle.digest": "d3092d3c942b50278f82451449d2adc3d1dc1bd724e206ae49dd0def6eb6386d",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 13.0\",\"cpu_family\":\"penryn\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1666437224\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:2a0bbff81707938631bffd395ae8c9d03d5ddf67b8d9669b18bf67de240534e2",
      "size": 2010,
      "platform": {
        "architecture": "arm64",
        "os": "darwin",
        "os.version": "macOS 11"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.arm64_big_sur",
        "sh.brew.bottle.digest": "1dce645628978038d4615669728089f9e22259a8c461f5d81672b741189f1f29",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 11\",\"cpu_family\":\"arm_firestorm_icestorm\",\"xcode\":\"13.2.1\",\"clt\":\"13.2.0.0.1.1638488800\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:1f839eae57ab0bd81e915cfcb3227cb61d551d3cbe9a8a8bd93e9aace869a53a",
      "size": 1980,
      "platform": {
        "architecture": "amd64",
        "os": "darwin",
        "os.version": "macOS 12.6"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.monterey",
        "sh.brew.bottle.digest": "ebdcd895a537933c8ae0111a96b02aa7e2ac8f8c991f0c3e4d9ec250619a29e5",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 12.6\",\"cpu_family\":\"penryn\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1666437224\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:d0b67f3ac3c52498d0a4de161e68e60d0ca12ae74db08e241cf2f72e5d70048b",
      "size": 1979,
      "platform": {
        "architecture": "amd64",
        "os": "darwin",
        "os.version": "macOS 11.7"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.big_sur",
        "sh.brew.bottle.digest": "c2b7d4f849d7af7e8be3c738e9670842c9c6b25053fd19a90ef8264b2a257158",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 11.7\",\"cpu_family\":\"penryn\",\"xcode\":\"13.2.1\",\"clt\":\"13.2.0.0.1.1638488800\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:3e4e657d85ff3428660fe52265e10c9656dc063b3b8885e79632bcc22a6b9af5",
      "size": 2011,
      "platform": {
        "architecture": "arm64",
        "os": "darwin",
        "os.version": "macOS 12"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.arm64_monterey",
        "sh.brew.bottle.digest": "45f18a632fd523c325bedda31a17ec8a1e577da0c4350b0342106ce360a925a5",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 12\",\"cpu_family\":\"arm_firestorm_icestorm\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1665256668\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:880727df1ae294f4df5f5dc0906c334b241f1283e5911974913ce3606d221bed",
      "size": 1991,
      "platform": {
        "architecture": "arm64",
        "os": "darwin",
        "os.version": "macOS 13"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.arm64_ventura",
        "sh.brew.bottle.digest": "e19a160e1012ed0d58f0e1f631d6954c2bb6feb3cf9f8e9417d6f8955b81236d",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"clang\",\"runtime_dependencies\":[{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true}],\"arch\":\"arm64\",\"built_on\":{\"os\":\"Macintosh\",\"os_version\":\"macOS 13\",\"cpu_family\":\"dunno\",\"xcode\":\"14.1\",\"clt\":\"14.1.0.0.1.1665256668\",\"preferred_perl\":\"5.30\"}}"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da",
      "size": 2213,
      "platform": {
        "architecture": "amd64",
        "os": "linux",
        "os.version": "5.15.0-1024-azure"
      },
      "annotations": {
        "org.opencontainers.image.ref.name": "3.40.1.x86_64_linux",
        "sh.brew.bottle.cpu.variant": "core2",
        "sh.brew.bottle.digest": "8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
        "sh.brew.bottle.glibc.version": "2.35",
        "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"gcc-11\",\"runtime_dependencies\":[{\"full_name\":\"ncurses\",\"version\":\"6.3\",\"declared_directly\":false},{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true},{\"full_name\":\"zlib\",\"version\":\"1.2.13\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Linux\",\"os_version\":\"5.15.0-1024-azure\",\"cpu_family\":\"skylake\",\"glibc_version\":\"2.35\",\"oldest_cpu_family\":\"core2\"}}"
      }
    }
  ],
  "annotations": {
    "com.github.package.type": "homebrew_bottle",
    "org.opencontainers.image.created": "2022-12-30",
    "org.opencontainers.image.description": "Command-line interface for SQLite",
    "org.opencontainers.image.documentation": "https://formulae.brew.sh/formula/sqlite",
    "org.opencontainers.image.license": "blessing",
    "org.opencontainers.image.ref.name": "3.40.1",
    "org.opencontainers.image.revision": "24944d797567cd81c25a0627b3f373e0d6472d94",
    "org.opencontainers.image.source": "https://github.com/homebrew/homebrew-core/blob/24944d797567cd81c25a0627b3f373e0d6472d94/Formula/sqlite.rb",
    "org.opencontainers.image.title": "sqlite",
    "org.opencontainers.image.url": "https://sqlite.org/index.html",
    "org.opencontainers.image.vendor": "homebrew",
    "org.opencontainers.image.version": "3.40.1"
  }
}

Индекс ссылается на несколько манифестов образов (image manifest). Каждый манифест соответствует определённой платформе, определяемой архитектурой процессора, операционной системой и её версией. Поскольку все метаданные машиночитаемы (и иногда человекочитаемы), мы можем легко заметить две вещи. Во‑первых, в аннотациях есть поле sh.brew.bottle.digest, которое содержит тот же дайджест SHA-256, что и в формуле Homebrew. Но он был помещен туда намеренно в процессе сборки. Во‑вторых, мы не видим здесь никаких файлов, а дайджест манифеста для Linux (x86_64) другой: ff58c2…8c22da. Теперь нам нужно получить манифест нужного нам образа.

curl \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/vnd.oci.image.manifest.v1+json" \
  "https://ghcr.io/v2/homebrew/core/sqlite/manifests/sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da"
Результат
{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:36d708da8f4d7e2450550b5179e41b4320628b97a2056cf56b8bb15a2759bb3d",
    "size": 228
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
      "size": 2682611,
      "annotations": {
        "org.opencontainers.image.title": "sqlite--3.40.1.x86_64_linux.bottle.tar.gz"
      }
    }
  ],
  "annotations": {
    "com.github.package.type": "homebrew_bottle",
    "org.opencontainers.image.created": "2022-12-30",
    "org.opencontainers.image.description": "Command-line interface for SQLite",
    "org.opencontainers.image.documentation": "https://formulae.brew.sh/formula/sqlite",
    "org.opencontainers.image.license": "blessing",
    "org.opencontainers.image.ref.name": "3.40.1.x86_64_linux",
    "org.opencontainers.image.revision": "24944d797567cd81c25a0627b3f373e0d6472d94",
    "org.opencontainers.image.source": "https://github.com/homebrew/homebrew-core/blob/24944d797567cd81c25a0627b3f373e0d6472d94/Formula/sqlite.rb",
    "org.opencontainers.image.title": "sqlite 3.40.1.x86_64_linux",
    "org.opencontainers.image.url": "https://sqlite.org/index.html",
    "org.opencontainers.image.vendor": "homebrew",
    "org.opencontainers.image.version": "3.40.1",
    "sh.brew.bottle.cpu.variant": "core2",
    "sh.brew.bottle.digest": "8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06",
    "sh.brew.bottle.glibc.version": "2.35",
    "sh.brew.tab": "{\"homebrew_version\":\"3.6.16-97-ge76c55e\",\"changed_files\":[\"lib/pkgconfig/sqlite3.pc\"],\"source_modified_time\":1672237605,\"compiler\":\"gcc-11\",\"runtime_dependencies\":[{\"full_name\":\"ncurses\",\"version\":\"6.3\",\"declared_directly\":false},{\"full_name\":\"readline\",\"version\":\"8.2.1\",\"declared_directly\":true},{\"full_name\":\"zlib\",\"version\":\"1.2.13\",\"declared_directly\":true}],\"arch\":\"x86_64\",\"built_on\":{\"os\":\"Linux\",\"os_version\":\"5.15.0-1024-azure\",\"cpu_family\":\"skylake\",\"glibc_version\":\"2.35\",\"oldest_cpu_family\":\"core2\"}}"
  }
}

Образ содержит список слоёв. Каждый слой хранится и скачивается в виде отдельного файла. В нашем случае образ имеет только один слой с заголовком sqlite--3.40.1.x86_64_linux.bottle.tar.gz и его дайджест полностью совпадает с указанным в формуле! Итак, нам удалось восстановить ту же ссылку, которую мы видели несколькими абзацами выше. Это означает, что теперь мы можем опрашивать реестр контейнеров, скачивать файлы и не полагаться на проставленные ссылки.

curl -I \
  -H "Authorization: Bearer $TOKEN" \
  "https://ghcr.io/v2/homebrew/core/sqlite/blobs/sha256:8d1baebd808a5cdb47c3fedbefd4de5cf7983700c41191432f3a9bed4885bb06"

В образовательных целях мы использовали cURL, но это не самый надёжный способ, потому что мы не охватываем всю спецификацию и никак не обрабатываем возможные ошибки.

Поскольку реестры контейнеров постепенно становятся в хранилищами общего назначения, организация Cloud Native Computing Foundation (CNCF) профинансировала проект OCI Registry As Storage (ORAS). Он позволяет скачивать и загружать образы и их метаданные так, как мы это делали с cURL, но с поддержкой со стороны CNCF и, надеюсь, с лучшей обработкой ошибок.

ORAS, OCI Registry as Storage (https://oras.land/)
ORAS, OCI Registry as Storage (https://oras.land/)

ORAS реализован на языке программирования Go. Мы можем заменить наши вызовы cURL для опроса манифеста и получить точно такой же JSON-ответ.

oras manifest fetch "ghcr.io/homebrew/core/sqlite:3.40.1"
oras manifest fetch "ghcr.io/homebrew/core/sqlite:3.40.1@sha256:ff58c21da5e58b82bae6e19207a8ec01e398a31512081e9b6560b2dec88c22da"

Отдельная команда ORAS позволяет скачать все образы в текущий каталог.

oras pull "ghcr.io/homebrew/core/sqlite:3.40.1"

При помощи команды oras push можно загрузить данные с компьютера в образ контейнера, но наш токен должен иметь соответствующие разрешения для реестра контейнеров (это не очень сложно, см. документацию).


Реестры контейнеров — популярный способ распространения больших файлов, использующий хорошо поддерживаемую Интернет‑инфраструктуру. Вы можете хранить там больше, чем просто программное обеспечение: файлы с параметрами моделей машинного обучения, обучающие наборы данных, и многое другое.

Tags:
Hubs:
Total votes 7: ↑6 and ↓1+5
Comments8

Articles