Упаковка и распространение
This content is not available in your language yet.
Пока программа живёт на вашей машине, всё просто: uv run mojo app.mojo,
и готово. Трудности начинаются, когда её нужно кому-то отдать. Их две,
и обе не видны на машине автора:
- собранный бинарник не самодостаточен — он ищет библиотеки рантайма
Mojo по абсолютному пути внутри вашего
.venv; - по умолчанию он собирается под ваш процессор и на более старом может упасть — причём не сразу, а только на том участке кода, где компилятор воспользовался «лишними» инструкциями.
С библиотеками своя история: предкомпилированный .mojoc жёстко привязан
к версии компилятора.
Глава — про Linux на x86-64: именно там всё показанное проверено
на Mojo 1.1.0 (кроме рецепта conda — о нём отдельная оговорка). На macOS
механизм тот же, но инструменты другие: вместо ldd и readelf —
otool, вместо $ORIGIN — @loader_path; этот путь мы не проверяли.
Программа: mojo build
Заголовок раздела «Программа: mojo build»def main(): var total = 0 for i in range(10): total += i print("привет из Mojo, сумма:", total)привет из Mojo, сумма: 45
uv run mojo build hello.mojo./helloПолучается исполняемый файл hello размером около 18 КБ — имя берётся
из имени исходника, другое задаётся флагом -o. Запускается он из любого
каталога, исходники рядом не нужны.
Теперь скопируем его на машину, где Mojo не установлен:
./hello: error while loading shared libraries: libKGENCompilerRTShared.so:cannot open shared object file: No such file or directoryТу же ошибку вы получите и на своей машине, если удалите .venv или
переместите проект в другой каталог, — там не поможет даже uv sync.
Почему так
Заголовок раздела «Почему так»Посмотрим, что записано в самом файле:
readelf -d hello | grep -E 'NEEDED|RUNPATH' (NEEDED) Shared library: [libKGENCompilerRTShared.so] (NEEDED) Shared library: [libc.so.6] (RUNPATH) Library runpath: [/home/you/hello-mojo/.venv/lib/python3.11/site-packages/modular/lib]Программа просит рантайм Mojo и ищет его по абсолютному пути в вашем
виртуальном окружении. Сам рантайм тянет ещё две библиотеки — полный список
показывает ldd hello:
| Библиотека | Размер | Откуда |
|---|---|---|
libKGENCompilerRTShared.so | 1,27 МБ | Mojo |
libAsyncRTRuntimeGlobals.so | 0,69 МБ | Mojo |
libMSupportGlobals.so | 0,05 МБ | Mojo |
libstdc++, libgcc_s, libm, libc | — | система |
Системные библиотеки есть на любом современном Linux. Три библиотеки Mojo — только там, где Mojo установлен.
Переносимая сборка
Заголовок раздела «Переносимая сборка»Решение классическое для Linux: положить три библиотеки рядом с программой
и сказать ей искать их относительно себя. $ORIGIN в RUNPATH
означает «каталог того файла, в котором этот RUNPATH записан» — для
программы это каталог, где лежит она сама:
mkdir -p dist/libuv run mojo build hello.mojo -o dist/hello \ --target-cpu x86-64-v3 \ -Xlinker -rpath -Xlinker '$ORIGIN/lib'
for lib in $(ldd dist/hello | awk '/modular/ {print $3}'); do cp -L "$lib" dist/lib/doneПолучается каталог:
dist/├── hello 18 КБ└── lib/ ├── libAsyncRTRuntimeGlobals.so ├── libKGENCompilerRTShared.so └── libMSupportGlobals.soВсего около 2 МБ. Этот каталог можно заархивировать, распаковать на другом
Linux x86-64, подходящем под требования ниже, и запустить. Мы проверили,
спрятав .venv целиком: dist/hello работает, а обычный hello падает
с ошибкой выше.
Две оставшиеся библиотеки находятся сами: у библиотек рантайма тоже
прописан $ORIGIN, так что им достаточно лежать рядом друг с другом.
Три детали, на которых легко споткнуться:
- Кавычки вокруг
'$ORIGIN/lib'обязательны. Без одинарных кавычек bash подставит вместо$ORIGINпустую строку, и в файл запишется путь/lib— системный каталог, где ваших библиотек нет. - Каталог
distсоздавайте заранее.mojo build -o dist/helloсам его не создаёт:error: unable to write file. The path '…/dist' does not exist. - Абсолютный путь никуда не делся. Флаг добавляет
$ORIGIN/libв конецRUNPATH, а путь к вашему.venvостаётся первым. На вашей машине библиотеки берутся из.venv, на чужой — изlib/. Работает, но у этого два минуса: путь к вашему домашнему каталогу виден в файле любому, кто запуститreadelf, а запуск на своей машине не проверяет комплект —dist/libпри этом вообще не используется.
Оба минуса убирает утилита patchelf (есть в пакетах любого дистрибутива
и в PyPI): она переписывает RUNPATH целиком.
patchelf --set-rpath '$ORIGIN/lib' dist/helloreadelf -d dist/hello | grep RUNPATH (RUNPATH) Library runpath: [$ORIGIN/lib]Теперь и у вас dist/hello берёт библиотеки из dist/lib, так что
обычный запуск заодно проверяет, что комплект полный.
Про --target-cpu x86-64-v3 — следующий раздел.
Под какой процессор собирается программа
Заголовок раздела «Под какой процессор собирается программа»Спросим компилятор, под что он собирает по умолчанию:
uv run mojo build hello.mojo --print-effective-targetEffective target configuration: --target-triple x86_64-unknown-linux-gnu --target-cpu cascadelake --target-features +adx,+aes,+avx,+avx2,+avx512bw,+avx512cd,+avx512dq,+avx512f,...cascadelake — это процессор машины, на которой шла сборка; у вас там
окажется название вашего. Mojo по умолчанию собирает под тот
процессор, на котором запущен компилятор, со всеми его расширениями —
в нашем случае с AVX-512.
От этого зависит не только машинный код, но и значения, которые программа
видит на этапе компиляции. Вот сумма массива векторами той ширины, которую
считает оптимальной simd_width_of:
from std.sys import simd_width_of
def main(): comptime W = simd_width_of[DType.float32]() var data = List[Float32](capacity=4096) for i in range(4096): data.append(Float32(i % 7))
var acc = SIMD[DType.float32, W](0) for i in range(0, len(data), W): acc += data.unsafe_ptr().unsafe_load[width=W](i) print("ширина вектора:", W, "сумма:", acc.reduce_add())W — константа времени компиляции, поэтому она зашита в бинарник
и зависит от флага --target-cpu:
--target-cpu | Что это | simd_width_of[DType.float32]() |
|---|---|---|
| не указан | процессор сборки (здесь cascadelake) | 16 |
x86-64-v4 | с AVX-512 | 16 |
x86-64-v3 | AVX2, Haswell 2013 года и новее | 8 |
x86-64-v2, x86-64 | SSE | 4 |
Что будет на чужом процессоре
Заголовок раздела «Что будет на чужом процессоре»Проверить сборку на другом процессоре, не имея его, можно эмулятором
qemu-user: он выполняет программу, притворяясь процессором заданной модели.
Мы собрали vsum.mojo и hello.mojo по-разному и запустили в эмуляторе
на двух старых процессорах, а для сравнения — на настоящем сервере
с AVX-512 (AVX-512 эмулятор qemu 8.2 не поддерживает, так что третью
колонку в нём не воспроизвести):
| Сборка | Ivy Bridge (2012, AVX) | Haswell (2013, AVX2) | сервер с AVX-512 |
|---|---|---|---|
hello, по умолчанию | падает | работает | работает |
vsum, по умолчанию | падает | падает | ширина 16 |
vsum, --target-cpu x86-64-v3 | падает | ширина 8 | ширина 8 |
vsum, --target-cpu x86-64 | падает | ширина 4 | ширина 4 |
Проверка выглядит так:
qemu-x86_64 -cpu Haswell-noTSX ./vsumИз таблицы следуют три вывода.
Сборка по умолчанию — лотерея. hello, собранный под AVX-512, на Haswell
работает: компилятору просто не понадобились новые инструкции. vsum,
собранный так же, падает — там AVX-512 используется. Предсказать, в какую
категорию попадёт ваша программа, трудно: достаточно добавить один векторный
цикл. Задним числом это видно по машинному коду — инструкции AVX-512
работают с регистрами zmm:
objdump -d vsum | grep -c zmm # 4objdump -d hello | grep -c zmm # 0На настоящем процессоре без AVX-512 такая программа печатает стек вызовов
рантайма и завершается с Illegal instruction. (Эмулятор в этом случае
сообщает Segmentation fault — это его особенность, суть та же.)
Ниже x86-64-v3 опускаться бессмысленно. На Ivy Bridge падает всё,
даже hello, собранный под базовый x86-64. Виновата не ваша программа,
а библиотеки рантайма: они сами используют AVX2 и BMI. Это совпадает
с официальными требованиями — процессор уровня x86-64-v3, то есть
Haswell-класс, примерно с 2013 года
(требования Mojo).
Значит, для раздачи — --target-cpu x86-64-v3. Это минимум, на котором
Mojo вообще работает, и программа запустится на любом процессоре, где
запустится рантайм. Цена — более узкие векторы: 8 чисел Float32 вместо 16.
Если скорость на AVX-512 важна, соберите два варианта и выбирайте нужный
при установке.
Настройки сборки: -D
Заголовок раздела «Настройки сборки: -D»Иногда одну программу нужно собрать в нескольких вариантах: с большим
буфером и с маленьким, отладочную и боевую. Для этого значение передаётся
в компилятор флагом -D и читается в коде на этапе компиляции:
from std.sys import get_defined_int, get_defined_string, is_defined
def main(): comptime SIZE = get_defined_int["BUF_SIZE", 64]() comptime MODE = get_defined_string["MODE", "debug"]() print("буфер:", SIZE, "режим:", MODE, "VERBOSE:", is_defined["VERBOSE"]())буфер: 64 режим: debug VERBOSE: False
Без флагов работают значения по умолчанию — второй параметр. С флагами:
uv run mojo build defs.mojo -D BUF_SIZE=4096 -D MODE=release -D VERBOSE./defsбуфер: 4096 режим: release VERBOSE: TrueФлаг -D работает и с mojo run, но там важен порядок: всё, что стоит
после имени файла, уходит аргументами программе, и флаг молча
пропадёт. Правильно — uv run mojo run -D BUF_SIZE=8 defs.mojo.
Значения — константы времени компиляции:
от них можно ветвиться через comptime if, и лишняя ветка в бинарник
не попадёт (подробнее — в главе comptime).
Если значение не подходит по типу, сборка останавливается:
note: define 'BUF_SIZE' is not an integer, got "abc" : !kgen.string
get_defined_int ждёт целое число, а в -D BUF_SIZE=abc пришла строка.
Сообщение прячется среди строк note: про неудавшуюся инстанциацию —
ищите в выводе слово define.
Передайте число (-D BUF_SIZE=4096) или читайте значение через
get_defined_string, если оно и правда строковое.
Библиотека: три способа поделиться
Заголовок раздела «Библиотека: три способа поделиться»С библиотекой задача другая: отдать не программу, а код, который кто-то подключит к своей программе. Способов три.
1. Исходники
Заголовок раздела «1. Исходники»Самый надёжный вариант: каталог пакета лежит в git-репозитории, пользователь
кладёт его к себе (копией, подмодулем git) и подключает флагом -I, как
в главе «Модули и пакеты». Флаг указывает на каталог,
внутри которого лежит пакет: если репозиторий shapes-lib склонирован
в vendor/shapes-lib, а пакет — это его src/shapes, то:
uv run mojo -I vendor/shapes-lib/src app.mojoИсходники компилируются той версией Mojo, что стоит у пользователя. Если ваш код с ней совместим, всё работает: привязки к конкретной версии компилятора нет.
2. Предкомпилированный .mojoc
Заголовок раздела «2. Предкомпилированный .mojoc»mojo precompile собирает пакет в один файл. Проверим, как такой файл
переживает смену версии компилятора: соберём один и тот же пакет Mojo 1.0
и Mojo 1.1 и подключим каждый файл другой версией.
error: Mojo precompiled file is incompatible with the current version of the Mojo compiler. Precompiled file 'b/shapes.mojoc' version 1.0.0 is older than compiler version 1.1.0. To proceed, recreate the precompiled file with `mojo precompile`, or use the version of the compiler it was created with.
Файл собран Mojo 1.0, а подключаете вы его в Mojo 1.1. В обратную
сторону — то же самое, только is newer than. Совместимости нет
ни вперёд, ни назад.
Пересоберите .mojoc той версией компилятора, которой будете
пользоваться. Документация Modular прямо говорит, что .mojoc
«не предназначен для распространения как универсальный формат»:
он привязан к точной версии компилятора
(пакеты в Mojo).
Зато от процессора .mojoc не зависит вовсе: в нём лежит ещё
не специализированный код, а машинные инструкции появятся, только когда
пакет подключат к программе и соберут её. Так что один .mojoc годится
для любого процессора, но только для одной версии Mojo.
Куда класть, чтобы работало без -I. Компилятор сам смотрит в каталог
lib/mojo рядом со своей установкой — там лежит и стандартная библиотека,
файл std.mojoc. В проекте на uv это
.venv/lib/python3.*/site-packages/modular/lib/mojo/: положите туда
shapes.mojoc, и from shapes import area заработает из любого каталога
без флагов. Проверено. Руками так делать не нужно — но именно этим путём
идут conda-пакеты из следующего раздела.
3. Пакет conda в канале modular-community
Заголовок раздела «3. Пакет conda в канале modular-community»Для публикации «чтобы ставилось одной командой» у Mojo есть общий канал
modular-community. Библиотеки туда попадают как пакеты conda,
которые собирает rattler-build по рецепту recipe.yaml: рецепт
предкомпилирует пакет в .mojoc и кладёт его в $PREFIX/lib/mojo —
тот самый каталог автопоиска
(упаковка в документации Mojo).
mkdir в рецепте нужен: mojo precompile, как и mojo build, каталог
для результата сам не создаёт.
Вот рецепт библиотеки для Mojo 1.1 — по образцу реальных рецептов канала:
context: version: "0.3.0"
package: name: shapes-lib version: ${{ version }}
source: - git: https://github.com/you/shapes-lib.git rev: 0123456789abcdef0123456789abcdef01234567 # полный SHA коммита
build: number: 0 script: - mkdir -p "${PREFIX}/lib/mojo" - mojo precompile src/shapes -o "${PREFIX}/lib/mojo/shapes.mojoc"
requirements: build: - mojo-compiler ==1.1.0 host: - mojo-compiler ==1.1.0 run: - mojo-compiler ==1.1.0 # ровно та версия, что собирала .mojoc
tests: - script: - mojo run test_package.mojo files: recipe: - test_package.mojo
about: repository: https://github.com/you/shapes-lib license: Apache-2.0 license_file: LICENSE summary: Площади фигур
extra: maintainers: - you # ваш логин на GitHub: по нему канал сообщит, если сборка сломаетсяРецепт отправляется пулреквестом в репозиторий modular-community, пакет
собирается автоматически, а пользователь подключает канал в своём
pixi.toml и ставит библиотеку через pixi add.
Версионирование
Заголовок раздела «Версионирование»Из всего сказанного получается несколько простых правил.
- Указывайте, с какой версией Mojo работает библиотека. В README
и в рецепте. Mojo 1.x обещает стабильность исходного кода, но
не формата
.mojoc: переход с 1.0 на 1.1 сломал все предкомпилированные файлы, хотя исходники почти не пришлось трогать. - Каждый выпуск Mojo — новая сборка
.mojoc. Новая версия компилятора — новый номер сборки пакета (build.number) или новая версия библиотеки. - Держите тесты рядом с рецептом. Строчка
mojo run test_package.mojoв разделеtests— это проверка, что пакет подключается и работает именно той версией компилятора, под которую собран. - Для программ записывайте цель сборки.
x86-64-v3в имени архива (tool-1.2.0-linux-x86-64-v3.tar.gz) избавит пользователей от гадания, почему у соседа работает, а у них нет.
Модуль для Python
Заголовок раздела «Модуль для Python»Модуль расширения из главы «Mojo из Python»
зависит от тех же трёх библиотек рантайма, и лечится это тем же приёмом.
Разница одна: библиотеки кладутся прямо рядом с .so, поэтому путь —
просто $ORIGIN:
mkdir -p distuv run mojo build hotspot.mojo --emit shared-lib -o dist/hotspot.so \ --target-cpu x86-64-v3 \ -Xlinker -rpath -Xlinker '$ORIGIN'
for lib in $(ldd dist/hotspot.so | awk '/modular/ {print $3}'); do cp -L "$lib" dist/doneПосле этого import hotspot из каталога dist работает без Mojo и без
LD_LIBRARY_PATH — мы проверили, спрятав и .venv, и все прочие
установки Mojo. Заодно выяснилось, что один и тот же hotspot.so
импортировался и работал в Python 3.10, 3.11, 3.12 и 3.13.
Что дальше
Заголовок раздела «Что дальше»Программа собрана и упакована. Осталось убедиться, что она работает правильно, — тестирование и отладка.
🎯 Проверь себя
Программа из mojo build работает у вас, но на другой машине пишет «cannot open shared object file: libKGENCompilerRTShared.so». Почему?
Бинарник подключает рантайм Mojo динамически и ищет его по абсолютному
пути в вашем .venv (это записано в RUNPATH). На другой машине этого
пути нет. Нужно положить три библиотеки Mojo в подкаталог lib/ рядом
с программой и прописать ей путь $ORIGIN/lib — флагом
-Xlinker -rpath -Xlinker '$ORIGIN/lib' при сборке или через patchelf.
Почему hello, собранный без --target-cpu на машине с AVX-512, работает на Haswell, а программа с векторным циклом — нет?
Без --target-cpu сборка идёт под процессор машины сборки. В hello
компилятору не понадобились инструкции AVX-512, а в векторном цикле
понадобились — и на процессоре без них программа падает. Предсказать
это заранее трудно, поэтому для раздачи цель указывают явно.
Какую цель сборки выбрать для программы, которую будут запускать на разных x86-компьютерах, и почему не ниже?
--target-cpu x86-64-v3. Ниже опускаться бессмысленно: библиотеки
рантайма Mojo сами используют AVX2 и на более старых процессорах
не запустятся, даже если ваш код собран под базовый x86-64.
Можно ли выложить .mojoc, собранный Mojo 1.1, чтобы им пользовались люди с Mojo 1.2?
Нет. .mojoc привязан к точной версии компилятора, и другая версия
откажется его загружать — ни вперёд, ни назад совместимости нет.
Для такой аудитории раздавайте исходники или отдельные сборки под
каждую версию.
Что не так со строкой pin_compatible('mojo-compiler') в рецепте пакета?
По умолчанию она закрепляет только первый компонент версии и разрешает
>=1.1.0,<2.0a0. Компилятор 1.2 подходит под это условие, но не загрузит
.mojoc, собранный на 1.1.0. Нужна точная версия: mojo-compiler ==1.1.0.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0