Поддержка сборочных скриптов
Некоторые пакеты требуют компиляции стороннего кода написанного на других языках программирования, например СИ-библиотек. Другие пакеты предназначены для линковки с СИ-библиотеками. Они могут располагаться где-то в системе, или собираться из исходников. Третий вариант - это контейнеры, которые требуют дополнительной подготовки к сборке, например генерации кода перед сборкой.
Cargo не ставит целью заменить инструменты которые хорошо оптимизированы
для своих задач. Вместо этого, данные инструменты можно интегрировать в процесс
сборки посредством опции build
сборочного манифеста.
[package]
# ...
build = "build.rs"
Rust файл, указанный в опции build
(относительно корня пакета) будет
скомпилирован и выполнен перед тем, как что-либо еще будет компилироваться
в этом пакете. Это позволяет вашему rust-коду зависеть от других собранных
или сгенерированных артефактов. Опция build
не имеет значения по умолчанию,
и должна быть указана явно, если это требуется.
Причание. В rust 1.17 этот механизм был доработан следующим образом. Если в корневой директории контейнера есть файл build.rs, то считается, что опция build = "build.rs" указана. Отключить это вы можете задав build=false.
Примеры случаев, для которых подходит опция build
:
- Сборка статической С-библиотеки
- Поиск С-библиотеки на диске
- Генерация rust-модуля из спецификации
- Осуществление любой платформо-специфичной конфигурации, которая нужна для сборки
Каждый из этих случаев детально описан ниже для пояснения того, как работает
опция build
.
Входные данные для скрипта сборки
Когда вы запускаете скрипт сборки, можно задать параметры, которые передаются путем задания переменных окружения environment variables.
Вдобавок к переменным окружения, текущая директория скрипта сборки является директорией пакета скрипта сборки.
Выходной результат скрипта сборки
Все строки, которые будут напечатаны в stdout, будут записаны в файл
target/debug/build/<pkg>/output
(точное расположение зависит от вашей конфигурации).
Все строки, которые начинаются с cargo:
интерпретируются напрямую cargo.
Эти строки обязаны задаваться в форме cargo:key=value
, как показано в примере:
cargo:rustc-link-lib=static=foo
cargo:rustc-link-search=native=/path/to/foo
cargo:rustc-cfg=foo
cargo:root=/path/to/foo
cargo:libdir=/path/to/foo/lib
cargo:include=/path/to/foo/include
Ниже перечислены ключи, которые влияют на cargo:
-
rustc-link-lib=[KIND=]NAME
передать указанное значение компилятору с опцией-l
. Опциональный параметр KIND может принимать значения static, dylib(по умолчанию), framework. Для получения подробностей см. rustc --help -
rustc-link-search=[KIND=]PATH
передать указанное значение компилятору с опцией-L
. Опциональный параметр KIND может быть одним из: dependency, crate, native, framework, all (по умолчанию). См. rustc --help для подробностей. -
rustc-flags=FLAGS
набор флагов, которые следует передать компилятору. Поддерживаются только-l
и-L
(Прим.переводчика -- тогда на кой оно нужно?). -
rustc-cfg=FEATURE
указывает передать компилятору c директивой--cfg
указанную возможность. Это может быть полезным для определения на этапе компиляции различных возможностей. -
rerun-if-changed=PATH
путь к файлу или директории, которые при изменении должны вызвать перезапуск скрипта сборки(определяется по атрибуту last-modified). Обычно скрипт сборки перезапускается если любой файл внутри контейнера изменяется. Эта опция позволяет указать более узкий набор файлов. Если опция указывает на директорию, то cargo смотрит только на изменение времени модификации директории, и не смотрит на каждый файл. Для того, чтобы скрипт перезапускался при любом изменении во всей директории, напечатайте строку с директорией и другую строку для всего внутри нее, рекурсивно. -
warning=MESSAGE
это сообщение будет напечатано в основную консоль после того, как скрипт сборки закончит выполнение. Варнинги печатаются только на зависимостях, добавленных через path. Таким образом, варнинги на зависимостях, которые приползли с crates.io не эмитятся по умолчанию.
Любой другой элемент является пользовательскими мета-данными, и передается зависимостям.
Больше информации можно найти в секции links
.
Зависимости для процесса сборки
Скрипт сборки может иметь зависимости на другие cargo-пакеты.
Эти зависимости объявляются в секции build-dependencies
.
[build-dependencies]
foo = { git = "https://github.com/your-packages/foo" }
Скрипту сборки НЕ ДОСТУПНЫ зависимости, перечисленные в секциях
dependencies
и dev-dependencies
. Так же, все зависимости этапа сборки
недоступны пакету, и если они нужны, то их следует указать явно.
Опция сборочного манифеста links
В добавление к опции build
, Cargo поддерживает опцию links
для указания
нативных библиотек, которые следует прилинковать.
[package]
# ...
links = "foo"
build = "build.rs"
Это означает, что пакет линкуется с нативной библиотекой libfoo
, и он
имеет скрипт сборки, который находит/собирает эту библиотеку. Cargo
требует, чтобы опция build
была указана, если вы используете опцию links
.
Цель этого ключа - чтобы Cargo знал о наборе используемых нативных библиотек, и передавал эту метаинформацию между скриптами сборки пакетов.
Вы не можете слинковать несколько пакетов с одной нативной либой. Однако есть определенные случаи соглашений в которых это требование смягчено.
Как мы выяснили выше, каждый билд-скрипт может генерировать набор метаданных из пар ключ-значение. Эти метаданные передаются билд-скриптам зависимых пакетов.
Например, если libbar
зависит от libfoo
, тогда если libfoo
генерирует
key=value
как часть своих метаданных, то билд-скрипт libbar
будет иметь
переменную окружения DEP_FOO_KEY=value
.
Также надо учитывать, что метаданные передаются непосредственно-зависящим пакетам, но не транзитивно на остальных. Мотивация такой передачи данных описана ниже в секции по линковке системных либ.
Явное указание метаданных
Ключ links
включает поддержку явного указания метаданных для конкретной библиотеки.
Цель этого действия может состоять в том, чтобы сократить время компиляции.
Для явного указания расположите соответствующую секцию конфигурации в
любом доступном для cargo месте .
[target.x86_64-unknown-linux-gnu.foo]
rustc-link-search = ["/path/to/foo"]
rustc-link-lib = ["foo"]
root = "/path/to/foo"
key = "value"
Эта секция декларирует, что для цели x86_64-unknown-linux-gnu
бибилотека с именем
foo
имеет явно указанные метаданные. Эти метаданные являются такими же, как
можно было бы получить запустив сборочный скрипт, предоставляющий набор пар
ключ-значение.
В таком варианте, если пакет декларирует, что линкуется с foo, сборочный скрипт не будет скомпилирован и запущен, что сэкономит время. Вместо этого будут использованы указанные метаданные.
Конкретный пример: кодогенерация
Некоторые пакеты cargo требуют, чтобы перед началом компиляции была произведена генерация кода. Изучим простой пример, который осуществляет генерацию кода в процессе сборки проекта.
Структура директории пакета выглядит следующим образом:
.
├── Cargo.toml
├── build.rs
└── src
└── main.rs
1 directory, 3 files
Как мы можем видеть, имеется исходник скрипта build.rs
и исходник бинарника
main.rs
. Содержимое Cargo.toml:
# Cargo.toml
[package]
name = "hello-from-generated-code"
version = "0.1.0"
authors = ["you@example.com"]
build = "build.rs"
Тут указано, что мы используем сборочный скрипт. Его содержимое следующее:
// build.rs use std::env; use std::fs::File; use std::io::Write; use std::path::Path; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let dest_path = Path::new(&out_dir).join("hello.rs"); let mut f = File::create(&dest_path).unwrap(); f.write_all(b" pub fn message() -> &'static str { \"Hello, World!\" } ").unwrap(); }
Несколько комментариев к сборочному скрипту:
-
Скрипт использует переменную окружения
OUT_DIR
, которая содержит путь к директории для сгенерированного кода. В качестве директории для входных файлов используется текущая директория запущенного процесса. (в нашем случае чтения каких бы то ни было входных файлов не производится) -
Этот скрипт тривиален. Он просто генерирует файл, содержащий маленький кусочек кода. В реальных задачах ваш скрипт сборки может делать что-то более полезное. Например, это может быть генерация rust-модуля из СИ-хэдэров, или каких-то других языков.
Содержимое main.rs:
// src/main.rs include!(concat!(env!("OUT_DIR"), "/hello.rs")); fn main() { println!("{}", message()); }
Как можно видеть, здесь не происходит никакой магии: мы подгружаем сгенерированный
ранее код при помощи макроса include!
. Макрос concat!
осуществляет конкатенацию строк
на этапе компиляции. Таким образом, получается путь к подгужаемому файлу.
Используя показанную структуру, вы можете подгружать любое количество сгенерированных файлов.
Конкретный пример: сборка нативного кода
Иногда нужно собрать нативный СИ или СИ++ код и использовать его как часть пакета. Это еще одна возможность, которую предоставляет сборочный скрипт. Для примера создадим rust-программу, которая вызывает СИ-код, который печатает “Hello, World!”.
Содержимое директории проекта:
.
├── Cargo.toml
├── build.rs
└── src
├── hello.c
└── main.rs
1 directory, 4 files
Содержимое Cargo.toml:
# Cargo.toml
[package]
name = "hello-world-from-c"
version = "0.1.0"
authors = ["you@example.com"]
build = "build.rs"
Контент сборочного скрипта выглядит следующим образом:
// build.rs use std::process::Command; use std::env; use std::path::Path; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); // note that there are a number of downsides to this approach, the comments // below detail how to improve the portability of these commands. Command::new("gcc").args(&["src/hello.c", "-c", "-fPIC", "-o"]) .arg(&format!("{}/hello.o", out_dir)) .status().unwrap(); Command::new("ar").args(&["crus", "libhello.a", "hello.o"]) .current_dir(&Path::new(&out_dir)) .status().unwrap(); println!("cargo:rustc-link-search=native={}", out_dir); println!("cargo:rustc-link-lib=static=hello"); }
Этот сборочный скрипт при запуске компилирует наш СИ файл в объектный файл
(при помощи gcc
) и затем конвертирует этот объектник в статическую библиотеку
(при помощи ar
). На финальном шаге устанавливаются опции cargo, которые
предписывают искать библиотеку для линковки в out_dir
и линковаться статически
с библиотекой libhello.a
(при запуске rustc будет использован
ключ -l static=hello
).
К данному скрипту имеется несколько замечаний:
- Программа
gcc
присутствует не на всех платформах. Например, вряд ли на Windows будет gcc, и не все Unix-платформы могут иметь gcc. С командойar
аналогичная ситуация. - Эти команды не учитывают случай, если мы производим кросс-компиляцию. Если мы производим кросс-компиляцию для платформы типа Андроид, то вряд ли gcc соберет нам исполняемый файл с ARM-архитектурой.
Однако, не стоит отчаиваться. Нам поможет секция build-dependencies
. Экосистема
Cargo имеет пакеты, которые полностью или частично абстрагируют нас от платформы
и делают этот этап сборки более простым, портабельным и стандартизованным.
Для примера, сборочный скрипт может быть написан следующим образом:
// build.rs // Bring in a dependency on an externally maintained `gcc` package which manages // invoking the C compiler. extern crate gcc; fn main() { gcc::compile_library("libhello.a", &["src/hello.c"]); }
Это потребует зависимости на gcc
контейнер. Добавить ее понадобиться в Cargo.toml
:
[build-dependencies]
gcc = "0.3"
Контейнер gcc
crate абстракция, позволяющая работать
с различными вариантами, использующими С-код.
- Выполняется правильный компилятор (MSVC на windows,
gcc
на MinGW,cc
на Unix- платформах и т.д.). - Принимает переменную окружения TARGET для передачи правильных флажков компилятору.
- Другие переменные окружения (
OPT_LEVEL
,DEBUG
, и т.д.) обрабатываются автоматически. - Стандартный вывод и расположение директории
OUT_DIR
так же обрабатываются контейнеромgcc
.
Here we can start to see some of the major benefits of farming as much functionality as possible out to common build dependencies rather than duplicating logic across all build scripts!
Вернемся к исследованию содержимого директории src
:
// src/hello.c
#include <stdio.h>
void hello() {
printf("Hello, World!\n");
}
// src/main.rs // аттрибут `#[link]` не является необходимым. Мы делегируем решение того, // с какой либой линковаться в скрипт сборки, вместо того чтобы захардкодить // его в исходном коде extern { fn hello(); } fn main() { unsafe { hello(); } }
And there we go! This should complete our example of building some C code from a Cargo package using the build script itself. This also shows why using a build dependency can be crucial in many situations and even much more concise!
Еще мы тут увидели пример использования в скрипте сборки зависимостей, специально предназначенных для процесса сборки.
Конкретный пример: линковка с системными библиотеками
В финальном примере будет показано как cargo линкуется с системными библиотеками, и как скрипт сборки может управлять этим процессом.
Иногда для контейнеров rust требуются системные библиотеки.
Quite frequently a Rust crate wants to link to a native library often provided on the system to bind its functionality or just use it as part of an implementation detail. This is quite a nuanced problem when it comes to performing this in a platform-agnostic fashion, and the purpose of a build script is again to farm out as much of this as possible to make this as easy as possible for consumers.
В качестве примера рассмотрим одну из зависимостей самого cargo Cargo’s own dependencies, libgit2. Эта библиотека имеет некоторые ограничения:
- Имеет опциональную зависимость на OpenSSL на Unix-системах для реализации https-транспорта.
- Имеет опциональную зависимость на libssh2 на всех платформах для реализации ssh-транспорта.
- Зачастую не установлена по умолчанию на всех системах.
- Собирается из исходников при помощи
cmake
.
Чтобы понять, как это происходит, рассмотрим билд-манифест Cargo.toml.
[package]
name = "libgit2-sys"
version = "0.1.0"
authors = ["..."]
links = "git2"
build = "build.rs"
[dependencies]
libssh2-sys = { git = "https://github.com/alexcrichton/ssh2-rs" }
[target.'cfg(unix)'.dependencies]
openssl-sys = { git = "https://github.com/alexcrichton/openssl-sys" }
# ...
Можно видеть, что в первой секции сборочного манифеста мы указываем сборочный скрипт
опцией build
. Также этот пример имеет опцию links
, которая указывает нашему
контейнеру (crate libgit2-sys
) линковаться с нативной либой git2
.
Контейнер имеет безусловную зависимость на libssh2
через указание
libssh2-sys
контейнера, и также платформо-специфичную зависимость на openssl-sys
Тут имеется следующий интуитивно-непонятный момент: у нас указана Си-зависимость в сборочном манифесте cargo. Это является частью соглашений для cargo, которому посвящена следующая глава.
*-sys
пакеты
Для облегчения линковки с системными библиотеками cargo имеет соглашение
по именованию пакетов и функциональности. Любой пакет с именем foo-sys
будет предоставлять две основные функциональные возможности:
-
контейнер будет линковаться с библиотекой
libfoo
. Обычно это вызывает проверку того, что либаlibfoo
установлена в системе. Если не установлена, то запускается процесс ее сборки. -
контейнер будет предоставлять декларации для функций в
libfoo
, но НЕ предоставляет биндингов для абстракций более высокого уровня.
Основное множество *-sys
пакетов предоставляют основное множество зависимостей
для линковки с нативными либами. Этим достигаются следующие выгоды для
контейнеров, имеющих отношение к нативным либам:
- Зависимости на
foo-sys
упрощают вышеописанное правило одного пакета на опциюlinks
. - A common dependency allows centralizing logic on discovering
libfoo
itself (или сборку с исходников). - Эти зависимости легко переопределить.
Сборка libgit2
Теперь, когда мы получили зависимости libgit2, нам нужно написать логику
сборочного скрипта. Мы не будем разбираться в узко-специфичных моментах кода
и сконцентрируем свое внимание лишь на высокоуровневых деталях сборочного скрипта
контейнера libgit2-sys
. Это не является рекомендацией для всех пакетов, а лишь
является одной из возможных специфичных стратегий.
На первом шаге сборочный скрипт может попытаться найти место, в которое установлена
libgit2. Это можно попытаться сделать при помощи предустановленного
pkg-config
(если он есть). We’ll also use a build-dependencies
section to refactor out all the pkg-config
related code (or someone’s already
done that!).
Если у pkg-config
не получилось найти libgit2, или если pkg-config
не
установлен, следующим шагом будет сборка libgit2 из встроенных исходников
(которые распространяются как часть самого libgit2-sys
).
Есть несколько нюансов, которые стоит упомянуть.
-
Система сборки libgit2,
cmake
, должна быть способна найти опциональную зависимость для libgit2’s -- libssh2. Нам не помешает убедиться, что она была собрана (это часть зависимостей Cargo), и мы должны обменяться с cargo этой информацией. Для осуществления этого используем формат метаданных для обмена между сборочными скриптами. В этом примере пакет libssh2 печатаетcargo:root=...
для того, чтобы сказать, куда libssh2 установлена. И мы передаем потом эту информацию в cmake через переменную окруженияCMAKE_PREFIX_PATH
. -
Нам надо указать специфичные
CFLAGS
флаги для сборки СИ-кода (и сказатьcmake
об этом). Некоторые флаги могут потребовать указания ключа-m64
для 64-битного кода,-m32
для 32-битного кода, или-fPIC
-
На финальной стадии вызывается
cmake
и оказывается располагать весь выхлоп поOUT_DIR
, и потом мы печатаем необходимые метаданные для указания rustc как линковаться с libgit2
Большая часть функциональности этого сборочного скрипта легко модифицируется под основные зависимости. Поэтому сборочных скрипт запугивает даже в меньшей степени, чем это его описание. На самом деле, предполагается, что скрипты сборки достаточно кратки и и содержат логику, подобную той, что было описано выше для построения необходимых зависимостей.