Пакетный менеджер (сфера_пакеты)

Отдельный инструмент (не часть самого компилятора) для установки стороннего кода — git как реестр, без собственной инфраструктуры вроде crates.io/npm: зависимость — это просто ссылка на git-репозиторий.

Манифест сфера.зависимости в корне проекта (простой построчный текст):

геометрия = git https://github.com/кто-то/геометрия-сфера.git
графика_доп = git https://github.com/кто-то/графика-доп.git ветка=разработка описание="Доп. примитивы рисования"

Команды:

сфера_пакеты установить     # клонирует и ставит все зависимости из манифеста
сфера_пакеты список         # показывает установленные пакеты и как их подключать
сфера_пакеты удалить ИМЯ    # убирает пакет целиком

Изоляция пространств имён. Каждый пакет устанавливается в СВОЮ собственную папку — пакеты/ИМЯ_ПАКЕТА/, куда копируются все .код файлы из корня его репозитория. Два разных пакета могут содержать одноимённые файлы (например, оба — утилиты.код) без какого-либо конфликта: физически они лежат в разных папках, и подключаются они по составному имени — подключить ИМЯ_ПАКЕТА.файл; (см. "Модули" выше). Команда список для каждого пакета сразу подсказывает точную строку подключить ...; для каждого его файла.

Транзитивные зависимости

Если у самого устанавливаемого пакета внутри его репозитория тоже есть файл сфера.зависимости, сфера_пакеты установить подхватывает и его — не только один уровень корневого манифеста, а весь граф, обходом в ширину. Пакет А, зависящий от Б, который зависит от В, ставит все три, без ручного перечисления Б и В в манифесте А.

Команда список честно показывает, какие пакеты установлены напрямую, а какие — транзитивно и через кого именно (`поставлен как зависимость пакета: ...`).

Циклы (А зависит от Б, Б — от А) корректно обрываются — каждое имя пакета обрабатывается ровно один раз.

Разрешение версионных конфликтов

Необязательное поле версия=X.Y.Z в манифесте (вместо ветка= — они взаимоисключающие на одной строке):

утилиты = git https://github.com/кто-то/утилиты-сфера.git версия=1.4.0

Технически это по-прежнему просто имя git-тега (передаётся в git clone --branch, должно точно совпадать с тегом в репозитории пакета — сфера_пакеты не подбирает варианты вроде «с v или без»). Но, в отличие от ветка, оно ДОПОЛНИТЕЛЬНО разбирается как простая семантическая версия (major.minor.patch) для сравнения:

Если один и тот же пакет (тот же адрес) транзитивно запрошен несколько раз с разными версиями — требования пересекаются как диапазоны версий (настоящий constraint solving, а не «побеждает большая» — см. подробнее в разделе "Диапазоны версий" ниже): итоговая версия обязана удовлетворять ВСЕМ требованиям, встреченным в дереве зависимостей, одновременно. Если уже выбранная версия по-прежнему подходит под сузившийся интервал — переустановка не нужна; если нет — пакет переустанавливается на версию, попадающую в итоговый, пересечённый интервал. Результат не зависит от порядка, в котором зависимости встречаются в дереве.

Конфликт имён. Если пакет с одним и тем же именем запрошен из разных АДРЕСОВ (не версий одного и того же репозитория, а буквально разных URL) — это не версионный конфликт: два разных места дерева буквально хотят разный код под одним именем, и сфера_пакеты не выбирает между ними молча. Вся установка честно проваливается с ошибкой, называющей оба адреса и через какие пакеты каждый из них был запрошен (корневой манифест или конкретное имя транзитивного пакета) — например:

Ошибка: пакет 'утилита' запрошен из двух РАЗНЫХ адресов в дереве зависимостей — ...
  - https://github.com/кто-то/утилита-а.git (запрошен напрямую в корневом сфера.зависимости)
  - https://github.com/кто-то/утилита-б.git (запрошен через пакет 'сеть')

Разработчик разрешает это сам — например, переименовав один из пакетов в том сфера.зависимости, где он объявлен (имя пакета — это просто ключ слева от =, оно не обязано совпадать с именем репозитория). Если версии есть, но хотя бы одну из них не удалось разобрать как X.Y.Z — это отдельный, более мягкий случай (тот же URL, просто версии несравнимы) — он по-прежнему честно предупреждает и оставляет уже выбранную, а не проваливает установку (см. выше).

Диапазоны версий (^X.Y.Z, ~X.Y.Z)

Поле версия= понимает не только точный тег, но и два вида диапазонов в стиле npm/cargo:

сеть = git https://github.com/кто-то/сеть-сфера.git версия=^1.2.0
утилиты = git https://github.com/кто-то/утилиты-сфера.git версия=~1.2.0

В отличие от точной версии (которая — это по-прежнему буквально имя git-тега), диапазон — это ТРЕБОВАНИЕ, которое нужно разрешить в конкретный тег ПЕРЕД клонированием. Для этого сфера_пакеты вызывает git ls-remote --tags URL (лёгкая сетевая операция, без полного клонирования), разбирает имена всех тегов как X.Y.Z (необязательный префикс v снимается — v1.2.0 и 1.2.0 равнозначны) и выбирает САМУЮ БОЛЬШУЮ версию, удовлетворяющую диапазону. Дальше эта конкретная версия используется точно так же, как если бы она была указана напрямую (клонируется как --branch НАЙДЕННЫЙ_ТЕГ, участвует в разрешении конфликтов наравне с точными версиями).

Семантика диапазонов:

  • ^X.Y.Z («каре», совместимые версии): при X > 0 разрешено

любое >=X.Y.Z с тем же X (например ^1.2.0 допускает 1.5.0, но не 2.0.0). При X == 0, Y > 0 — совместимость сужается до того же Y (^0.3.1 допускает 0.3.4, но не 0.4.0) — как и в npm, версии 0.x считаются нестабильными, поэтому даже минорная цифра ломает совместимость. При X == 0, Y == 0 — разрешён только точно тот же Z (0.0.x считается настолько нестабильным, что ничего, кроме точного совпадения, не считается совместимым).

  • ~X.Y.Z («тильда», только патчи): разрешено >=X.Y.Z при тех

же X и Y — то есть только патч-версии внутри одного минора.

Если ни один тег не удовлетворяет диапазону (либо у пакета вообще нет тегов в формате семвера), установка честно завершается ошибкой с объяснением, а не тихо берёт что попало.

Настоящее пересечение диапазонов (constraint solving)

Каждое требование версии (точная версия, ^X.Y.Z или ~X.Y.Z) внутренне представляется как полуоткрытый интервал версий [ниж, верх) — нижняя граница включена, верхняя исключена (например, ^1.2.0 — это [1.2.0, 2.0.0), а точная версия 1.2.0 — вырожденный интервал [1.2.0, 1.2.1), содержащий ровно одну версию).

Если один и тот же пакет (тот же адрес) запрошен из разных мест дерева зависимостей, сфера_пакеты не выбирает между требованиями («какое победит»), а пересекает их интервалы — на каждый новый запрос интервал становится ⊆ прежнему (или остаётся тем же, если новое требование шире уже накопленного). Итоговый, суженный интервал должен удовлетворять ВСЕМ требованиям, встреченным до сих пор, одновременно.

  • Если уже выбранная версия пакета попадает в суженный интервал —

ничего не происходит, переустановка не нужна (только запоминается более узкий интервал — на случай, если дерево зависимостей попросит этот пакет ещё раз, с ещё одним требованием).

  • Если уже выбранная версия перестала попадать в суженный интервал —

пакет разрешается заново (git ls-remote --tags, либо, если подходящий тег уже есть в сфера.блокировка, без похода в сеть — см. ниже), но уже в пределах нового, суженного интервала.

  • Если пересечение интервалов оказывается пустым — то есть не

существует версии, которая устраивала бы оба требования одновременно — это доказуемый, настоящий конфликт версий. Установка честно проваливается с ошибкой, называющей оба требования (в виде интервала и версии, если она уже была выбрана) и через какие пакеты каждое из них было запрошено, например:

Ошибка: настоящий конфликт версий пакета 'лог': требования из разных мест дерева
зависимостей не пересекаются — уже выбран интервал >=1.2.0, <2.0.0 (версия v1.4.0)
(запрошен через пакет 'сеть'), а новый запрос требует интервал >=1.1.0, <1.2.0
(запрошен транзитивно через пакет 'утилиты') — не существует версии, удовлетворяющей
ОБОИМ требованиям одновременно. Разрешите это вручную — ослабьте одно из требований
версии в 'сфера.зависимости', или явно зафиксируйте нужную версию.

Разработчик разрешает такой конфликт сам — ослабив одно из требований версии в соответствующем сфера.зависимости, либо явно зафиксировав одну и ту же совместимую версию в обоих местах.

⚠️ Честные ограничения v1

  • Пересечение диапазонов реализовано только для версий вида X.Y.Z

(major.minor.patch), точных или заданных через ^/~, у пакетов с ОДНОГО И ТОГО ЖЕ адреса. Нет пререлизов/build-метаданных в сравнении (1.2.0-rc1 сравнивается как 1.2.0), и нет >=, <=, * или hyphen-диапазонов (1.2.0 - 2.0.0) из npm — только ^, ~ и точная версия. Диапазоны работают только если у пакета в git реально есть теги вида X.Y.Z/vX.Y.Z — сфера_пакеты не подбирает версию по коммитам или веткам. Если хотя бы одно из двух требований к одному и тому же пакету несравнимо как семвер (например, одно место дерева просит ветка=... или версию вообще не указывает) — пересекать нечего: сфера_пакеты, как и раньше, честно предупреждает в консоль и оставляет уже выбранную версию, вместо того чтобы гадать.

  • Разные адреса под одним именем пакета не разрешаются — да и не могут:

это не версия, а разный код. Реакция — честный отказ всей установки с ошибкой (см. выше), а не молчаливый выбор. Нет автоматического способа «подружить» два таких пакета (например, через алиасы) — только ручное переименование одного из них разработчиком.

  • сфера_пакеты удалить и список также узнают старый, плоский формат

установки (пакеты/файл.код напрямую, без подпапки — как ставилось ДО появления изоляции пространств имён) — для плавного перехода, но предупреждают переустановить зависимость, чтобы получить изоляцию; для этого старого формата защита при удалении (см. ниже) не работает.

Защита при удалении

удалить ИМЯ проверяет, не требуется ли этот пакет другим уже установленным пакетам, и по умолчанию отказывает, если да:

$ сфера_пакеты удалить утилита
Ошибка: пакет 'утилита' требуется другим установленным пакетам: б, с — ...
        повтори команду с флагом --force.

Список требователей собирается во время последнего установить — при обходе дерева зависимостей (BFS) фиксируется КАЖДЫЙ пакет, объявивший данный своей зависимостью (не только первый — если и б, и с одновременно требуют утилита, оба попадут в список, даже если утилита была установлена/переустановлена только один раз). Список сохраняется в метаданные пакета (поле требуется_пакетами=, видно и в список). При удалении из него отфильтровываются те требователи, что уже сами удалены (если у имя_требователя больше нет своей папки в пакеты/ — он не живой, предупреждать не о чем).

Если предупреждение — осознанный выбор (переустановишь или сам поправишь зависимости требователей), команда принимается с флагом --force — удаление проходит, но с явным предупреждением в консоль о том, кто по-прежнему требует удаляемый пакет.

Честная граница: это снимок на момент последнего установить, а не живой, постоянно поддерживаемый граф зависимостей — сфера_пакеты не хранит такой граф между запусками вообще. Если зависимости поменяли вручную (не через сфера_пакеты установить) после этого снимка, список требователей может быть неточным (устаревшим или неполным). Для старого, плоского формата установки (см. выше) защита не работает вовсе — там метаданные о требователях никогда не записывались.

Файл блокировки (сфера.блокировка)

Диапазоны версий (^/~) намеренно нестабильны во времени — у пакета могут появиться новые теги, и git ls-remote в следующий раз найдёт уже другой, более новый максимум. Это удобно для разработки, но плохо для воспроизводимости: тот же сфера_пакеты установить на другой машине (или через неделю) мог бы тихо поставить другую версию. сфера.блокировка решает это — после каждого успешного установить она перезаписывается целиком, тем же построчным форматом, что и у сфера.зависимости, но версия в ней всегда УЖЕ разрешённый конкретный тег, никогда ^/~:

утилита = git https://github.com/кто-то/утилита-сфера.git версия=1.8.0

При следующем установить, встретив диапазон в манифесте, сфера_пакеты сначала смотрит в блокировку: если для этого имени и АДРЕСА там уже есть тег, и он всё ещё удовлетворяет ТЕКУЩЕМУ диапазону из манифеста — используется он напрямую, без обращения к git ls-remote (экономия сети и, главное, гарантия того же результата, а не более новой версии внутри того же диапазона). Если тег из блокировки диапазону больше не удовлетворяет (диапазон в манифесте сознательно сузили) — версия разрешается заново, и блокировка перезаписывается новым результатом. Флаг --обновить-блокировку игнорирует файл целиком и разрешает все диапазоны заново (например, чтобы сознательно перейти на новейшие версии, удовлетворяющие текущим диапазонам).

Пакеты, установленные через ветка= (или вовсе без поля версия=), в блокировку не попадают — ветка не семвер, фиксировать её как "версию" было бы нечестной подменой понятий; git clone --branch для них всегда берёт актуальную голову ветки, блокировки для них попросту нет.

Честные ограничения:

  • Блокировка хранит только ОДНУ, ИТОГОВУЮ версию на имя пакета — ту, что

победила при разрешении конфликтов (см. выше). Если несколько мест в дереве запрашивают один и тот же пакет с разными диапазонами, и один из них проигрывает (побеждает БОЛЬШАЯ версия) — для проигравшего диапазона блокировка не поможет напрямую (итоговая версия может не удовлетворять ЕГО исходному диапазону), и разрешение для него всё равно пойдёт через git ls-remote; результат всё равно сойдётся к тому же итогу, просто без экономии сети для этого конкретного места.

  • Не защищает от переименования/удаления самого тега в репозитории

пакета (если тег 1.8.0 потом удалили и создали заново на другом коммите — блокировка знает только имя тега, не хэш коммита).

  • Файл предполагается коммититься в git проекта (как Cargo.lock /

package-lock.json) — сфера_пакеты сама этого не делает.

Публичный индекс пакетов (искать / добавить / индекс-*)

Раньше, чтобы поставить чужой пакет, нужно было заранее знать его git-адрес — взять его было решительно неоткуда. Индекс закрывает именно это: один git-репозиторий с одним каталог-файлом внутри (`имя = git URL описание="...", тот же формат, что и у сфера.зависимости`), который можно искать и добавлять из него в свой проект.

Настраивается один раз на машину (не на проект):

сфера_пакеты индекс-настроить URL_ИНДЕКСА   # запоминает адрес в ~/.сфера/
сфера_пакеты индекс-обновить                # клонирует/обновляет локальный кэш

Дальше — в любом проекте:

сфера_пакеты искать математика     # ищет по имени и описанию в каталоге
сфера_пакеты добавить математика   # дописывает готовую строку в сфера.зависимости
сфера_пакеты установить            # как обычно — реально ставит

добавить только правит сфера.зависимости (не устанавливает сам пакет) — намеренно: между «нашёл и добавил в манифест» и «реально поставил в пакеты/» всегда остаётся отдельный, явный шаг, так же, как установить уже отдельно от искать/добавить.

Публикация своего пакета в индекс — pull request с одной новой строкой в его каталог.пакеты (или прямое редактирование, если репозиторий свой), не загрузка на какой-то сервер: сам индекс — это только git, никакой новой инфраструктуры сфера_пакеты не запускает.

⚠️ Честно, сознательно временное решение, не финальное. Это первый, самый лёгкий шаг к полноценному реестру. Следующий, гораздо больший шаг (когда будет куплен домен и сервер) — настоящий веб-сервис с базой данных и HTTP API для поиска и публикации, полностью заменяющий этот git-индекс, а не дополняющий его.