# Глава 11: Сборка

Ручное написание скриптов сборки — это примерно так же весело, как отладка кода на ассемблере, и я уверен, что у вас есть дела получше, чем бороться с Makefile. Если мои предположения верны, система сборки Zig здесь, чтобы спасти положение. Это не типичная система сборки, которая держит вас за руку. Система сборки Zig похожа на того жестоко честного друга, который говорит вам в лицо всё, что не так с вашим кодом, даже если это немного больно. Она явная. Она мощная. И, осмелюсь сказать, она даже приятна в использовании (как только вы преодолеете начальную кривую обучения).

Это не пушистая система «всё просто работает». Это система «засучи рукава, пойми лежащие в основе механизмы и возьми управление на себя». Вы будете иметь дело с зависимостями, целями (targets), режимами релиза, кэшами и даже немного с теорией графов (не волнуйтесь, это не так страшно, как звучит).

Но в обмен на ваши усилия вы получите систему сборки, которая быстра, надёжна и удивительно гибка.

В этой главе мы рассмотрим следующие темы:

  • Система сборки Zig: автоматизация вашего рабочего процесса.
  • Кросскомпиляция: фирменный трюк Zig (одна кодовая база, 57 ошибок, специфичных для платформы).
  • Управление пакетами: потому что изобретать колесо — это прошлый век.
  • Кэш: память компилятора.
  • zig build --watch: ваш новый лучший друг (который не отвечает).

Затем мы перейдём к основным концепциям: build.zig.zon для зависимостей, zig fetch для их получения и важнейшей проверке хэша (потому что безопасность имеет значение даже в системах сборки).

Наконец, мы углубимся в продвинутые темы: понимание системы кэширования Zig (секрет её скорости), внешние зависимости и освоение zig build --watch для сверхбыстрого рабочего процесса разработки. К концу этого путешествия вы станете волшебником системы сборки Zig, способным создавать сборки наравне с лучшими из них. Так что пристегнитесь, возьмите чашку крепкого кофе и ныряйте!

# Технические требования

Весь код, показанный в этой главе, можно найти в каталоге Chapter11 нашего репозитория Git: https://github.com/PacktPublishing/Learning-Zig/tree/main/Chapter11.

# Система сборки Zig: автоматизация вашего рабочего процесса

Поскольку нам предстоит охватить много материала, давайте начнём с самой первой точки входа для создания вашего следующего шедевра на Zig. Встречайте zig init — способ Zig сказать: «Вот фонарик, теперь выбирайся из шахт шаблонного кода». Вы инициализируете проекты, разберёте build.zig и заставите систему сборки подчиняться вашей воле. Никаких заклинаний не требуется.

# zig init: стартовый набор вашего проекта (со свободным экзистенциальным ужасом)

Давайте перейдём сразу к делу (и тревоге). Просто введите zig init в новом каталоге в вашем терминале и нажмите Enter. Бум! Теперь вы официально инициированы.

Итак, вы выполнили zig init и получили файл build.zig, файл build.zig.zon и два файла Zig, которые вы не просили. Поздравляю — вы только что усыновили робота-дворецкого, который настаивает на том, чтобы складывать ваш код в оригами. Спойлер: Zig предполагает, что вы пишете библиотеку, даже если вы просто делаете приложение со списком дел.

Вот что вы получаете:

.
├── build.zig          # Ваш скрипт сборки (RIP Makefiles)
├── build.zig.zon      # Ответ Zig на "ад зависимостей"
└── src/
    ├── main.zig       # "Hello World", который вы удалите через 5 минут
    └── root.zig       # Пассивно-агрессивный толчок Zig к написанию библиотеки

Это пассивно-агрессивная иерархия:

  • build.zig: Скрипт сборки, созданный на чистом Zig. Здесь золото.
  • root.zig: Скелет библиотеки. «Но я просто хотел исполняемый файл!» Не повезло. Zig знает лучше, что ты хочешь.
  • main.zig: Ваше исполняемое приложение «Hello World», которое почти наверняка будет удалено в течение 5 минут, чтобы освободить место для ваших собственных творений.
  • build.zig.zon: Новый блестящий манифест пакета Zig (подумайте о package.json, но с меньшим количеством катастроф с left-pad). В настоящее время он пуст от фактического кода, но наполнен комментариями, как тот старый древний монолитный легаси-код, скрывающийся в корпоративных базах кода.

Мы здесь, чтобы исследовать систему сборки, поэтому давайте заглянем в эти два исходных файла, просто чтобы знать, что было сгенерировано.

# Инь и Ян близнецов Zig init

(Или почему Zig дал вам два файла, когда вы хотели только один)

Zig хочет, чтобы вы учились путём удаления. Файл main.zig — это временная демонстрация, жертвенный агнец. Вы должны выпотрошить его в течение нескольких минут и заменить своим кодом. Это песочница для вас, чтобы ломать, тестировать и сбрасывать буферы (буквально).

Zig подталкивает вас к модульному дизайну. root.zig — это скелет многократно используемой библиотеки, даже если сегодня вы просто создаёте инструмент командной строки. Zig предполагает, что со временем вы захотите делиться кодом, оптимизировать производительность или проводить боевые испытания компонентов — и он уже на шаг впереди вас.

Обратите внимание, что оба файла включают тесты, чтобы напомнить вам: пишите тесты заранее, иначе я сделаю ваши сеансы отладки сущим адом. Кроме того, Zig хочет, чтобы вы думали с точки зрения компонуемых частей, а не монолитных скриптов.

Даже если вы удалите root.zig завтра, само его существование научило вас структурировать код для гибкости. Основная идея, стоящая за zig init, заключается в том, чтобы дать вам два файла, потому что это контрол-фрик с благими намерениями. main.zig — это ваше сегодня, root.zig — это ваше завтра.

Теперь, когда вы были инициированы в мир двойного просветления исходных файлов Zig, пришло время познакомиться с кукловодом за кулисами: скриптом build.zig.

# Анатомия build.zig: что в коробке?

Zig не просто вручает вам фонарик, чтобы выбраться из шахт шаблонного кода, он даёт вам чертёж, чтобы построить собственный туннель побега. И этот чертёж написан на самом Zig. Никакого загадочного синтаксиса, никаких тайных заклинаний — только чистый, неразбавленный код Zig. Это не ящик Пандоры. В основном. Предполагая, что мы запустили zig init в каталоге под названием «zigt», вот пошаговое описание действий.

Во-первых, у нас есть библиотека, которую вы не хотели:

const lib = b.addStaticLibrary(.{
    .name = "zigt",
    .root_source_file = b.path("src/root.zig"),  // ← Тонкий намёк Zig
});

Имейте в виду, что она существует, потому что Zig предполагает, что вы в конечном итоге будете писать повторно используемый код. Может быть, вы этого не сделаете, но позвольте ему пока предаваться этой фантазии. Если мы хотим создать простой исполняемый файл, в скрипте есть другой раздел:

const exe = b.addExecutable(.{
    .name = "zigt",
    .root_source_file = b.path("src/main.zig"),  // Ваш фактический код
});

Также есть разделы, касающиеся запуска тестов проекта:

const exe_unit_tests = b.addTest(.{
    .root_source_file = b.path("src/main.zig"),
    .target = target,
    .optimize = optimize,
});

const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);

Перевод: Вы напишете тесты в конце концов. Вот строительные леса для вас.

Вы думаете о том же, о чём и я? Вы никогда раньше не писали и не читали скрипт сборки для проекта Zig, но благодаря всем знаниям, которые у вас есть до сих пор, вы просто читаете сборку без особых усилий, несмотря на функции, которых вы ещё не знаете!

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

# Использование zig build

Скрипт сборки содержит четыре основные вещи, которые вы, вероятно, делаете в ходе ежедневного рабочего процесса: запуск (run), тестирование (test), установка (install) и удаление (uninstall). Вам не нужно верить мне на слово, давайте проверим это вместе, выполнив zig build --help. Помимо бесчисленных опций, есть раздел под названием «Шаги»:

Steps:
  install (default)            Copy build artifacts to prefix path
  uninstall                    Remove build artifacts from prefix path
  run                          Run the app
  test                         Run unit tests

# Сборка

Здесь выполняется шаг по умолчанию (install). Другими словами, чтобы скомпилировать программу (и выполнить все шаги, связанные со сборкой), выполните команду zig build, а результат будет находиться по адресу zig-out/bin/zigt (и библиотека, которую вы никогда не собираетесь использовать):

zig-out
    ├── bin
    │   └── zigt
    └── lib
        └── libzigt.a

Ах, да! Вы всё правильно поняли. Шаг uninstall отменяет программы, созданные шагом install. Следующий шаг — запустить наши автоматизированные тесты. Мы же не животные, печатающие в stdout для проверки поведения программы, верно?

# Тестирование

Здесь снова представлена версия системы сборки команды zig test. Выполните в терминале zig build test. Она запускает тесты как для исполняемого файла, так и для библиотеки (которую вы не хотели писать). Если тесты не проходят, сообщения об ошибках Zig прожарят вас сильнее, чем костёр.

Давайте перейдём к последнему и самому долгожданному шагу: запуску.

# Запуск

Чтобы запустить наш исполняемый файл, мы всегда можем вызвать его после сборки, но когда вам нужно быстро выполнить эту задачу, есть ярлык: zig build run. Эта команда очень похожа на использование zig run src/hello.zig, исполняемый файл создаётся и запускается за один шаг. Когда мы используем zig build run, нам не нужно указывать исходный файл, поскольку он уже встроен в скрипт:

const std = @import("std");

pub fn build(b: *std.Build) void {
	// Стандартные параметры цели и оптимизации
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Создаём модуль для исполняемого файла
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    // Создаём артефакт исполняемого файла
    const exe = b.addExecutable(.{
        .name = "hello-zig",
        .root_module = exe_mod,
    });

    // Устанавливаем исполняемый файл в zig-out/bin
    b.installArtifact(exe);

    // Создаём шаг "run" для выполнения скомпилированного бинарного файла
    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep()); // Убедитесь, что он сначала собран.
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

main.zig:

const std = @import("std");

pub fn main() !void {
    // Точка входа для исполняемого файла
    std.debug.print("Hello, Student!\n", .{});
}

Проверьте сами!

Мы только что рассмотрели основы работы с build.zig, что даёт вам отправную точку для понимания того, как настраивать и выполнять сборки в Zig. Эти первые шаги предоставляют всё необходимое для начала работы, но впереди ещё много открытий, когда мы погрузимся глубже в мощную систему сборки Zig.

# Настройка build.zig: от «нормально» до «минимально жизнеспособного продукта»

Повышая градус, мы исследуем больше опций, таких как сборка для разных платформ (кроссплатформенная совместимость), оптимизация вашего кода для релиза (режимы выпуска) и погружение в тонкости того, как работают сборки под капотом — а именно, в структуру направленного ациклического графа (DAG), который управляет процессом сборки Zig.

Давайте начнём с кросскомпиляции!

# Кросскомпиляция: фирменный трюк Zig (одна кодовая база, 57 ошибок, специфичных для платформы)

Представьте, что вы создаёте игру, которая должна работать на Nintendo Switch, веб-сервер, нацеленный на облачную инфраструктуру на базе ARM, или даже крошечное устройство IoT с ограниченными ресурсами. Разве не было бы замечательно, если бы вы могли собирать программы для всех этих платформ, не покидая своей машины для разработки?

Что ж, с Zig вы можете это сделать. Возможности кросскомпиляции Zig настолько бесшовны, что кажутся волшебством. Больше никаких баталий со сложными наборами инструментов или молитв о том, чтобы ваши зависимости скомпилировались на чужой платформе. Система сборки Zig делает кросскомпиляцию такой же простой, как щелчок выключателем. Итак, давайте погрузимся в то, как вы можете собрать проект для любой платформы, любой архитектуры и любой цели — всё это с комфортом вашей собственной машины.

Вы помните параметр target, который появляется несколько раз в build.zig? Это ручка управления для контроля кросскомпиляции.

Позвольте мне объяснить вам: цель (target) — это резюме вашего компьютера, верно? Она включает в себя все детали об архитектуре процессора, включённых функциях, версии операционной системы и ABI — по сути, всё, что делает ваш код таким же портативным, как бетонный блок. Но не волнуйтесь, с Zig вам повезло! Этот язык общего назначения (потому что давайте посмотрим правде в глаза, специфика — это для птиц) разработан для генерации оптимального кода для огромного множества целей.

О, и если вам интересно узнать, какие существуют эти цели, ну, просто выполните команду zig targets — это похоже на магию, но без блёсток или веселья.

Теперь, по умолчанию, Zig компилирует ваш код для текущей машины. Это фантастика, потому что это означает, что ваш исполняемый файл будет бесполезен на любом другом компьютере. Великолепно! Но если вы каким-то образом хотите использовать свой код в другом месте (я знаю, радикальная идея), вам нужно будет указать опцию -target. Кроме того, наша любимая стандартная библиотека Zig имеет абстракции для кросс-платформенной работы, что можно перевести как «она пытается облегчить вашу жизнь, заставляя один и тот же исходный код работать на многих целях». Но эй, некоторый код просто более портируем, чем другой — например, как некоторые люди могут носить спортивный костюм, в то время как другим даже думать об этом не стоит. В целом, код на Zig чрезвычайно портируемый по сравнению с другими языками. Так что если вы устали от привязки своего кода к одной платформе, Zig вас прикроет — хотя я уверен, что всё ещё есть крайние случаи, которые заставят вас захотеть выбросить компьютер из окна.

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

Поддержка целей в Zig разделена на четыре уровня (Tier 1–4), которые отражают уровень поддержки (или любви) и надёжности при создании программного обеспечения для конкретных аппаратных архитектур. Понимание этих уровней помогает нам решить, какие цели подходят для их проектов, основываясь на стабильности, поддержке сообщества и экспериментальном статусе.

# Уровень 1: поддержка, готовая к производству

Цели в Уровне 1 имеют надёжную, хорошо протестированную поддержку в Zig. Эти цели считаются готовыми к производству и широко используются. Они получают покрытие непрерывной интеграции (CI), чтобы гарантировать их надёжную работу с последними обновлениями Zig.

Ключевые особенности целей Уровня 1:

  • Стабильный ABI (прикладной двоичный интерфейс): Соглашения о вызовах и двоичная совместимость хорошо определены и последовательны.
  • Комплексная поддержка стандартной библиотеки: Стандартная библиотека предоставляет всю необходимую функциональность для этих целей.
  • Регулярное тестирование: Эти цели проходят частое автоматизированное тестирование для раннего обнаружения регрессий.
  • Поддержка сообщества и экосистемы: Существует активное сообщество, вносящее вклад в инструменты, библиотеки и ресурсы для этих целей.

Примеры целей Уровня 1 включают:

  • x86_64 (Windows, macOS, Linux)
  • aarch64 (Apple Silicon macOS, Linux ARM64)
  • riscv64
  • PowerPC64

# Уровень 2: поддержка, поддерживаемая сообществом

Цели Уровня 2 имеют некоторый уровень поддержки в Zig, но могут быть не так тщательно протестированы или поддерживаться, как цели Уровня 1. Эти цели сильно зависят от вклада сообщества и могут потребовать дополнительных усилий для эффективного использования.

Ключевые особенности целей Уровня 2:

  • Частичная поддержка стандартной библиотеки: Некоторые части стандартной библиотеки могут работать, в то время как другие могут нуждаться в настройке.
  • Менее частое тестирование: Эти цели тестируются реже, что может приводить к ошибкам или проблемам с совместимостью.
  • Зависимость от вклада сообщества: Обслуживание и улучшения часто зависят от вклада пользователей, которые полагаются на эти цели.

Примеры целей Уровня 2 включают:

  • ARM32 (Linux)
  • MIPS
  • SPARC
  • Архитектуры «Система на кристалле» (SoC)

# Уровень 3: экспериментальная или минимальная поддержка

Цели Уровня 3 имеют минимальную поддержку в Zig и считаются экспериментальными. Эти цели могут быть плохо задокументированы или тщательно протестированы, и для их эффективного использования часто требуются значительные усилия.

Ключевые особенности целей Уровня 3:

  • Очень слабая поддержка стандартной библиотеки: Стандартная библиотека имеет ограниченные знания об этих целях или не имеет их вовсе.
  • Требуется ручная настройка: Пользователям может потребоваться вручную настраивать компилятор, указывать размеры целых чисел C или предоставлять собственные соглашения о вызовах.
  • Маловероятно, что они будут тестироваться регулярно: Эти цели не тестируются часто и могут иметь проблемы с совместимостью с более новыми версиями Zig.

Примеры целей Уровня 3 включают:

  • Hexagon
  • AMDGPU
  • SPARC (32-битная)
  • Некоторые встраиваемые архитектуры

# Уровень 4: устаревшая или унаследованная поддержка

Цели Уровня 4 либо устарели их соответствующими поставщиками, либо предоставляют только экспериментальную поддержку. Эти цели могут не обновляться в будущем и должны использоваться с осторожностью.

Ключевые особенности целей Уровня 4:

  • Не рекомендуется поставщиками: Эти цели больше не поддерживаются производителями оборудования или программными экосистемами.
  • Экспериментальный статус в Zig: Они могут требовать специальной конфигурации или пользовательских сборок LLVM для использования.
  • Ограниченная функциональность: Некоторые цели могут поддерживать только вывод ассембблерного кода (-femit-asm) и не объектные файлы.

Примеры целей Уровня 4 включают:

  • AVR
  • RISC-V32
  • xCore
  • MSP430

Слишком много информации? Примите это как общее руководство:

  • Уровень 1: Идеально подходит для производственных сред из-за их надёжности и всесторонней поддержки.
  • Уровень 2: Подходит для проектов, требующих менее распространённых архитектур, но готовых приложить усилия для тестирования и обслуживания.
  • Уровни 3 и 4: Лучше всего зарезервировать для экспериментальных или нишевых случаев использования, когда специфические возможности цели оправдывают риски.

# Использование цели

Прежде чем мы начнём втискивать значения в target, крайне важно понять, что такое target в скрипте. Итак, target — это тип ResolvedTarget. Другими словами, наша первая строка, определяющая target в файле build.zig, должна быть написана так:

const target: ResolvedTarget = b.standardTargetOptions(.{});

В системе сборки Zig цели задаются с помощью Target.Query, который преобразуется в ResolvedTarget, содержащий как сам запрос, так и полностью определённые детали цели. Вот как это работает.

Target.Query представляет собой частичную спецификацию цели (например, из параметров командной строки). Она может включать следующее:

  • Триплет (например, x86_64-linux-gnu)
  • Возможности процессора (CPU features)
  • Путь к динамическому компоновщику

Примечание: Триплет в этом контексте — это стандартизированный строковый формат, определяющий следующие части целевой платформы:

  • Архитектура (тип ЦП)
  • Операционная система
  • ABI/среда выполнения

Это формат: <архитектура>-<ОС>-<ABI>.

Примеры включают:

  • x86_64-linux-gnu (настольный Linux)
  • aarch64-macos-none (Mac на Apple Silicon)
  • wasm32-freestanding-musl (WebAssembly)

Он задаётся с помощью флага -Dtarget=<триплет> и обрабатывается внутри через std.Target.Query.parse().

ResolvedTarget создаётся путём разрешения запроса с помощью std.zig.system.resolveTargetQuery(). Он содержит исходный запрос и полный объект std.Target со всеми заполненными деталями.

Понимая уровни поддержки целей в Zig, разработчики могут принимать обоснованные решения о том, какие архитектуры использовать для своих проектов, балансируя стабильность, производительность и ресурсы сообщества.

Ключевые функции в кодовой базе Zig (build.zig) в этом рабочем процессе следующие:

// Разбирает параметры командной строки в Target.Query
pub fn standardTargetOptionsQueryOnly(b: *Build, args: StandardTargetOptionsArgs) Target.Query

// Преобразует запрос в полную конфигурацию цели
pub fn resolveTargetQuery(b: *Build, query: Target.Query) ResolvedTarget

Хорошо-хорошо! Я чувствую, как вы хмуритесь и сопротивляетесь желанию пропустить все детали. Держитесь крепче! Есть ещё один фрагмент информации перед настройкой целей. Мы должны знать, что такое StandardTargetOptionsArgs. К счастью, их всего несколько: whitelist и default_target.

Лично я думаю, что whitelist можно было бы просто назвать allowed_targets. Но что я знаю, верно?

Оба они являются Target.Query, но whitelist — это срез (slice), а default_target — одиночное значение:

  • whitelist: Эта опция ограничивает допустимые цели сборки для обеспечения совместимости.
  • default_target: Это запасной вариант, когда цель не указана.

Опыт пользователя (ваш опыт) должен заключаться в вызове zig build для использования цели по умолчанию и zig build -Dtarget=... для её переопределения.

Сравним использование по умолчанию и с параметрами цели:

const target = b.standardTargetOptions(.{});

затем

const target = b.standardTargetOptions(.{
    .whitelist = &.{ // Необязательный список разрешённых целей
        .{ .cpu_arch = .x86_64, .os_tag = .linux },
        .{ .cpu_arch = .aarch64, .os_tag = .macos }
    },
    .default_target = .{} // Нативная цель
});

Мы можем изучить это более подробно, чтобы было легче понять:

const allowed_targets = &[_]std.Target.Query{
    .{ // x86_64 Linux
        .cpu_arch = .x86_64,
        .os_tag = .linux,
        .abi = .gnu
    },
    .{ // ARM macOS
        .cpu_arch = .aarch64,
        .os_tag = .macos
    }
};

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{
        .whitelist = allowed_targets,
        .default_target = .{}
    });
    // ... используем target ...
}

Соединяются ли кусочки? Разрешённые цели — это срез std.Target.Query, который ориентирует standardTargetOptions для достижения кросскомпиляции. Фух! Какая поездка, а?

Zig кросскомпилирует так, будто играет в Stardew Valley на лёгком уровне сложности.

При использовании недопустимой цели:

$ zig build -Dtarget=riscv64-windows-msvc
error: выбранная цель 'riscv64-windows-msvc' не входит в разрешённый список
Разрешённые цели:
  - x86_64-linux-gnu
  - aarch64-macos-none

zig build -Dtarget=riscv65-windows-msvc не удалось разобрать цель 'riscv65-windows-msvc': UnknownArchitecture
zig build -Dtarget=riscv64-windows11-msvc неизвестная ОС: 'windows11' доступные операционные системы: freestanding ananas (длинный список ОС)
zig build -Dtarget=riscv64-windows-hello не удалось разобрать цель 'riscv64-windows-hello': UnknownApplicationBinaryInterface

Эта система обеспечивает надёжное управление целями, сохраняя при этом фирменный опыт разработчика Zig. Сочетание гибких триплетов, безопасного «белого списка» и разумных значений по умолчанию делает кросскомпиляцию доступной, но контролируемой. И да, здесь нет этой ерунды ./configure && make && sudo make install.

# Почему это важно?

  • Безопасность кросскомпиляции: «Белый список» предотвращает недопустимые комбинации целей.
  • Руководство для пользователя: Чёткие ошибки помогают разработчикам указывать допустимые цели.
  • Согласованность проекта: Они гарантируют, что все члены команды собирают проект для одних и тех же целей.
  • Надёжность CI/CD: Они предотвращают случайные сборки для неправильных архитектур.

За кулисами происходит так много всего, а нам просто нужно передать простые параметры. Это ещё один урок прагматизма от Zig. Ваш бинарный файл для Windows компилируется на Linux, пока вы спорите о табах и пробелах.

Теперь, когда вы увидели, как система сборки Zig может справиться с основами и даже кросскомпилировать для разных платформ, давайте поговорим об оптимизации вашего кода для реального использования.

# Режимы релиза

Независимо от того, создаёте ли вы высокопроизводительную игру, встраиваемую систему с ограниченными ресурсами или производственный веб-сервис, вам нужно будет подумать о том, как компилируется ваш код. Должен ли он быть быстрым? Маленьким? Удобным для отладки? Режимы релиза Zig дают вам точный контроль над этими компромиссами.

С помощью нескольких изменений в файле build.zig вы можете оптимизировать код для скорости, уменьшить размер бинарного файла или включить подробную отладочную информацию. Это как иметь регулятор производительности для вашего кода — и Zig упрощает его использование. Итак, давайте исследуем, как вы можете использовать режимы релиза Zig, чтобы сделать ваши приложения быстрее, стройнее и готовыми к реальному миру.

# Оптимизации: Отладка против Релиза (выберите свой яд)

Снова вернувшись к первым строкам скрипта build.zig, мы можем найти следующую строку:

const optimize = b.standardOptimizeOption(.{}); // Debug = "Crash Verbosely", Release = "Crash Fast"

Но подождите. Что ожидает получить эта функция? Мы можем изучить сигнатуру функции, чтобы лучше понять её и с лёгкостью ориентироваться в будущих проблемах:

pub fn standardOptimizeOption(b: *Build, options: StandardOptimizeOptionOptions) std.builtin.OptimizeMode

Чтобы раскрыть скрытую информацию о типах в нашем скрипте сборки, мы можем написать ту же самую строку следующим образом:

const optimize: std.builtin.OptimizeMode = b.standardOptimizeOption(std.Build.StandardOptimizeOptionOptions{});

Не беспокойтесь об этой многословной версии. Мы не будем её использовать, как только поймём, что происходит. Главное, что нужно знать об этой функции, — это то, что параметры StandardOptimizeOptionOptions имеют всего один параметр, preferred_optimize_mode, типа std.builtin.OptimizeMode, который в точности равен возвращаемому значению функции. Другими словами, вы можете выбрать предпочтительный режим оптимизации.

Переходя к определению std.builtin.OptimizeMode, мы можем найти все опции, объявленные в этом перечислении:

pub const OptimizeMode = enum {
    Debug,
    ReleaseSafe,
    ReleaseFast,
    ReleaseSmall,
};

Хм... Интересно. Но что означают эти значения? Что ж, вы часто будете встречать эти значения как режимы релиза. Zig предоставляет четыре различных режима релиза, которые предлагают разные компромиссы между производительностью, безопасностью и размером бинарного файла. Мы можем использовать их для настройки нашего скрипта сборки (и выбирать их во время компиляции).

Вы можете задаться вопросом, почему это называется OptimizeMode, а не ReleaseMode. Что ж, ответ таков: в кодовой базе существуют «настоящие» режимы релиза, но они находятся на один уровень абстракции глубже, чем OptimizeMode, и называются, как вы догадались, ReleaseMode:

pub const ReleaseMode = enum {
    off,
    any,
    fast,
    safe,
    small,
};

Вы не трогаете их, потому что функция standardOptimizeOption выполняет всю тяжёлую работу за вас. Если вы всё же хотите напрямую взаимодействовать с ReleaseMode, вы должны получить к нему прямой доступ из текущей ссылки на std.Build:

b.release_mode = std.Build.ReleaseMode.any;

Когда режим релиза не установлен явно, значением по умолчанию является off, что переводится в OptimizeMode.Debug. Вы всё ещё можете выбрать одно из оставшихся значений, но если вы выберете значение any, вы отложите принятие решения. В этом случае вывод выполнения будет кричать на вас: «Проект не объявляет предпочтительный режим оптимизации. Выберите: --release=fast, --release=safe или --release=small».

Вернёмся к компромиссам. Давайте исследуем один из режимов релиза.

# Debug

Debug — это режим релиза по умолчанию. Это осторожный целитель. Тот, кто дважды проверяет каждую дверь на наличие ловушек. Оптимизации отключены, а предохранитель включён. Это означает, что время компиляции сокращается, но это приводит к большим потерям производительности во время выполнения, и мы отказываемся от воспроизводимых сборок. Думайте об этом режиме как о режиме «разработки и отладки»:

// Режим Debug: «Ты уверен, что хочешь это сделать?»
var x: u8 = 255;
x += 1; // Паникует: «Обнаружено переполнение целого числа!»

# ReleaseSafe

Это наш сбалансированный воин. Надёжный боец, который быстр, но всё ещё носит шлем. Это первый режим для использования в продакшене. Это первый режим для использования в рабочей среде. Однако у этого режима есть недостатки, такие как умеренные потери производительности во время выполнения и более медленная компиляция. На этот раз у нас есть воспроизводимые сборки. Это режим, в котором безопасность всё ещё имеет решающее значение:

// ReleaseSafe: «Я защищу тебя, но не доводи до предела».
var x: u8 = 255;
x += 1; // Всё ещё паникует, но быстрее!

Чтобы изменить это в сборке, установите b.release_mode = std.Build.ReleaseMode.safe или через интерфейс командной строки с аргументом zig build -Doptimize=ReleaseSafe.

# ReleaseFast

ReleaseFast — это то, где всё становится по-зиговски. Этот режим — берсерк. Безрассудный варвар, который бросается в атаку, не задавая вопросов. Он нацелен на продакшен, но с критически важными требованиями к производительности. Проверки безопасности отключены, что напрямую переводится в лучшую производительность во время выполнения:

// ReleaseFast: «Что такое проверка безопасности? В АТАКУ!»
var x: u8 = 255;
x += 1; // Неопределённое поведение. Удачи!

Чтобы изменить это в сборке, установите b.release_mode = std.Build.ReleaseMode.fast или через интерфейс командной строки с аргументом zig build -Doptimize=ReleaseFast.

# ReleaseSmall

Это режим для ограниченных сред. Скрытный разбойник, который проскальзывает сквозь трещины и не оставляет следов. В этом режиме мы получаем ту же производительность, что и с ReleaseSafe, но с меньшим размером бинарного файла:

// ReleaseSmall: «Я впишусь куда угодно, но не проси о помощи».
var x: u8 = 255;
x += 1; // Неопределённое поведение, но эй, оно крошечное!

Используйте команду zig build -Doptimize=ReleaseSmall, чтобы сделать ваш бинарный файл меньше вашего эго.

Вот в чём суть: хотите скорость? Вы пожертвуете безопасностью. Хотите безопасность? Вы пожертвуете скоростью. Это как выбор между щитом и цвайхандером (двуручный меч, — прим. переводчика) — оба хороши, но вы не можете владеть обоими сразу (по крайней мере, в текущей версии Zig).

Вот практическая демонстрация того, как разные режимы релиза влияют на поведение во время выполнения. Сначала создайте файл build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{}); // Позволить пользователю выбрать режим оптимизации из командной строки
    const optimize = b.standardOptimizeOption(.{});

    // Создать модуль для исполняемого файла
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "safety-demo",
        .root_module = exe_mod,
    });

    b.installArtifact(exe);
    const run_cmd = b.addRunArtifact(exe);
    const run_step = b.step("run", "Run with selected optimization");
    run_step.dependOn(&run_cmd.step);
}

Затем создайте файл main.zig:

const std = @import("std");

pub fn main() void {
    var x: u8 = 255;
    std.debug.print("Initial value: {}\n", .{x});
    // Это вызовет переполнение.
    x += 1;
    // Эта строка может быть достигнута или нет, в зависимости от режима релиза.
    std.debug.print("Value after overflow: {}\n", .{x});
}

Попробуйте запустить это с разными режимами оптимизации:

  • zig build run -Doptimize=Debug (паникует с обнаружением переполнения)
  • zig build run -Doptimize=ReleaseSafe (паникует с обнаружением переполнения)
  • zig build run -Doptimize=ReleaseFast (неопределённое поведение, скорее всего, оборачивается в 0)
  • zig build run -Doptimize=ReleaseSmall (неопределённое поведение, оптимизировано по размеру)

# Смешивание и сопоставление

Хотя вы не можете использовать два режима оптимизации одновременно, вы можете смешивать режимы в рамках одного проекта. Например, вы можете скомпилировать свою основную логику в ReleaseFast, оставив тесты в Debug. Это как иметь берсерка на передовой и целителя в тылу:

const core = b.addExecutable(.{
    .name = "core",
    .root_source_file = .{ .path = "src/core.zig" },
    .optimize = .ReleaseFast,
});

const tests = b.addTest(.{
    .root_source_file = .{ .path = "src/tests.zig" },
    .optimize = .Debug,
});

# Неопределенное поведение и детальный контроль

Zig использует неопределённое поведение (UB) для оптимизаций, но не стесняется этого. В режимах ReleaseFast и ReleaseSmall UB — это как дракон: мощный, но опасный. Если вы не будете осторожны, он сожжёт ваш код дотла:

@setRuntimeSafety(false); // «Я знаю, что делаю, Zig. Доверься мне».
var x: u8 = 255;
x += 1; // Неопределённое поведение. Вы были предупреждены.

Zig предоставляет детальный контроль с помощью автоматических проверок безопасности в Debug/ReleaseSafe и явных небезопасных блоков с использованием @setRuntimeSafety(false).

Вот где вы можете использовать детальный контроль над безопасностью:

fn optimizedFunction() void {
    @setRuntimeSafety(false);
    // остальная часть функции опущена.
}

Один важный аспект @setRuntimeSafety заключается в том, что управление устанавливается для области видимости, содержащей вызов функции. Таким образом, вы можете установить безопасность для одного блока кода и изменить её для другого блока, независимо от того, насколько глубоко он находится.

Примечание для читателей в будущем: существуют планы переименовать @setRuntimeSafety в @optimizeFor. Если вы не можете найти @setRuntimeSafety, это означает, что будущее уже наступило.

На данный момент мы изучили два стандартных варианта (standardTargetOptions и standardOptimizeOption), но как насчёт наших собственных пользовательских вариантов? Давайте начнём настраивать наши собственные опции.

# Пользовательские опции

Прежде чем мы углубимся, подумайте о пользовательской опции как о ключевой точке принятия решения в вашем процессе сборки. Подобно хорошо спроектированной функции в программировании, она принимает входные данные (возможно, аргумент или условие) и на основе этого ввода направляет поток вашего процесса сборки таким образом, который является не только эффективным, но и ясным и поддерживаемым. Итак, как вы вводите эти «точки принятия решений» в свой проект Zig? Что ж, это начинается с понимания того, что опции в Zig — это не просто простые переключатели.

Это гибкие конструкции, очень похожие на то, как мы создаём чистый, читаемый код — чтобы быть динамичными и выразительными, но не перегружающими.

Представьте, что вы разрабатываете мультиплатформенное приложение и хотите дать пользователям возможность включать или отключать специальную функцию, например, тёмный режим для пользовательского интерфейса, прямо через конфигурацию сборки. Это не просто включение или выключение чего-либо. Возможно, функцию нужно активировать только при определённых условиях — например, в зависимости от среды или выбора разработчика.

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

Рассмотрим простую опцию для управления версиями:

const enable_dark_mode = b.option(bool, "enable_dark_mode", "Enable Dark Mode UI") orelse false;
const options = b.addOptions();
options.addOption([]const u8, "dark_mode_enabled", enable_dark_mode);
exe.root_module.addOptions("ui", options);

В этом примере опция enable_dark_mode становится переключателем для включения темы пользовательского интерфейса «Тёмный режим». При сборке проекта разработчики (или даже конвейеры CI/CD) могут установить эту опцию в значение true или false. Если она не установлена, по умолчанию используется значение false, что означает использование светлой темы.

При сборке приложения, если разработчик решит протестировать приложение с включённым тёмным режимом, он может просто указать эту опцию следующим образом:

zig build -Denable_dark_mode true

В программе вы можете импортировать эти опции так же просто, как с помощью @import("ui"), и это известно во время компиляции. Другими словами, эта единственная строка изменяет поведение приложения, демонстрируя, как пользовательские опции в Zig могут сделать ваши сборки более адаптируемыми и отзывчивыми к различным потребностям без изменения основного кода.

Вот полная реализация функции пользовательских опций:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Определяем пользовательскую булеву опцию для тёмного режима
    const enable_dark_mode = b.option(bool, "dark_mode", "Enable Dark Mode UI") orelse false;

    // Создаём модуль опций для передачи значения в программу
    const options = b.addOptions();
    options.addOption(bool, "is_dark_mode_enabled", enable_dark_mode);

    // Создаём модуль для исполняемого файла
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const exe = b.addExecutable(.{
        .name = "ui-app",
        .root_module = exe_mod,
    });

    // Добавляем опции под именем "config"
    exe.root_module.addOptions("config", options);
    b.installArtifact(exe);
    const run_cmd = b.addRunArtifact(exe);
    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

Вот файл main.zig:

const std = @import("std");

// Импортируем модуль опций, созданный в build.zig
const config = @import("config");

pub fn main() void {
    if (config.is_dark_mode_enabled) {
        std.debug.print("UI is running in Dark Mode!\n", .{});
    } else {
        std.debug.print("UI is running in Light Mode.\n", .{});
    }
}

Попробуйте запустить этот пример:

  • zig build run (выводит «UI is running in Light Mode.»)
  • zig build run -Ddark_mode=true (выводит «UI is running in Dark Mode!»)

Красота этого подхода в том, что конфигурация известна во время компиляции, что позволяет компилятору оптимизировать неиспользуемые ветви кода.

Это лишь самое начало системы сборки Zig, и мы должны продолжить изучение нескольких функций, и, поверьте мне, их десятки. Эта книга называется «Изучаем Zig», поэтому я хочу дать вам инструменты для понимания системы сборки и того, как двигаться дальше в своих исследованиях. Вы же не ожидаете большой туториал для копирования и вставки, верно? Давайте поймём сущности системы сборки.

# Команда сборки

Система сборки Zig — это команда:

  • Build — это руководитель проекта.
  • Steps — это работники.
  • Modules — это результаты работы.
  • Dependencies — это субподрядчики.
  • LazyPath и Cache — это инструменты и логистика.

У каждого члена есть своя роль, а DAG — это рабочий процесс, который держит их всех в синхронизации.

На самом первом месте в списке у нас есть наш хорошо знакомый Build, который выступает в роли менеджера, координирующего всех работников (steps). Как мы видели в предыдущих примерах, это корневой объект, создаваемый в скрипте build.zig:

pub fn build(b: *std.Build) void { ... }

Он управляет всем графом сборки, опциями и зависимостями.

# Шаги: атомарные действия конвейера сборки

В театре разработки программного обеспечения шаги — это сольные исполнители, каждому из которых поручена точная, но не гламурная роль, которая в совокупности воплощает проект в жизнь. Это заклёпки на сборочной линии, невоспетые герои воспроизводимости. Объявить шаг — значит поставить хореографию намерения: здесь скомпилируй это. Там — выполни то. Теперь положи артефакт сюда. Система сборки, всегда верный помощник режиссёра, гарантирует, что каждый актёр попадёт в свою точку.

# Таксономия шагов

Шаги следуют философии единственной цели. Как и инструменты Unix, они делают что-то одно, но хорошо, и при этом элегантно компонуются. Давайте разберём архетипы:

  • Step.Compile: Переводчик. Он берёт сырой диалект Zig, C или C++ и сплетает его на машинный язык. За кулисами он договаривается об оптимизациях, включении библиотек и тонком искусстве не выходить за пределы памяти. Например, компиляция файла .zig в исполняемый файл сродни выпечке суфле — точность в ингредиентах (флаги) и времени (зависимости) имеет значение.
  • Step.Run: Исполнитель. Этот шаг — наёмный работник, выполняющий команды оболочки, скрипты генерации кода или даже вызывающий другие инструменты в вашем наборе. Представьте его как консьержа: «Хотите сгенерировать привязки protobuf перед компиляцией? Конечно».
  • Step.Install: Архивариус. Он копирует бинарные файлы, заголовки или ресурсы в назначенные им папки, наводя порядок из хаоса промежуточных файлов. Без него ваши артефакты сборки разлетелись бы как сиротские носки.
  • И ваши собственные пользовательские шаги: Джокеры. Это шаги, которые вы определяете для обработки задач, которые Zig не предусмотрел — упаковка ресурсов, загрузка релизов или даже отправка поздравительного сообщения в Slack после сборки. Пользовательские шаги — это то место, где система сборки признаёт: «Хорошо, ты здесь творческий человек».

# Пользовательские шаги: аварийный люк системы сборки

Определить пользовательский шаг — значит впрыснуть свою собственную логику в вены Zig. Предположим, вы хотите упаковать ресурсы для игры:

const bundle_step = b.step("bundle-assets", "Pack textures and sounds");

const bundle_assets = b.addSystemCommand(&.{
    "python3", "scripts/bundle_assets.py", "--output", "assets.zip"
});

bundle_step.dependOn(&bundle_assets.step);

Это уступка со стороны системы сборки: «Ты прав, я не могу сделать всё. Но я прослежу, чтобы твой скрипт сработал в самый подходящий момент». Пользовательские шаги — это то место, где точность Zig встречается с вашим хаосом — рукопожатие между фреймворком и творчеством.

# Принцип побочного квеста в RPG

«Тестовые шаги — это побочные квесты в RPG. Пропустишь их — и твой проект останется посредственным».

Тесты — это необязательные квесты, которые, если их игнорировать, оставляют вашу кодовую базу уязвимой для драконов (регрессий) и проклятого лута (багов). Функция addTest в Zig — это мастер гильдии, который подталкивает вас: «Ты можешь броситься выполнять основной квест, но выживешь ли ты в битве с финальным боссом?». Привязывая тесты к DAG сборки, вы гарантируете, что они не просто запоздалая мысль — они являются частью пути героя.

# Философия под капотом

Система сборки Zig — это исследование ограниченной гибкости. Она даёт вам лексику шагов и грамматику (DAG) для их компоновки, но сопротивляется искушению стать всемогущим движком для скриптов. Как опытный плотник, она верит в острые, хорошо определённые инструменты — а не в один швейцарский армейский нож.

Когда вы в следующий раз будете писать скрипт build.zig, помните, что вы не просто компилируете код. Вы составляете чертёж, ставите пьесу и да — иногда задабриваете этого пассивно-агрессивного дворецкого.

Хорошо, давайте попробуем ещё раз разобраться с этой системой сборки Zig, а? Вы думаете, что видели всё? Я кручусь в этом деле с тех пор, когда перфокарты были в ходу, и позвольте мне сказать вам, система сборки Zig... ну, она особенная. Она как тот эксцентричный дядюшка в программировании: немного странный, но как только ты его узнаёшь поближе, понимаешь, что у него есть на удивление хорошие идеи. Считайте меня вашим потрёпанным проводником по этой дикой местности. Я видел больше систем сборки, чем у вас было горячих ужинов, и я здесь, чтобы сказать вам: на Zig стоит взглянуть. Но предупреждаю: она не будет с вами нянчиться. Это система типа «закатай рукава и испачкай руки». Готов нырнуть? Хорошо. Приступим.

В старые добрые времена мы просто сваливали весь наш код в один файл и молились. Теперь все одержимы «модульностью». У Zig, в его бесконечной, причудливой мудрости, есть свой взгляд на это, который он, что неудивительно, называет модулями.

# DAG: более строгий кузен Git

Если шаги — это актёры, то DAG — это сюжет. Он диктует, кто говорит первым, кто ждёт за кулисами и кто должен закончить до того, как поднимется занавес. DAG Zig — это более строгий кузен Git, не из злобы, а из-за строгости — он обеспечивает порядок зависимостей с суровостью библиотекаря, который повидал слишком много просроченных книг.

Когда вы объявляете test_step.dependOn(&run_tests.step), вы не просто добавляете задачу в очередь. Вы высекаете правило на каменной скрижали DAG: «Не пройдёшь дальше, пока эти тесты не будут запущены».

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

# Декларативный синтаксис: иллюзия простоты

Определение шагов в build.zig кажется обманчиво простым. Рассмотрим этот фрагмент:

const tests = b.addTest(.{
    .root_source_file = .{ .path = "src/tests.zig" },
});

const run_tests = b.addRunArtifact(tests);
test_step.dependOn(&run_tests.step);

Здесь мы срежиссировали миниатюрную сагу:

  • Создать артефакт теста: Бинарный файл, который воплощает ваш набор тестов.
  • Подготовить его к выполнению: addRunArtifact оборачивает его в Step.Run.
  • Привязать его к шагу теста: Зависимость, которая гарантирует, что команда zig build test запускает проверку.

Декларативный синтаксис маскирует механизмы под капотом. Это как заказывать кофе, описывая вкус — бариста (Zig) мелет зёрна, вспенивает молоко и подаёт вам чашку, пока вы абстрактно размышляете о кофеине.

# Модули

Для начала давайте попробуем понять, что такое модуль в извращённом уме Zig. Это как идеально организованный ящик для инструментов, где у каждого инструмента есть своё место. Каждый модуль — это не просто хаотичная коллекция кода. О нет, это тщательно подобранный набор исходных файлов, зависимостей и конфигураций. Это самодостаточная единица, но давайте будем честны, это больше похоже на крошечное, самодовольное королевство.

# Острова кода

Каждый модуль — это маленький остров кода. У него есть свои исходные файлы, свои маленькие правила и раздутое чувство собственной важности. Это как крошечное королевство, где живёт ваш код. Но помните: с большой силой приходит большая ответственность не устраивать беспорядок.

// Это модуль во всей его славе
const my_module = b.addExecutable(.{
    .name = "my_precious",
    .root_source_file = .{ .path = "src/main.zig" },
});
// Узрите королевство "my_precious"

# Коллекционеры зависимостей

Модули любят собирать зависимости, как дракон любит золото. Именно здесь они отслеживают все другие маленькие королевства, от которых зависят. Zig не стесняется этого, всё это выложено на виду, явно, как налоговая декларация:

my_module.addImport("utils", utils_module);
// «Мне нужны 'utils' для работы. Иди и принеси их, крестьянин!»

# Фанатики конфигурации

Если вы думали, что вы привередливы в настройках табуляции в редакторе, подождите, пока не увидите модули Zig. Архитектура, оптимизация, настройки отладки — у них есть мнение обо всём. Это как иметь менеджера проекта, который также является помешанным на контроле офисного термостата:

my_module.setTarget(target);
my_module.setOptimizeOptions(.ReleaseSafe);
// «Скомпилируй это для ЭТОЙ архитектуры и оптимизируй, но не смей делать это небезопасным!»

# Круговорот (модульной) жизни: от объявления к доминированию

Модули не рождаются полностью сформированными. Они проходят через «жизненный цикл», что является причудливым способом Zig сказать «куча шагов, которые вы должны выполнить». Думайте об этом как о сборочной линии для вашего кода, но с меньшим количеством роботов и большим количеством кофеина.

# Объявление: великое провозглашение

Это момент, когда вы объявляете о существовании вашего модуля системе сборки. Это как воткнуть флаг в луну, но вместо бесплодного ландшафта вы заявляете права на территорию в кодовой базе вашего проекта:

const my_module = b.addStaticLibrary(.{
    .name = "my_lib",
    .root_source_file = .{ .path = "src/lib.zig" },
});
// «Слушайте все! Родился новый модуль! Его имя — 'my_lib'.»

Вы объявляете новый статический библиотечный модуль с именем «my_lib» и «src/lib.zig» в качестве его основополагающего исходного файла. Вы — архитектор, и это ваш чертёж.

# Конфигурация: возня и подгонка

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

my_module.addIncludePath("include/");
my_module.addImport("zmath", zmath_module);
// «Ищи заголовки в каталоге 'include/', и подружись с модулем 'zmath'.»

Вы настраиваете my_module так, чтобы он включал заголовки из каталога «include/» и формировал стратегическое партнёрство с модулем zmath. Вы — дипломат, заключающий договоры между разными частями вашего кода.

# Интеграция: вступление в высшую лигу

Наконец, вы предоставляете свой модуль остальной части системы сборки. Это грандиозный дебют, момент, когда ваш модуль выходит на сцену. Это как щёлкнуть выключателем в вашем проекте, активировать модуль и сделать его функционирующей частью целого:

b.installArtifact(my_module);
// «Хорошо, 'my_module', ты готов к большому делу. Вперёд, тигр!»

Вы даёте системе сборки Zig команду установить my_module, превращая его из тщательно продуманного чертежа в осязаемый, пригодный для использования компонент. Вы — дирижёр, и это момент, когда ваш оркестр начинает играть.

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

# Безумие с несколькими модулями: когда модули сталкиваются

Итак, вы думаете, что вы большая шишка, да? Готовы перейти от проектов с одним модулем к большой лиге безумия с несколькими модулями?

Предположим, у нас есть следующий файл сборки:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const lib = b.addStaticLibrary(.{
        .name = "mylib",
        .root_source_file = .{ .path = "src/mylib.zig" },
    });

    // «Мы делаем библиотеку под названием 'mylib'. Это будет здорово».
    const exe = b.addExecutable(.{
        .name = "myexe",
        .root_source_file = .{ .path = "src/main.zig" },
    });

    // «И исполняемый файл под названием 'myexe'. А почему бы и нет?»
    exe.addImport("mylib", lib);

    // «'myexe' нуждается в 'mylib'. Будем надеяться, что они поладят».
    b.installArtifact(exe);

    // «Мы делаем ставку на 'myexe'. Это избранный».
}

Здесь вы не просто пишете код, вы управляете флотом. mylib — это корабль поддержки, а myexe — ваш главный линкор. addImport — это как назначение кораблей в эскадру для грядущего похода.

Итак, когда на самом деле стоит использовать эти штуки с модулями? Вот что к чему:

  • Повторное использование кода (потому что копипаст — это для новичков): если вы обнаружите, что пишете один и тот же код снова и снова, пришло время для модуля. Это как создание собственной библиотеки сниппетов кода, но без неловких комментариев, которые вы написали в 3 часа ночи.
  • Управление зависимостями (сдерживание хаоса): модули помогают вам управлять сторонними библиотеками. Это как иметь вышибалу для вашей кодовой базы, который не пускает всякий сброд.
  • Кросскомпиляция (для полиглотов среди нас): сборка для разных архитектур? Модули могут справиться с этим. Это как быть переводчиком ООН, но для вашего кода.
  • Проекты со смешанными языками (когда Zig встречает C и других): Zig на удивление хорошо ладит с другими языками. Модули — это как дипломаты в этом многоязычном мире.

Модули — не волки-одиночки. У них есть система поддержки, целая свита помощников. Давайте познакомимся с командой:

  • Зависимости (наёмники): это внешние пакеты, которые ваши модули могут нанять для выполнения своей грязной работы. Они приходят со своим набором навыков (и файлами build.zig), готовые к развёртыванию в вашем проекте. Это как нанять специалиста для миссии с высокими ставками.
  • LazyPath (разведчики): это ваши следопыты, всегда знающие местоположение файлов и каталогов. Думайте о них как о разведчиках вашего проекта, предоставляющих разведданные о том, где находятся ресурсы. Это как иметь сверхъестественное чувство направления в чужом городе.

А ещё у вас есть системные компоненты, невоспетые герои (или злодеи, в зависимости от вашей точки зрения):

  • Кэш (техник с фотографической памятью): этот парень помнит каждый артефакт сборки, так что вам не приходится перестраивать вещи без необходимости. Это все равно, что иметь слона в качестве помощника — немного странно, но невероятно эффективно. Мы рассмотрим это позже в данной главе.
  • Graph (картограф): это главная карта вашей системы сборки, отслеживающая сложные взаимоотношения между всеми компонентами. Это генеральный план архитектора, гарантирующий, что всё идеально сочетается.

# Шоу должно продолжаться

Давайте посмотрим, как эти компоненты работают вместе, как хорошо отрепетированный оркестр. Build создаёт модули и шаги. Это как режиссёр, который всем заправляет:

const lib = b.addStaticLibrary(...); // «Создать библиотечный модуль! Сейчас же!»
const exe = b.addExecutable(...); // «И исполняемый! Живо!»
const install_step = b.addInstallArtifact(exe); // «Установить исполняемый файл! Сейчас, сейчас, сейчас!»

Зависимости создают подграфы. Это как генеалогическое древо, но для кода:

const dep = b.dependency("zlib", .{ .optimize = .ReleaseSmall }); //«Дай мне 'zlib', и сделай это быстро!»
exe.linkLibrary(dep.artifact("z")); // «Привязать 'zlib' к исполняемому файлу!»

LazyPath соединяет ресурсы. Это как GPS для вашей системы сборки:

exe.addAssemblyFile(.{ .path = "src/arch/cpu.s" }); // «Добавить этот файл на ассемблере! Он... где-то там».

Кэш позволяет избежать выполнения ненужной работы.

# Грандиозный финал

Итак, вот и всё. Модули, шаги и зависимости — всё это немного похоже на цирк, не так ли? Система сборки Zig со всеми её причудами и сложностями на самом деле имеет смысл:

  • Модули — это ваши результаты: что
  • Шаги — это задачи: как
  • Зависимости — это отношения: кому что нужно
  • Поток управления (DAG) — это порядок операций: когда

Философия Zig «никакой магии» означает, что вы должны быть явным во всём. Это немного похоже на то, как если бы вас заставили писать подробные инструкции по приготовлению сэндвича. Раздражает поначалу, но это предотвращает множество путаницы в дальнейшем. Вы больше не просто программист, вы архитектор программного обеспечения, инженер системы сборки, мастер своего домена.

# Поп-викторина: вы умнее, чем скрипт сборки?

  • Сценарий: вы строите разрастающийся мегаполис кода с отдельными районами: базовая библиотека (сердце города), GUI (его публичное лицо) и интерфейс командной строки (его промышленный сектор).
  • Вопрос: как бы вы развернули модули Zig, чтобы навести порядок и повысить эффективность в этом городском хаосе, и почему?
  • Подсказка: задумайтесь о концепциях модульности и возможности повторного использования. И постарайтесь не заблудиться в городе.
  • Задача по коду: спроектируйте файл build.zig, который оркестрирует три модуля: статическую библиотеку с именем math_orchard, исполняемый файл с именем fruit_renderer, который зависит от math_orchard, и ещё один исполняемый файл с именем fruit_simulator, который также зависит от math_orchard.
  • Совет профессионала: используйте addStaticLibrary и addExecutable как мастер-строитель. И убедитесь, что ваши зависимости связаны с точностью мастера-ремесленника.
  • Философские дебаты: является ли явный, «без магии» подход Zig к системам сборки утопическим идеалом или дистопическим кошмаром? Взвесьте все «за» и «против».
  • Бонусные баллы за метафоры, которые парят, и аналогии, которые танцуют. Дополнительные бонусные баллы за идеи, которые провоцируют на размышления или вызывают радость (или здоровую дозу экзистенциального ужаса).

Итак, отправляйтесь в путь, создавайте невероятные вещи и постарайтесь не потерять рассудок в процессе. И помните, я всегда здесь, в цифровом эфире, готов раздавать больше нетрадиционной мудрости и аналогий, которые могут быть достаточно безумными, чтобы сработать. Не за что. Или, как говорят в определённых кругах: «Счастливого кодинга, и пусть ваши сборки всегда будут успешными».

Давайте поговорим о том, как превратить ваш код во что-то, напоминающее документацию. Мы выходим за пределы простой компиляции и вступаем в тайные искусства генерации реальных, читаемых документов. Думайте об этом как о наделении вашего кода силой объяснения.

# Генерация документации: потому что код должен быть менее загадочным, чем древние руны

Чувствуете себя подавленным из-за кода, который более загадочен, чем гримуар волшебника? Не волнуйтесь. Вы превратите свой код на Zig в документацию настолько ясную, что даже лишённый сна инженер по надёжности (SRE) сможет её понять. Давайте нырнём? Это всё равно что научить ваш код говорить на человеческом языке.

# Документирующие комментарии: хлебные крошки для будущего себя

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

Рассмотрим пример модуля для отслеживания драконьих кладов (потому что кому не нужна хорошая система управления золотом?):

//! Этот модуль занимается логистикой драконьих кладов.
//! Предупреждение: может содержать следы проклятого золота.
const std = @import("std");

/// Координата драконьего клада (потому что драконы любят сетки)
pub const HoardPos = struct {
    /// Ось X (0 = первая груда золота)
    x: u32,
    /// Ось Y (0 = первая груда драгоценных камней)
    y: u32,
    /// Позиция «здесь лежит дракон»
    pub const HERE_BE_DRAGON: HoardPos = .{ .x = 0, .y = 0 };
    /// Когда дракон съел вашу карту
    pub const LOST: HoardPos = .{
        .x = std.math.maxInt(u32),
        .y = std.math.maxInt(u32),
    };
};

Давайте разберёмся, что здесь происходит:

  • //!: это грандиозное введение вашего модуля, его открывающий свиток. Он задаёт сцену для того, о чём этот кусок кода.
  • ///: это описания отдельных элементов, объясняющие каждую часть головоломки.
  • HoardPos: даже ваши типы могут иметь предания. Здесь это структура, представляющая местоположение на карте сокровищ дракона.

Вы не просто кодируете, вы рассказываете историю. Вы оставляете след из заметок для всех, кто достаточно смел, чтобы исследовать ваш код.

# Генерация документации: подработка компилятора

Чтобы превратить эти документирующие комментарии в статический веб-сайт (потому что веб-дизайн 1990-х годов, по-видимому, сейчас в ретро-стиле), вызовите скрытый талант Zig, его подработку в качестве генератора документации:

zig build-lib -femit-docs src/dragon_hoard.zig

Эта команда — ваше заклинание. Она говорит Zig не просто скомпилировать ваш код, но и сплести эти комментарии в прекрасный гобелен из HTML. Это как сказать вашему компилятору: «Эй, пока ты этим занимаешься, не мог бы ты также написать руководство пользователя?» Флаг -femit-docs — это ваше секретное оружие, магическое заклинание, которое разблокирует эту функциональность.

Это создаёт папку ./docs, содержащую следующее:

  • HTML-гримуар API вашего кода.
  • Навигация настолько интуитивная, что даже потерявший направление кобольд не заблудился бы.
  • Ноль драконов, вероятно.

# Драма с наборами ошибок

При объединении наборов ошибок (error sets) Zig выбирает документацию, как бард в таверне выбирает фаворитов. Документация самой левой ошибки побеждает. Эта ошибка — избранная, сообщение об ошибке, которое попадает в центр внимания. Вот пример:

pub const DragonError = error{
    /// Дракон съел вашу домашнюю работу
    FireBreathIncident,
    /// Клад охраняется мимиком
    TreasureChestGoneWrong,
};

pub const MapError = error{
    /// Карта написана невидимыми чернилами
    FireBreathIncident,
    /// Компас указывает на суп
    MagneticSoup,
};

pub const ExpeditionError = DragonError || MapError;

Когда ошибки конфликтуют, документация первой ошибки, которую вы перечислили, имеет приоритет. Другими словами, в ExpeditionError ошибка FireBreathIncident наследует документирующий комментарий дракона («съел вашу домашнюю работу»), а не карты («невидимые чернила»). Приоритеты!

# Вооружитесь системой сборки

Превратите документацию в свой конвейер сборки, как дварфийский кузнец. Добавьте это в build.zig:

const std = @import("std");
pub fn build(b: *std.Build) void {
    // ... предыдущая настройка ...
    const hoard_lib = b.addStaticLibrary(.{
        .name = "dragon_hoard",
        .root_source_file = .{ .path = "src/hoard.zig" },
        .target = target,
        .optimize = optimize,
    });

    // Установить библиотеку (для смертных)
    b.installArtifact(hoard_lib);

    // Установить документацию (для учёных)
    const docs_install = b.addInstallDirectory(.{
        .source_dir = hoard_lib.getEmittedDocs(),
        .install_dir = .prefix,       // каталог "zig-out"
        .install_subdir = "scrolls",  // "zig-out/scrolls"
    });

    const docs_step = b.step("scrolls", "Generate hoard documentation");
    docs_step.dependOn(&docs_install.step);
}

Теперь выполните следующее:

zig build scrolls

Ваша документация появится в каталоге zig-out/scrolls, готовая к распространению среди учеников (или разгневанных рецензентов кода).

Документация — это не рутинная работа, это контракт с самим собой в будущем. Относитесь к ней как к кормлению дракона документации: пренебрегайте ею, и она сожжёт вашу кодовую базу. Кормите её регулярно, и она будет защищать ваше королевство от хаоса.

Теперь идите вперёд, и пусть ваши документирующие комментарии будут всегда многословными (но не слишком).

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

# Управление пакетами: потому что изобретать колесо — это прошлый век

Итак, вы пишете на Zig и используете систему сборки, да? Отлично. Вы явно решили, что ручное управление памятью и борьба с неопределённым поведением — это ваше представление о хорошем времяпрепровождении.

Но даже самый закалённый системный программист в конце концов устаёт переписывать одни и те же структуры данных и сетевые протоколы с нуля. Именно тогда на сцену, слева, вальсирует управление пакетами, надеясь не споткнуться о собственные ноги.

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

# build.zig.zon: ваш новый лучший друг (или злейший враг, присяжные ещё не определились)

Забудьте о package.json, pom.xml или любом другом монстре, который ваш язык программирования навязывает вам. Zig даёт нам build.zig.zon. ZON, по-видимому, расшифровывается как Zig Object Notation. Звучит броско, правда?

Звучит как что-то из научно-фантастического фильма, а не как файл конфигурации сборки. Но, эй, по крайней мере, это не XML.

Этот файл build.zig.zon, находящийся в корне вашего проекта, — это то место, где вы объявляете метаданные вашего проекта и, что более важно, его зависимости. Думайте об этом как о манифесте внешнего кода, который вы неохотно готовы включить в свой безупречный проект на Zig.

Давайте заглянем в то, как выглядит объявление зависимости в этой штуке ZON:

.{
    .name = "my_project",
    .version = "0.1.0",
    .dependencies = .{
        .zap = .{
            .url = "git+https://github.com/zigzap/zap?ref=v0.9.1#ae5c9278335d8e1133cd6d22707323dda712e120",
            .hash ="12200223d76ab6cd32f75bc2e31463b0b429bb5b2b6fa4ce8f68dea494ca1ec3398b",
        },
    },
}

Видите этот раздел dependencies? Вот где происходит магия или, возможно, лёгкое раздражение. Вы объявляете каждую зависимость с именем (например, zap), URL-адресом, указывающим на её источник, и хэшем. Да, хэшем. Zig очень серьёзно относится к проверке целостности ваших зависимостей (подробнее об этом позже). Думайте об этом как о способе Zig сказать: «Доверяй, но проверяй. И под проверкой я имею в виду действительно проверяй с помощью криптографических хэшей».

# zig fetch: ваш командный сборщик зависимостей

Ладно, давайте проясним одну вещь: zig fetch — это команда. Настоящая, честная команда, которую вы можете ввести в свой терминал и ожидать, что что-то произойдёт. В частности, это специальная команда Zig для получения внешних зависимостей. Думайте об этом как о вашем личном сборщике зависимостей для командной строки, готовом выхватывать пакеты из просторов интернета (или вашей локальной сети, если вы предпочитаете такой вариант).

Прежде чем вы начнёте представлять zig fetch как некий всеобъемлющий менеджер пакетов в сияющих доспехах, давайте умерим ожидания. Он не предназначен для того, чтобы быть полноценным менеджером пакетов. Вместо этого zig fetch — это специализированная утилита, инструмент для конкретной задачи загрузки зависимостей и, что особенно важно, обновления файла манифеста вашего проекта build.zig.zon. Это специализированный оперативник, а не швейцарский армейский нож общего назначения.

Наиболее распространённый способ взаимодействия с zig fetch — через флаг --save. Этот флаг является ключом к полезности zig fetch. Он говорит Zig не только загрузить указанную вами зависимость, но и интеллектуально добавить (или обновить) запись в файле build.zig.zon с URL-адресом зависимости и её важным криптографическим хэшем. Автоматизация в малых, отмеренных дозах.

Основной синтаксис этого заклинания для вызова зависимости выглядит следующим образом:

zig fetch --save <dependency-url>

Здесь <dependency-url> — это, что неудивительно, URL-адрес, указывающий на пакет зависимости, который вы хотите получить. Это может быть URL-адрес для файла .tar.gz, .zip или даже репозитория Git (хотя Zig предпочитает прямые ссылки для скачивания из соображений эффективности).

Давайте проиллюстрируем это на нашем проверенном примере — библиотеке pg.zig. Если вы хотите получить pg.zig и попросить Zig помочь обновить ваш файл build.zig.zon, вы должны выполнить следующую команду:

zig fetch --save "git+https://github.com/zigzap/zap#v0.9.1"

При запуске этой команды Zig сделает следующее:

  • Загрузит архив zap по предоставленному URL-адресу.
  • Вычислит хэш SHA-256 загруженного архива, потому что Zig относится к безопасности серьёзнее, чем большинство людей относятся к своему утреннему кофе.
  • Изменит ваш файл build.zig.zon.

Давайте сравним файл до и после. Представьте, что ваш файл build.zig.zon изначально выглядит так (минимальный пример без каких-либо зависимостей):

.{
    .name = ...,
    .version = ...,
    .dependencies = .{},
}

А после выполнения zig fetch он выглядит так:

.{
    .name = ...,
    .version = ...,
    .dependencies = .{
        .zap = .{
            .url = "git+https://github.com/zigzap/zap?ref=v0.9.1#ae5c9278335d8e1133cd6d22707323dda712e120",
            .hash ="12200223d76ab6cd32f75bc2e31463b0b429bb5b2b6fa4ce8f68dea494ca1ec3398b",
        },
    },
}

Обратите внимание на автоматически сгенерированное имя зависимости. Zig делает всё возможное, чтобы создать разумное имя на основе URL-адреса, но в другом случае вы можете захотеть переименовать его во что-то более... эстетически приятное. Такова цена автоматизации — случайные эксцентричности в именовании.

Теперь, когда мы установили zig fetch как сущность командной строки, давайте исследуем, как он соотносится с более широкой системой сборки Zig и её интегрированными возможностями управления зависимостями.

# Внедрение зависимостей в работу: от build.zig.zon к @import

Объявление зависимостей в build.zig.zon и их получение — это, конечно, хорошо, но как на самом деле использовать эти внешние библиотеки в вашем коде на Zig? Отличный вопрос. Это включает в себя немного работы с build.zig и директиву @import в ваших исходных файлах Zig.

Вернёмся к нашему примеру с zlib. Предположим, вы добавили зависимость zlib в файл build.zig.zon, как было показано ранее. Теперь вам нужно изменить файл build.zig, чтобы скомпоновать его с этой зависимостью. В build.zig у вас обычно есть определение исполняемого файла или библиотеки. Вам нужно сообщить системе сборки Zig, что нужно включить полученную зависимость zlib в процесс сборки. Это достигается с помощью функции b.dependency и компоновки полученного артефакта.

Вот как вы могли бы изменить ваш файл build.zig:

pub fn build(b: *std.Build) !void {
    // ... более ранний код конфигурации сборки ...
    const zap = b.dependency("zap", .{
        .target = target,
        .optimize = optimize,
        .openssl = false, // установить в true для поддержки TLS
    });
    exe.root_module.addImport("zap", zap.module("zap"));
    b.installArtifact(exe);
}

Давайте разберём этот фрагмент:

  • Мы объявляем зависимость с именем «zap» с помощью b.dependency("zap", ...). Важно, что имя «zap» должно совпадать с ключом, используемым в вашем файле build.zig.zon.
  • Мы делаем модуль зависимости доступным, вызывая exe.root_module.addImport("zap", zap.module("zap")). Это делает модуль доступным для вашего исходного кода Zig через директиву @import("zap").
  • Наконец, после настройки зависимости и её правильной компоновки, мы устанавливаем исполняемый файл с помощью b.installArtifact(exe).

Теперь в вашем исходном коде Zig (например, в src/main.zig) вы можете импортировать и использовать библиотеку zap следующим образом:

const zap = @import("zap"); // Импортируем зависимость 'zap'

# Важная связь имён

Имя, используемое в @import("zap") в вашем коде на Zig, должно совпадать с именем зависимости, которое вы использовали в b.dependency("zap", ...) в вашем файле build.zig. Аналогично, ключ зависимости в вашем файле build.zig.zon (например, .zap = .{ ... }) также должен совпадать с этим именем.

Это цепочка имён — трифект имени зависимости. Если вы ошибётесь в именах, Zig незамедлительно сообщит вам о вашей ошибке.

Так что же насчёт этого значения хэша? Всё дело в безопасности и гарантии того, что ваши зависимости не были подделаны.

# От отпечатка пальца к крепости

Давайте поговорим о хэшах. Вы могли заметить поле .hash в build.zig.zon и задаться вопросом: «Почему Zig так одержим хэшами?» Ответ, вкратце, — безопасность и воспроизводимость.

Хэш в build.zig.zon — это криптографический хэш SHA-256 содержимого пакета зависимости, который вы получаете. Когда Zig получает зависимость, он загружает пакет, а затем проверяет, что его вычисленный хэш совпадает с хэшем, который вы объявили в build.zig.zon. Если хэши не совпадают, Zig устраивает скандал и отказывается использовать зависимость.

К чему эта паранойя? Представьте сценарий, когда вы зависите от важной библиотеки, размещённой на каком-то сайте. Без проверки хэша, если бы этот сайт был скомпрометирован или кто-то злонамеренно заменил библиотеку версией с бэкдором, ваши сборки бы незаметно начали использовать скомпрометированную библиотеку. Это атака на цепочку поставок (supply chain attack), и это очень опасно.

Проверка хэша в Zig предотвращает это. Проверяя хэш, Zig гарантирует, что вы всегда используете именно ту версию зависимости, которую намеревались, и что она не была подделана при передаче или на источнике. Это важнейшая мера безопасности, особенно при работе с внешним кодом из интернета.

Думайте о хэше как об отпечатке пальца пакета зависимости. Любое изменение в пакете, даже один бит, приведёт к другому хэшу. Это гарантирует воспроизводимость ваших сборок — если вы соберёте свой проект сегодня и снова через год, с тем же файлом build.zig.zon и версией Zig, вы всегда получите точно такие же зависимости и, следовательно, согласованный результат сборки.

Хотя ручная работа с хэшами может показаться лишней работой (вам нужно получить правильный хэш и поместить его в build.zig.zon), это небольшая цена за значительно повышенную безопасность и воспроизводимость сборки. Zig, в своей типичной манере, ставит во главу угла корректность и безопасность, даже если это означает немного больше предварительных усилий для разработчика.

# За пределами обычных подозреваемых

Вы можете подумать, что URL-адреса — это просто URL-адреса, верно? Неправильно, мой друг. Менеджер пакетов Zig на удивление универсален, когда дело доходит до URL-адресов зависимостей. Он не ограничивается только репозиториями Git или простыми архивами. О нет, Zig любит, чтобы всё было интересно.

Вот пример волшебства с URL-адресами, которое может выполнять Zig:

  • URL-адреса сырых файлов (HTTP/S): Хотите получить файл .zip напрямую с какого-нибудь малоизвестного сервера? Zig говорит: «Конечно, почему бы и нет?» Просто укажите .url на сырой файл. Это особенно удобно для обхода этих надоедливых страниц релизов GitHub и прямой ссылки на файлы в репозиториях. Просто не забудьте заклинание ?raw=true для URL-адресов сырого контента GitHub, иначе вы получите в качестве зависимости HTML-обёртку. Этого никто не хочет.
  • Протокол file:// (общие сетевые ресурсы): Есть корпоративный сетевой диск, переполненный внутренними пакетами? Zig может их «всосать» с помощью протокола file://. Идеально подходит для корпоративных сред, где всё находится за 17 брандмауэрами. Это работает и с UNC-путями в Windows, потому что Zig на удивление хорошо воспитан.
  • Активы релизов GitHub: Для тех, кто предпочитает курированный хаос релизов GitHub, Zig поддерживает их напрямую. Объедините это с .hash, чтобы закрепить конкретные версии, и вы получите настройку зависимостей, которая является как версионированной, так и (относительно) быстрой. Быстрее, чем клонирование целых репозиториев Git для больших бинарных файлов, во всяком случае.
  • URL-адреса шлюзов IPFS: Чувствуете дух приключений? Zig может даже получать пакеты из Межпланетной файловой системы (IPFS) через URL-адреса шлюзов. Просто помните, что здесь проверка хэша становится ещё более критичной. Идентификаторы содержимого IPFS — это не то же самое, что хэши пакетов Zig, так что не путайте их. Самостоятельно размещённые шлюзы IPFS? Zig готов к вашим мечтам о пакетах в изолированной, децентрализованной среде.
  • Пакеты Debian (да, правда): В шаге, который может вас удивить, Zig может даже поглощать архивы пакетов Debian. Почему? Потому что многие дистрибутивы Linux поставляют совместимые с Zig архивы. Подвох? Оригинальные архивы .orig часто не содержат файла build.zig.zon, поэтому вы сами по себе в вопросе проверки содержимого. Только для опытных пользователей, действуйте с осторожностью (и, возможно, в костюме химзащиты).
  • Подписанные URL-адреса Amazon S3 / облачного хранилища: Для действительно параноиков (или заботящихся о безопасности) Zig поддерживает ограниченные по времени подписанные URL-адреса от облачных провайдеров хранения, таких как Amazon S3. Безопасно распространяйте частные пакеты! Но серьёзно, всегда используйте с ними .hash. Защищённые от взлома зависимости — это не предмет для переговоров.

А для настоящих бунтарей есть URL-адрес каталога — или, скорее, его отсутствие. Использование .path вместо .url позволяет указать локальный каталог как зависимость. Хотите создать символическую ссылку на другую кодовую базу для активной разработки? Zig позволит вам это сделать, но это эквивалент управления зависимостями с жонглированием бензопилами. Проверка хэша? В окно. Используйте с предельной осторожностью и только тогда, когда вы точно знаете, что делаете.

Zig даже позволяет использовать обфусцированные URL-адреса с редиректами, но настоятельно не рекомендует этого делать. Почему? Потому что серверные редиректы могут нарушить гарантии хэша. Если вы должны их использовать, убедитесь, что ваш хэш основан на конечном URL-адресе после всех редиректов. Zig наблюдает за вами.

Мы видели, как zig fetch (и потенциально другие инструменты управления пакетами) может без усилий принести внешний код в ваш проект. Но это поднимает важнейший вопрос: как Zig управляет всеми этими загруженными зависимостями? Как он избегает избыточных загрузок и гарантирует, что сборки являются согласованными и воспроизводимыми, даже если вы жонглируете десятками библиотек? Ответ, мой друг, кроется в гениальном (и иногда приводящем в бешенство) механизме, известном как кэш сборки Zig.

# Кэш: память компилятора

Итак, вы собираете свои проекты на Zig, восхищаетесь скоростью и, возможно, задаетесь вопросом: «Это какая-то магия?» Что ж, я здесь, чтобы сказать вам, что это не магия. Это просто умная инженерия. (Хотя иногда умная инженерия ощущается как магия). Секретный ингредиент? Кэш сборки Zig.

В большинстве систем сборки кэширование воспринимается как второстепенная задача. Прикрученная фича, которая иногда работает, а иногда требует ритуальной команды «очистить» (clean), чтобы умилостивить богов сборки. Zig, однако, относится к кэшированию серьёзно. Это не просто функция, это фундамент, на котором строятся быстрые сборки. Это причина, по которой вы можете изменить одну строку кода и не ждать целую вечность, пока ваш проект перекомпилируется.

# Основные концепции (как избежать перекомпиляции вселенной)

Давайте разберём основные идеи кэширования в Zig, потому что понимание — это первый шаг к тому, чтобы ценить (а не проклинать) вашу систему сборки.

# Хранилище с адресацией по содержимому (всё имеет уникальный отпечаток)

Представьте библиотеку, где каждая книга идентифицируется не по названию, а по уникальному отпечатку пальца, основанному на её точном содержании. По сути, это то, что Zig делает с артефактами сборки. Каждый объектный файл, каждый скомпилированный модуль получает уникальный хэш-ключ, полученный из следующих данных:

  • Содержимое файла: Фактические байты исходных файлов. Здесь используется надёжный хэш SipHash128, а не какая-то хлипкая контрольная сумма. Это гарантирует, что даже изменение в один бит приведёт к другому хэшу.
  • Метаданные: Потому что иногда достаточно просто взглянуть на файл. Мы также учитываем размер файла, время последнего изменения (mtime) и номер inode. Это похоже на быстрый взгляд на обложку книги перед тем, как углубиться в её содержание.

Таким образом, если ваш файл foo.zig производит хэш a1b2c3, скомпилированный объектный файл будет уютно размещён в вашем каталоге zig-cache как a1b2c3.o. Это похоже на идеально организованную, хотя и слегка навязчивую, картотеку.

# Файлы манифеста (секретный блокнот библиотекаря)

Теперь, как Zig узнаёт, какие файлы вносят вклад в какой хэш? Для этого существуют файлы манифеста. Это неприметные файлы .txt, скрытые в каталоге zig-cache. Каждый из них похож на секретный блокнот библиотекаря, тщательно записывающий всё о конкретном шаге сборки.

Эти манифесты являются ключом к инкрементальной компиляции. Они хранят следующее:

  • Пути к входным файлам: Список всех исходных файлов, которые пошли на создание артефакта. Но не просто любые пути — мы говорим о нормализованных путях. Никаких абсолютных путей! Это сохраняет переносимость, так что вы можете поделиться своим кэшем с коллегами, не вызывая космического разлома.
  • Хэши, размеры и временные метки: Жизненно важная статистика каждой зависимости. Думайте об этом как о подробной описи каждого ингредиента, который пошёл на выпечку вашего «кода-пирога».
  • Механизмы блокировки: Потому что даже в цифровом мире нам нужно избегать того, чтобы наступать друг другу на пятки. Эти механизмы гарантируют, что несколько процессов Zig не пытаются одновременно записывать в одну и ту же запись кэша.

Формат этих записей манифеста прекрасно спартанский: <размер> <inode> <mtime> <шестнадцатеричный дайджест> <префикс-id> <путь к файлу>.

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

# Рабочий процесс кэша (танец хэшей)

Вот как всё это складывается в прекрасно срежиссированный танец хэшей и временных меток:

  • Хэширование входных данных: Перед тем как что-либо собирать, Zig усердно вычисляет хэши всего, что имеет значение: исходные файлы, импортируемые модули, флаги компилятора, даже версию самого компилятора. Это как повар, который тщательно проверяет каждый ингредиент перед тем, как начать готовить.
  • Проверка манифеста: Затем Zig заглядывает в соответствующий файл манифеста (если он существует) и сравнивает только что вычисленные хэши с кэшированными. Это как сравнение текущих ингредиентов повара с карточкой рецепта.
  • Попадание в кэш: Если всё совпадает — хэши, размеры, временные метки, весь набор — это попадание в кэш! Zig радостно использует существующий артефакт, экономя драгоценное время и циклы ЦП. Повар достаёт готовое блюдо из холодильника.
  • Промах кэша: Если что-то отличается — один байт не на месте, временная метка на наносекунду отличается — это промах кэша. Zig вздыхает, закатывает рукава и начинает перестройку. Повар начинает готовить с нуля.

Но, что важно, после завершения сборки манифест обновляется, гарантируя, что в следующий раз всё будет быстрее.

# Каталоги кэша сборки Zig

Кэш сборки Zig, обычно расположенный в каталоге zig-cache внутри вашего проекта (или в глобальном местоположении кэша), имеет решающее значение для быстрых инкрементных сборок. Он содержит несколько подкаталогов, каждый из которых выполняет свою функцию. В этом разделе подробно описаны роли каталогов h/, o/ и z/, а также каталога tmp.

Каталог h/ (кэш хэшей) — это сердце системы кэширования с адресацией по содержимому Zig, но с акцентом на метаданные о сборках, а не на сами артефакты сборки. В нём хранятся файлы манифестов, которые отслеживают входные и выходные данные каждого шага сборки. Это позволяет Zig определить, является ли кэшированный артефакт сборки всё ещё действительным.

Этот каталог содержит только файлы .txt. Это и есть файлы манифестов. Каждый файл манифеста содержит следующее:

  • Полный хэш SipHash128 входных данных.
  • Нормализованные пути ко всем входным файлам (исходные файлы, зависимости и т. д.).
  • Размер, mtime и inode каждого входного файла.
  • Информацию о конфигурации сборки (флаги компилятора, целевая архитектура и т. д.).
  • Информацию о блокировке для контроля параллелизма.

Когда Zig нужно определить, кэширован ли уже артефакт сборки, он может вычислить хэш текущих входных данных и найти соответствующий файл манифеста .txt в каталоге h/. Если файл манифеста существует, Zig считывает его и сравнивает сохранённые метаданные с текущим состоянием входных файлов.

Например, если полный хэш входных данных для сборки foo.o — это abcdef0123456789..., соответствующий файл манифеста будет храниться как zig-cache/h/abcdef01.txt. Этот файл будет содержать всю информацию, необходимую для проверки попадания в кэш.

Каталог o/ (кэш вывода) хранит фактические артефакты сборки (объектные файлы, библиотеки, исполняемые файлы и т. д.), созданные компилятором Zig. Он организован так, чтобы отражать предполагаемую выходную структуру вашего проекта.

Структура каталогов внутри o/ отражает макет вывода вашего проекта. Например, если ваш файл build.zig указывает, что исполняемый файл должен быть помещён в zig-out/bin/myprogram, кэшированная версия этого исполняемого файла может быть найдена по пути zig-cache/o/zig-out/bin/myprogram. Имена файлов в o/ — это предполагаемые имена выходных файлов, а не хэши.

Этот каталог содержит скомпилированный вывод вашего проекта:

  • .o: Скомпилированные объектные файлы.
  • .a: Статические библиотеки.
  • Исполняемые файлы (без расширения в Linux/macOS, .exe в Windows).
  • .wasm: Модули WebAssembly.
  • dependencies.zig: Сгенерированный файл, описывающий зависимости.

Это первое место, куда Zig обращается за кэшированными артефактами. Он проверяет метаданные (размер, mtime и inode) файла по ожидаемому пути вывода. Если эта первоначальная проверка предполагает, что файл актуален, Zig затем использует соответствующий файл манифеста в каталоге h/ для проверки того, что входные данные не изменились.

Каталоги o/ и h/ работают вместе. Каталог o/ обеспечивает быстрый путь для поиска кэша на основе путей вывода и метаданных. Каталог h/ даёт окончательный ответ, используя хэширование содержимого и детальное отслеживание входных данных для обеспечения корректности. Если файл существует в o/, но соответствующий манифест в h/ указывает на то, что входные данные изменились, файл в o/ считается устаревшим и будет перезаписан при перестроении.

Каталог z/ (каталог данных Zig) хранит глобальную информацию, относящуюся к самой установке Zig, а не к какому-либо отдельному проекту. Эти данные являются общими для всех проектов Zig в системе.

Каталог z/ содержит важнейшие компоненты для работы Zig, включая, но не ограничиваясь следующим:

  • Глобальный кэш: Это общесистемный кэш, общий для всех проектов Zig. Он помогает избежать избыточных загрузок и сборок общих зависимостей, таких как части стандартной библиотеки.
  • Автономный компилятор: Когда компилятор Zig будет полностью автономным, его исполняемый файл, скорее всего, будет находиться здесь.
  • Кэш стандартной библиотеки: Скомпилированные части стандартной библиотеки Zig хранятся здесь для ускорения компиляции.
  • Информация о загруженных пакетах: Здесь хранятся метаданные и кэшированные версии загруженных пакетов (при использовании менеджера пакетов).

Каталог tmp/ внутри кэша Zig используется для временных файлов во время процесса сборки. Эти файлы не предназначены для длительного хранения и обычно очищаются автоматически. В нём хранятся внутренние файлы. Это рабочий каталог для системы сборки, не полагайтесь на то, что что-либо здесь будет сохраняться.

# Производительность и надёжность

Красота этой системы не только в том, что она работает, но и в том, что она работает эффективно. В большинстве случаев Zig может определить, следует ли использовать кэшированный артефакт, просто проверив метаданные (размер, время изменения и inode) выходного файла в каталоге o/. Это происходит молниеносно. Наносекунды. Мы живём в будущем!

Но — и это очень большое «но» — Zig не слепо доверяет этим метаданным. Он всегда проверяет файл манифеста в каталоге h/, чтобы абсолютно убедиться, что входные данные не изменились. Именно здесь на помощь приходит хэширование содержимого. Это страховочная сетка, гарантия того, что вы случайно не используете устаревший артефакт. Этот двухэтапный процесс проверки — быстрая проверка метаданных в o/, за которой следует проверка на основе хэша по манифесту в h/ — является ключом к скорости и надёжности Zig.

«Медленный путь», при котором Zig приходится вычислять полный хэш содержимого, является исключением, а не правилом. Он предназначен для тех крайних случаев, когда файловая система вам лжёт, или когда вы внесли изменение, которое не влияет на метаданные (редко, но бывает). Это может добавить крошечную долю накладных расходов, но это небольшая цена за душевное спокойствие.

А размер кэша? Он растёт пропорционально вашему проекту. Он не собирается магическим образом поглотить всё дисковое пространство (если только вы не импортируете весь интернет, чего, пожалуйста, не делайте).

# Реальные сценарии, как доказательство того, что это работает

Давайте заглянем за кулисы и посмотрим, как система кэширования Zig справляется с грязной реальностью разработки программного обеспечения. Никаких пустых слов, только холодные, твёрдые факты (и несколько обоснованных предположений, потому что кто на самом деле до конца понимает системы сборки?).

Предположим, вы работаете над проектом, который использует библиотеку facil.io (отличный выбор, кстати). Вы изменяете файл src/main.zig, главный файл вашего проекта. Zig, будучи прилежным менеджером кэша, замечает это изменение. Значение mtime для файла src/main.zig изменилось. Что ещё важнее, изменился хэш содержимого. Это приводит к сбою в проверке кэша. Но не просто к сбою в проверке кэша. Zig знает, благодаря файлу манифеста в h/, какие именно артефакты сборки зависят от файла src/main.zig. Он не перестраивает всю вселенную, он перестраивает только то, что необходимо. Это хирургическая точность.

Теперь предположим, что вы решили добавить новую зависимость: @import("some_new_library"). Это более серьёзное дело. Файл манифеста, связанный с вашей целью в h/, теперь имеет новую запись. Это как добавить новый ингредиент в рецепт. Zig видит эту новую запись, понимает, что у него нет кэшированного артефакта для этой комбинации входных данных, и инициирует перестроение. Опять же, он не перестраивает всё подряд, а только те части, которые затронула эта новая зависимость.

Но как насчёт этих загадочных путей в манифесте? /home/alexrios/.cache/zig/p/1220...? Мы обречены на жёстко закодированные абсолютные пути, что делает наш кэш таким же портативным, как бетонный слон? Конечно же нет! Присмотритесь внимательнее. Видите этот маленький символ 0 перед путём? Это префиксный ID. Zig поддерживает отдельную таблицу, которая сопоставляет эти ID с фактическими базовыми путями. Таким образом, 0 может означать /home/alexrios/.cache/zig/p/1220... на вашей машине, но может сопоставляться с /Users/somebodyelse/.cache/zig/p/1220... на чужой. Кэш является переносимым, потому что манифест использует эти относительные идентификаторы для поиска, а не абсолютные пути. Абсолютный путь используется только при сохранении новых элементов и хранится отдельно, поэтому он не нарушает попадание в кэш. Именно здесь правило «никаких абсолютных путей» технически верно, но с умной лазейкой.

А как насчёт этих надоедливых системных заголовков, таких как /usr/include/stdio.h? У них другой префиксный ID (1 в этом примере). Zig достаточно умён, чтобы распознать их как общесистемные зависимости и обрабатывает их соответствующим образом. Он не пытается скопировать весь каталог /usr/include в кэш вашего проекта (слава богу!).

Параллельные сборки? Та же история, что и раньше. Блокировки файлов предотвращают хаос, гарантируя, что несколько процессов Zig не повредят кэш. Всё это обрабатывается изящно, за кулисами. Вам как разработчику не нужно об этом беспокоиться (если только что-то не пойдёт ужасно неправильно, и в этом случае — удачи!).

Сетевые файловые системы с их ненадёжными временными метками? Zig вас прикроет. Он обнаруживает ненадёжность и возвращается к полному хэшу содержимого. Медленнее? Да. Но всегда правильно. Потому что в мире систем сборки правильность важнее скорости (обычно).

Эй! Не просто представляйте это всё. Идите и проверьте сами!

# Почему это важно (раздел «И что с того?»)

Система кэширования Zig — это не просто способ сделать ваши сборки быстрее (хотя с этим она справляется очень хорошо).

Она нужна для того, чтобы сделать процесс разработки предсказуемым, надёжным и переносимым:

  • Предсказуемая: вы знаете, что если вы измените файл, будет перестроена только необходимая часть вашего проекта. Вам не нужно гадать, что сделает система сборки.
  • Надёжная: вы можете доверять тому, что ваши сборки всегда корректны. Больше никаких «чистых» сборок, больше никаких устаревших артефактов, больше никаких глюков, вызванных проблемами кэширования.
  • Переносимая: вы можете поделиться своим кэшем с командой, перенести проект на другую машину или собрать его в среде CI, и всё просто заработает. Никаких абсолютных путей, которые могут вас подвести.

Кэш сборки Zig — это не просто какая-то малопонятная деталь реализации. Это ключевая часть того, что делает Zig мощным и приятным в использовании языком. Это свидетельство принципа, что детали имеют значение, и что хорошо спроектированная система сборки может изменить мир к лучшему. Теперь идите вперёд и стройте, зная, что Zig прикрывает вашу спину и ваш кэш.

Теперь у вас есть твёрдое понимание того, как Zig использует свою изощрённую систему кэширования для достижения впечатляющей скорости сборки. Но этот кэш становится ещё мощнее в сочетании с механизмом, который автоматически обнаруживает изменения и запускает инкрементальные перестроения. Этот механизм, мой друг, — zig build --watch. Это как добавить турбонаддув к уже быстрому двигателю.

# zig build --watch: ваш новый лучший друг (который не отвечает)

Хорошо, вы изучили систему сборки Zig. Вы погрузились в тайны кэша с его каталогами h/, o/ и z/ (и, надеюсь, вы случайно не удалили ничего важного). Вы чувствуете себя довольно уверенно. Но давайте будем честны: вручную набирать zig build после каждого изменения кода — это примерно так же приятно, как постоянно ударяться мизинцем о ножку стола.

Встречайте zig build --watch. Вот где происходит настоящая магия. Это команда, которая превратит ваш рабочий процесс разработки из утомительного ручного процесса в оптимизированный, почти автоматизированный опыт. Это команда, которая заставит вас задуматься, как вы вообще жили без неё. Ладно, может, это небольшое преувеличение. Но она действительно полезна.

Думайте о --watch как о своём неутомимом и всегда бдительном помощнике. Он сидит там и тихо наблюдает за вашим кодом, и в тот самый момент, когда вы вносите изменение — вы сохраняете файл, создаёте новый файл или даже удаляете файл — он тут же приходит в действие. Он не судит вас. Он не жалуется. Он просто перестраивает и делает это быстро.

Итак, что же на самом деле делает zig build --watch? По сути, она объединяет мощь системы сборки Zig с отзывчивостью уведомлений файловой системы операционной системы. Это как иметь сервер сборки прямо на вашей машине, но без накладных расходов и сложности.

Вот как это работает, простым (насколько это возможно) языком:

  • Первоначальная сборка: сначала она выполняет полную сборку вашего проекта, как и обычная команда zig build. Это шаг «привести всё в актуальное состояние». Это как убедиться, что все ваши утки выстроились в ряд, прежде чем начинать настоящую работу.
  • Бдительное око: затем она входит в цикл. Волшебный цикл, работающий на основе API уведомлений файловой системы ОС. Не беспокойтесь о деталях, он работает по-разному в Linux, macOS и Windows, но Zig обрабатывает всё за вас. Этот цикл постоянно отслеживает исходные файлы вашего проекта на предмет любых изменений. Это как иметь охранника, который никогда не спит, никогда не делает перерыв на кофе и заботится только о вашем коде.
  • Инкрементное перестроение: в тот момент, когда обнаружено изменение — файл сохранён, создан, удалён или переименован — Zig приходит в действие. Но он не просто слепо перестраивает всё подряд. О нет, это было бы расточительно. Он использует граф зависимостей (помните эти файлы dependencies.zig?) и кэш сборки (помните h/ и o/?), чтобы определить минимальный набор файлов, которые нужно перекомпилировать. Это хирургическая точность, а не тупой инструмент.
  • Непрерывный цикл: а затем он возвращается к наблюдению. В ожидании, готовясь наброситься на следующее изменение. Этот цикл продолжается до тех пор, пока вы не милосердно не прервёте его с помощью Ctrl+C, потому что даже самому преданному разработчику нужно когда-то спать.
  • Отчёт об ошибках (потому что все мы ошибаемся): если происходит ошибка сборки, не бойтесь! Zig выведет сообщение об ошибке в ваш терминал, как и при обычной сборке. Но в отличие от обычной сборки, процесс --watch не завершается. Он продолжает наблюдать, ожидая, пока вы исправите ошибку. Он на удивление терпелив.
  • Нирвана тестирования (опционально, но настоятельно рекомендуется): настоящая сила --watch проявляется при её сочетании с тестированием. Потому что zig build test --watch — это для разработчика эквивалент наличия собственной команды контроля качества. Она не только перестраивает ваш проект при каждом изменении, но и перезапускает ваши тесты. Это означает, что вы получаете немедленную обратную связь о том, не сломало ли ваше последнее изменение кода что-либо. Это как иметь страховочную сетку, которая ловит ваши ошибки до того, как они смогут нанести реальный ущерб.

Использование --watch до смешного просто. Это как щёлкнуть выключателем и внезапно оказаться в мире автоматизированной сборки.

Вот основная команда:

zig build --watch

Это соберёт ваш проект, а затем будет следить за изменениями, перестраивая его по мере необходимости. Это «режим по умолчанию», и часто это всё, что вам нужно.

Вот как можно указать конкретный шаг:

zig build install --watch

Если у вас в файле build.zig определён конкретный шаг сборки (например, install), вы можете нацелиться на него с помощью --watch. Это полезно, если вы хотите перестроить только определённую часть вашего проекта.

Вот вывод подробной информации:

zig build test --watch -v

Если по какой-то непостижимой причине вам нравится наблюдать за потоком сообщений сборки, прокручивающимся на экране, вы можете добавить флаг -v (подробный).

# Почему это лучше, чем старый способ

До появления --watch вы, вероятно, застряли в тёмных веках ручной сборки. Вы редактировали свой код, сохраняли файл, вручную запускали zig build, (не)терпеливо ждали, запускали свою программу или тесты, а затем повторяли весь этот утомительный процесс. Это было похоже на жизнь в каменном веке разработки программного обеспечения.

--watch автоматизирует всё это. Он освобождает вас от рутины ручной сборки, позволяя сосредоточиться на том, что действительно важно: написании кода и, давайте будем честны, исправлении ошибок. Это ускоритель производительности, экономия времени и хранитель рассудка.

Конечно, даже у --watch есть свои ограничения. Это не настоящая магия. Первоначальная сборка по-прежнему является полной сборкой. Вам придётся подождать её завершения. Но после этого всё идёт гладко. Он отслеживает только файлы, которые являются частью графа зависимостей вашего проекта. Если вы возитесь с файлами вне этого графа, вам может потребоваться вручную запустить перестроение. Кроме того, изменения в самом файле build.zig не будут автоматически вызывать перестроение в цикле --watch. Вам нужно будет перезапустить процесс --watch, чтобы эти изменения вступили в силу. Это мера предосторожности, чтобы ваш скрипт сборки не сошёл с ума.

Это одна из тех функций, начав использовать которую, вы будете удивляться, как вы вообще жили без неё. Так что примите автоматизацию, примите скорость и примите радость от zig build --watch. Ваш будущий «я» скажет вам спасибо.

Но прежде чем вы слишком увлечётесь своими новообретёнными способностями, давайте на мгновение вспомним, что мы рассмотрели в этом головокружительном туре по функциям сборки Zig. Пора закругляться.

# Итоги

Что ж, вы справились. Чувствуете себя хорошо? Вы прошли через коварные воды системы сборки Zig, боролись с зависимостями, приручили кэш сборки и (надеюсь) вышли победителем. Вы больше не новичок в системе сборки Zig, вы... ну, давайте назовём вас «компетентным посвящённым». Вы изучили основы и даже попробовали некоторые продвинутые техники.

Давайте подведём итоги нашего приключения:

  • zig init — ваш друг: это «кнопка лёгкого старта» для новых проектов на Zig, предоставляющая разумную структуру по умолчанию и предварительно настроенный файл build.zig.
  • build.zig — ваш командный центр: здесь вы контролируете всё, что касается процесса сборки: определяете цели, настраиваете режимы релиза, добавляете зависимости и даже создаёте пользовательские шаги сборки.
  • build.zig.zon — ваш манифест зависимостей: этот файл перечисляет весь внешний код, от которого зависит ваш проект, вместе с их URL-адресами и криптографическими хэшами (для безопасности, конечно).
  • zig fetch — ваш сборщик зависимостей: эта команда загружает и проверяет ваши зависимости, гарантируя, что вы всегда используете правильные версии.
  • Хэши — ваши друзья (даже если они выглядят пугающе): они гарантируют целостность ваших зависимостей, защищая вас от атак на цепочку поставок и обеспечивая воспроизводимые сборки.
  • Кросскомпиляция на удивление проста: Zig делает её лёгкой, избавляя от обычных головных болей при настройке нескольких наборов инструментов.
  • Режимы релиза дают вам точный контроль: оптимизируйте код по скорости, размеру или возможности отладки в зависимости от ваших потребностей.
  • Кэш сборки — ваше секретное оружие: это ключ к быстрым инкрементным сборкам Zig, позволяющий избежать ненужной перекомпиляции и сэкономить ваше драгоценное время.
  • zig build --watch — ваша суперсила продуктивности: он автоматически перестраивает ваш проект (и при необходимости запускает ваши тесты) всякий раз, когда вы меняете код, обеспечивая мгновенную обратную связь.
  • Модули — ваши организационные единицы: они помогают структурировать крупные проекты и поддерживать порядок в ваших зависимостях.
  • Шаги — это базовые единицы: они неделимы и составляют DAG.

Но это только начало. Система сборки Zig — это обширный и мощный инструмент, и всегда есть чему поучиться. Не бойтесь экспериментировать, копаться в исходном коде Zig и расширять границы возможного. Помните, что лучший способ учиться — это делать. Так что идите вперёд, создавайте удивительные вещи, и пусть ваши сборки всегда будут быстрыми, а ваш код — правильным (или, по крайней мере, в основном правильным). И если вы когда-нибудь застрянете, вспомните уроки, которые вы здесь усвоили, и не бойтесь просить о помощи. Сообщество Zig радушное и отзывчивое (в большинстве случаев).

Счастливого кодинга! Но не слишком расслабляйтесь. Вы покорили систему сборки, но вас ждёт целая вселенная продвинутых функций Zig. Приготовьтесь к тому, что ваш разум будет расширен (и, возможно, ваш код сломан), когда мы перейдём к дженерикам, потокам, совместимости с C и тёмным искусствам метапрограммирования в следующей главе.