Пакетный менеджер (сфера_пакеты)
Отдельный инструмент (не часть самого компилятора) для установки стороннего кода — 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-индекс, а не дополняющий его.