Skip to content

Упаковка и распространение

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; этот путь мы не проверяли.

hello.mojo
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.so1,27 МБMojo
libAsyncRTRuntimeGlobals.so0,69 МБMojo
libMSupportGlobals.so0,05 МБMojo
libstdc++, libgcc_s, libm, libc—система

Системные библиотеки есть на любом современном Linux. Три библиотеки Mojo — только там, где Mojo установлен.

Решение классическое для Linux: положить три библиотеки рядом с программой и сказать ей искать их относительно себя. $ORIGIN в RUNPATH означает «каталог того файла, в котором этот RUNPATH записан» — для программы это каталог, где лежит она сама:

Окно терминала
mkdir -p dist/lib
uv 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/hello
readelf -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-target
Effective 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:

vsum.mojo
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-51216
x86-64-v3AVX2, Haswell 2013 года и новее8
x86-64-v2, x86-64SSE4

Проверить сборку на другом процессоре, не имея его, можно эмулятором 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 # 4
objdump -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 и читается в коде на этапе компиляции:

defs.mojo
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, если оно и правда строковое.

С библиотекой задача другая: отдать не программу, а код, который кто-то подключит к своей программе. Способов три.

Самый надёжный вариант: каталог пакета лежит в git-репозитории, пользователь кладёт его к себе (копией, подмодулем git) и подключает флагом -I, как в главе «Модули и пакеты». Флаг указывает на каталог, внутри которого лежит пакет: если репозиторий shapes-lib склонирован в vendor/shapes-lib, а пакет — это его src/shapes, то:

Окно терминала
uv run mojo -I vendor/shapes-lib/src app.mojo

Исходники компилируются той версией Mojo, что стоит у пользователя. Если ваш код с ней совместим, всё работает: привязки к конкретной версии компилятора нет.

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-пакеты из следующего раздела.

Для публикации «чтобы ставилось одной командой» у Mojo есть общий канал modular-community. Библиотеки туда попадают как пакеты conda, которые собирает rattler-build по рецепту recipe.yaml: рецепт предкомпилирует пакет в .mojoc и кладёт его в $PREFIX/lib/mojo — тот самый каталог автопоиска (упаковка в документации Mojo). mkdir в рецепте нужен: mojo precompile, как и mojo build, каталог для результата сам не создаёт.

Вот рецепт библиотеки для Mojo 1.1 — по образцу реальных рецептов канала:

recipe.yaml
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) избавит пользователей от гадания, почему у соседа работает, а у них нет.

Модуль расширения из главы «Mojo из Python» зависит от тех же трёх библиотек рантайма, и лечится это тем же приёмом. Разница одна: библиотеки кладутся прямо рядом с .so, поэтому путь — просто $ORIGIN:

Окно терминала
mkdir -p dist
uv 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.

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0