Кросскомпиляция выполняемых файлов Rust для Windows из Linux +34


Наверное не будет уж очень удивительным если я тут, на 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, добро пожаловать под кат.


Disclaimer

Далее я рассматриваю только цели 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]$ 

Ну вот, как-то так.




К сожалению, не доступен сервер mySQL