[recovery mode] Кросскомпиляция выполняемых файлов Rust для Windows из Linux
Наверное не будет уж очень удивительным если я тут, на IT площадке Хабра, скажу что я иногда балую себя программированием.
Основная OS у меня Linux, но иногда приходится собирать исполняемые файлы и для Windows. И естественно что перегружаться в Windows только для сборки exe не особо хочется. С языками C и C++ проблем нет, давно существует кросскомпилятор MinGW
, который прекрасно с этим справляется. Про Python
и Java
даже упоминать не стоит, кроссплатформенность в них изначально. Но в прошлом году я решил попробовать такой пока что новомодный язык, как Rust. При сборке исполняемого файла при помощи включённого в дистрибутив Rust пакетного менеджера cargo
вроде как достаточно задать ключ --target
, при помощи которого указать результирующий процессор, архитектуру и ABI и при сборке из Linux в результате получить exe, который будет являться стандартным исполняемым файлом для Windows. Но пытаясь так сделать:
cargo build --target x86_64-pc-windows-gnu
я получил только сообщения об ошибках линкера:
error: linking with `gcc` failed: exit code: 1
[...]
= note: /usr/bin/ld: unrecognized option '--nxcompat'
/usr/bin/ld: use the --help option for usage information
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Если кому интересно как я это поборол и теперь спокойно могу кросскомпилировать программы на Rust для Windows, не покидая Linux, добро пожаловать под кат.
Далее я рассматриваю только цели 32bit и 64bit pc-windows-gnu, цели pc-windows-msvc для меня интереса не представляют и поэтому в них я не углублялся. Так же речь будет идти о том дистрибутиве Linux, который установлен на моём компьютере, то есть Fedora Linux 31, но я не думаю что на других дистрибутивах Linux будут очень уж существенные различия. И я использую Rust установленный при помощи The Rust toolchain installer, а не входящий в репозиторий Fedora Rust по причине того, что мне иногда требуются nightly сборки Rust, которых в стандартном репозитории, естественно, нет.
Первым делом убеждаемся что у нас установлены необходимые цели, запустив следующую команду:
rustup target list
Получаем список всех возможных целей, и целей, которые у нас установлены:
aarch64-apple-ios
aarch64-fuchsia
[...]
i686-pc-windows-gnu (installed)
[...]
i686-unknown-linux-gnu (installed)
[...]
x86_64-pc-windows-gnu (installed)
x86_64-unknown-linux-gnu (installed)
[...]
Для создания исполняемых файлов для Windows из Linux нам необходимы цели i686-pc-windows-gnu
для 32bit exe и x86_64-pc-windows-gnu
для 64bit exe. Если данные цели не отмечены как (installed)
, то доставляем их при помощи команды
rustup target add имя_цели
После убеждаемся что у нас установлен кросскомпилятор MinGW
, запустив
rpm -qa | grep mingw
или другой пакетный менеджер для нашего дистрибутива Linux:
mingw32-gcc-9.2.1-1.fc31.x86_64
mingw32-binutils-2.32-6.fc31.x86_64
mingw64-gcc-9.2.1-1.fc31.x86_64
mingw-binutils-generic-2.32-6.fc31.x86_64
mingw-filesystem-base-110-1.fc31.noarch
mingw64-winpthreads-6.0.0-2.fc31.noarch
mingw32-winpthreads-6.0.0-2.fc31.noarch
mingw32-crt-6.0.0-2.fc31.noarch
mingw64-binutils-2.32-6.fc31.x86_64
mingw64-crt-6.0.0-2.fc31.noarch
mingw64-filesystem-110-1.fc31.noarch
mingw32-filesystem-110-1.fc31.noarch
mingw32-cpp-9.2.1-1.fc31.x86_64
mingw64-headers-6.0.0-2.fc31.noarch
mingw32-headers-6.0.0-2.fc31.noarch
mingw64-cpp-9.2.1-1.fc31.x86_64
При отсутствии MinGW
устанавливаем необходимые пакеты, запустив
sudo dnf install mingw32-gcc mingw64-gcc
Ну вот вроде бы теперь всё в наличии, далее будем решать проблемы по мере их появления (ага, можно сказать что это получается прям какой-то Test-Driven Development, :-)
Создаём простейший проект на языке Rust:
[pfemidi@pfemidi rust]$ cargo new foobar
Created binary (application) `foobar` package
[pfemidi@pfemidi rust]$ cat foobar/src/main.rs
fn main() {
println!("Hello, world!");
}
[pfemidi@pfemidi rust]$
Сначала компилируем и запускаем его как родное приложение Linux:
[pfemidi@pfemidi foobar]$ cargo run
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
Finished dev [unoptimized + debuginfo] target(s) in 1.65s
Running `target/debug/foobar`
Hello, world!
[pfemidi@pfemidi foobar]$
Всё работает. Теперь пробуем его собрать как цель x86_64-pc-windows-gnu
:
cargo build --target x86_64-pc-windows-gnu
и получаем всё то же сообщение об ошибке сборки:
error: linking with `gcc` failed: exit code: 1
[...]
= note: /usr/bin/ld: unrecognized option '--nxcompat'
/usr/bin/ld: use the --help option for usage information
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Понятно, для сборки вызывается не линкер из MinGW
, а уже установленный в системе gcc
. Исправляем эту ситуацию, для этого создаём в проекте директорию .cargo и в ней файл config со следующим содержимым:
[pfemidi@pfemidi foobar]$ mkdir .cargo
[pfemidi@pfemidi foobar]$ cat > .cargo/config
[target.i686-pc-windows-gnu]
linker = "i686-w64-mingw32-gcc"
ar = "i686-w64-mingw32-ar"
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-ar"
[pfemidi@pfemidi foobar]$
Это необходимо для того чтобы при сборке целей для Windows в качестве линкера использовался не установленный в системе gcc
, а линкер из MinGW
.
Пробуем собрать проект снова:
cargo build --target x86_64-pc-windows-gnu
и получаем другую ошибку от линкера, уже от x86_64-w64-mingw32-gcc
:
error: linking with `x86_64-w64-mingw32-gcc` failed: exit code: 1
[...]
= note: /usr/lib/gcc/x86_64-w64-mingw32/9.2.1/../../../../x86_64-w64-mingw32/bin/ld: cannot find -lpthread
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Дело в том, что Rust по-умолчанию собирает всё в статическом виде, поэтому кроме пакетов mingw32-winpthreads
и mingw64-winpthreads
, которые dnf
автоматически установил как зависимости для mingw32-gcc
и mingw64-gcc
обязательно должны быть установлены пакеты статических библиотек mingw32-winpthreads-static
и mingw64-winpthreads-static
, без них линкер всё время будет жаловаться на отсутствующий -lpthread
и сборка не пройдёт. Доустанавливаем недостающие пакеты:
sudo dnf install mingw??-winpthreads-static
и опять запускаем компиляцию:
cargo build --target x86_64-pc-windows-gnu
Опять ошибка линковки! Но уже другая:
error: linking with `x86_64-w64-mingw32-gcc` failed: exit code: 1
[...]
= note: /usr/lib/gcc/x86_64-w64-mingw32/9.2.1/../../../../x86_64-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/crt2.o:crtexe.c:(.rdata$.refptr.__onexitbegin[.refptr.__onexitbegin]+0x0): undefined reference to `__onexitbegin'
/usr/lib/gcc/x86_64-w64-mingw32/9.2.1/../../../../x86_64-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/crt2.o:crtexe.c:(.rdata$.refptr.__onexitend[.refptr.__onexitend]+0x0): undefined reference to `__onexitend'
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Линкер жалуется на отсутствующие символы __onexitbegin
и __onexitend
в файле ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/crt2.o
, который мы установили в составе цели x86_64-pc-windows-gnu
. После некоторых раздумий, гугления, чтения доков на сайте Rust, изучения исходников самого Rust, того как и чем сам Rust собирается я понял: дело в том что сам Rust для Windows, и соответственно его компоненты для целей pc-windows-gnu, собраны с использованием MinGW 6.3.0, а у меня в Fedora Linux 31 версия MinGW 9.2.1, поэтому и происходит несоответствие в CRT. Ok, попробуем перенести crt2.o из федориного MinGW в директорию Rust для цели x86_64-pc-windows-gnu. И кроме crt2.o перенесём ещё и dllcrt2.o, который является точкой входа для динамических библиотек:
[pfemidi@pfemidi foobar]$ cd ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/
[pfemidi@pfemidi lib]$ cp /usr/x86_64-w64-mingw32/sys-root/mingw/lib/crt2.o .
[pfemidi@pfemidi lib]$ cp /usr/x86_64-w64-mingw32/sys-root/mingw/lib/dllcrt2.o .
[pfemidi@pfemidi lib]$ cd -
/home/pfemidi/mywork/rust/foobar
[pfemidi@pfemidi foobar]$
и опять запускаем компиляцию нашего проекта на Rust:
pfemidi@pfemidi foobar]$ cargo build --target x86_64-pc-windows-gnu
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
Finished dev [unoptimized + debuginfo] target(s) in 4.46s
[pfemidi@pfemidi foobar]$
Прекрасно! Всё собралось! Т.к. у меня установлен wine, то тут же я могу и проверить как это работает:
[pfemidi@pfemidi foobar]$ cargo run --target x86_64-pc-windows-gnu
Finished dev [unoptimized + debuginfo] target(s) in 0.38s
Running `target/x86_64-pc-windows-gnu/debug/foobar.exe`
Hello, world!
[pfemidi@pfemidi foobar]$
И даже работает! Теперь пробуем сделать то же самое для 32bit версии исполняемого файла Windows, делаем сразу run
без предварительного build
:
error: linking with `i686-w64-mingw32-gcc` failed: exit code: 1
[...]
= note: /usr/lib/gcc/i686-w64-mingw32/9.2.1/../../../../i686-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/crt2.o:crtexe.c:(.text+0x75): undefined reference to `__onexitend'
/usr/lib/gcc/i686-w64-mingw32/9.2.1/../../../../i686-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/crt2.o:crtexe.c:(.text+0x7a): undefined reference to `__onexitbegin'
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Ошибку с отсутствием символов __onexitbegin
и __onexitend
теперь уже в файле ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/crt2.o
мы уже проходили, лечится точно так же, как и для 64bit цели заменой файлов crt2.o и dllcrt2.o на аналогичные по именам, но из дистрибутива MinGW
из Fedora:
[pfemidi@pfemidi foobar]$ cd ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/
[pfemidi@pfemidi lib]$ cp /usr/i686-w64-mingw32/sys-root/mingw/lib/crt2.o .
[pfemidi@pfemidi lib]$ cp /usr/i686-w64-mingw32/sys-root/mingw/lib/dllcrt2.o .
[pfemidi@pfemidi lib]$ cd -
/home/pfemidi/mywork/rust/foobar
[pfemidi@pfemidi foobar]$
Проверяем:
[pfemidi@pfemidi foobar]$
[pfemidi@pfemidi foobar]$ cargo run --target i686-pc-windows-gnu
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
Finished dev [unoptimized + debuginfo] target(s) in 5.12s
Running `target/i686-pc-windows-gnu/debug/foobar.exe`
Hello, world!
[pfemidi@pfemidi foobar]$
Тут теперь тоже всё собирается и работает.
И всё было прекрасно пока я не использовал никакие функции, которые паникуют (macro panic!, функция expect и т.д.) в 32bit целях для Windows. В целях 64bit всё хорошо, а вот в целях 32bit нет.
Добавим в наш проект панику:
[pfemidi@pfemidi foobar]$ cat src/main.rs
fn main() {
println!("Hello, world!");
panic!("I'm panicked!"); // ВОТ НАША ПАНИКА!
}
[pfemidi@pfemidi foobar]
и попробуем собрать как исполняемый файл для 64bit Windows:
[pfemidi@pfemidi foobar]$ cargo run --target x86_64-pc-windows-gnu
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
Finished dev [unoptimized + debuginfo] target(s) in 2.95s
Running `target/x86_64-pc-windows-gnu/debug/foobar.exe`
Hello, world!
thread 'main' panicked at 'I'm panicked!', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
[pfemidi@pfemidi foobar]$
И компилируется, и собирается, и работает. Попробуем теперь сделать то же самое, но в качестве цели укажем 32bit Windows.
Упс:
[pfemidi@pfemidi foobar]$ cargo run --target i686-pc-windows-gnu
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
error: linking with `i686-w64-mingw32-gcc` failed: exit code: 1
[...]
= note: /usr/lib/gcc/i686-w64-mingw32/9.2.1/../../../../i686-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/libpanic_unwind-1a1fb2d4d34efaf8.rlib(panic_unwind-1a1fb2d4d34efaf8.panic_unwind.2hbcqjo8-cgu.0.rcgu.o): in function `ZN12panic_unwind3imp5panic17hdaabfe6326236dacE':
/rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8\/src\libpanic_unwind/gcc.rs:73: undefined reference to `_Unwind_RaiseException'
/usr/lib/gcc/i686-w64-mingw32/9.2.1/../../../../i686-w64-mingw32/bin/ld: /home/pfemidi/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/libpanic_unwind-1a1fb2d4d34efaf8.rlib(panic_unwind-1a1fb2d4d34efaf8.panic_unwind.2hbcqjo8-cgu.0.rcgu.o): in function `rust_eh_unwind_resume':
/rustc/5e1a799842ba6ed4a57e91f7ab9435947482f7d8\/src\libpanic_unwind/gcc.rs:327: undefined reference to `_Unwind_Resume'
collect2: error: ld returned 1 exit status
error: aborting due to previous error
error: could not compile `foobar`.
Опять линкер жалуется на отсутствие символов, но теперь это символы _Unwind_RaiseException
и _Unwind_Resume
в модуле libpanic
стандартной библиотеки Rust.
Снова раздумия, снова гугление, снова чтение доков и изучение исходников как самого Rust, так и его стандартной библиотеки. И я понял почему возникает такая ошибка.
Для разматывания стека при исключении Rust использует метод Dwarf
для 32bit целей Windows и SEH
для 64bit целей Windows, а MinGW
из стандартного репозитория Fedora Linux использует метод SJLJ
для 32bit целей Windows и SEH
для 64bit целей Windows (о различии между этими методами читать тут). Поэтому 64bit цели собираются без вопросов, а для 32bit просто нет необходимых символов и объектных файлов. Чтобы получить данные файлы необходимо пересобрать MinGW
с поддержкой Dwarf
вместо поддерки SJLJ
по умолчанию для 32bit целей Windows.
Я не буду вдаваться в подробности как именно пересобирать MinGW
, это уже не так сложно и не так интересно (configure
там надо запускать с параметром --disable-sjlj-exceptions
, остальное тривиально), скажу только одно: после того как MinGW
пересобран с разматыванием стека Dwarf
вместо SJLJ
оттуда надо взять всего один файл под названием libgcc_eh.a
и положить его в директорию с библиотеками для цели i686-pc-windows-gnu
. После этого проекты в которых используются паникующие функции начнут собираться не только для 64bit целей Windows, но и для 32bit:
[pfemidi@pfemidi foobar]$ cd ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-pc-windows-gnu/lib/
[pfemidi@pfemidi lib]$ cp ~/rpmbuild/BUILD/gcc-9.2.1-20190827/build_win32/i686-w64-mingw32/libgcc/libgcc_eh.a .
[pfemidi@pfemidi lib]$ cd -
/home/pfemidi/mywork/rust/foobar
[pfemidi@pfemidi foobar]$ cargo run --target i686-pc-windows-gnu
Compiling foobar v0.1.0 (/home/pfemidi/mywork/rust/foobar)
Finished dev [unoptimized + debuginfo] target(s) in 4.57s
Running `target/i686-pc-windows-gnu/debug/foobar.exe`
Hello, world!
thread 'main' panicked at 'I'm panicked!', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
[pfemidi@pfemidi foobar]$
Ну вот, как-то так.