Кросскомпиляция выполняемых файлов 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, добро пожаловать под кат.


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]$ 

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

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 16

    0
    Когда решал такую же задачу, с cross плясать с бубном пришлось гораздо меньше.
      0
      Про cross я знаю, мне его на Stackoverflow в прошлом году уже рекомендовали. Но через него нельзя собирать 32битные цели, только 64битные. В общем мне он не подошёл. А по моему рецепту можно и 32bit, и 64bit.
        0
        Хм, действительно, 32 бит под винду нет. Возможно, стоит заслать фича-реквест.
      +1
      Увы, с настройкой кросс-компиляции бинарников проблемы не заканчиваются, а только начинаются. Потому что современные приложения чаще всего подразумевают GUI, который тоже должен быть кроссплатформенным и который тоже надо настроить.
      И вот тут-то вдруг выясняется, что в современном мире не существует НИ ОДНОГО вменяемого кроссплатформенного GUI-фреймворка, пригодного для нативных приложений. Люди не просто так массово на Electron пересаживаются, а потому что всё остальное использовать по-настоящему мучительно.
      И это не только в Rust-инфраструктуре, это вообще нигде ни шиша нет, даже в мире C/C++. Всё, что имеется — оно жутко раздуто, проблемно в сборке и тянет за собой тонны зависимостей. Даже Qt не позволяет получить итоговый exe менее 5Мб, остальное тащит с собою ещё больше. А уж какое лютое шаманство творится при сборке и сопряжении с Rust…
      Для себя нашёл, что из всех сортов дерьма наиболее удобно и компактно происходит написание GUI на (кто бы мог подумать!) Lazarus, а Rust-часть с полезной нагрузкой просто собирается как статическая библиотека (.a или .lib в зависимости от платформы), и всё это потом линкуется с GUI частью в единый бинарник.
      В итоге проекты с одним окошком и одной кнопкой «Открыть файл» получаются менее мегабайта по объёму.
        +1
        А уж какое лютое шаманство творится при сборке и сопряжении с Rust…

        А в чем шаманство? Я писал интерфейс на Qt/C++ и логику на Rust. Общение
        было с помощью Rust/FFI. Rust/FFI я сам конечно не писал он автоматом генерировался.
        Но у меня конечно не было цели влезть в ±5МБ, хотя в теории это возможно,
        если статически слинковаться с Qt и выкинуть неиспользуемые функции,
        но я проверял давно и с Qt 4.x, возможно с Qt 5.x все хуже (имеется ввиду провверял возможность влезть в ±5 мегабайт).

          0
          Я, правда, давненько уже собирал статический exe с qt5, но, помнится мне, в 5 мб он действительно не укладывается, там размер получается раза в 2 больше как минимум, а то и в 3, зависит от набора используемых модулей. С другой стороны, это все равно гораздо меньше, чем тот же Электрон, элементарный hello world на котором занимает в разы больше, там чуть ли не 50-100 мб в зависимости от платформы (самый большой по размеру пакет выходил на macos).
            0

            Если собирать релизный бинарник с простым hello world на Rust, то в 2018 он занимал чутка больше 5 мегабайт и чуть поменьше 4 после strip. Сейчас там накрутили кое-чего и с lto и какими-то еще дополнительными параметрами там вроде можно собрать такую программу размером в примерно 1 мегабайт. А вот чтобы собрать меньше — нужно делать немало телодвижений — от простого no_std, до перереализации IO/паник и замены аллокатора. Благо wasm и emded способствуют развитию этого.

              +2
              там вроде можно собрать такую программу размером в примерно 1 мегабайт

              Ну в данном случае мы же говорим не о бинарнике, а разделяемой библиотеке на Rust.
              У меня она занимает 27K после strip, думаю с panic=abort можно еще ужать.
              Это конечно "hello world" с одной функцией:


              #[no_mangle]
              pub extern "C" fn add(x: i32) -> i32 {
                  x + 1
              }

              Бинарник собранный в release режиме кстати занимает 207k с main вида 'println!("hello")'.

            0
            Just try juce
            0

            Тут напрашиваются cargo-xbuild или xargo.

              0
              Не, тут же не надо весь sysroot пересобирать, тут можно и малой кровью обойтись.
                0

                Для этих целей ещё есть cargo build -Z build-std

                0
                Да, в Go все проще обстоит: habr.com/ru/post/249449

                Кстати, очень удобная штука — можно собирать исполняемые файлы с разными веб-серверами как для ARM систем вроде Raspberry, так даже и для некоторых моделей роутеров если у них процессор с поддерживаемой архитектурой. Например такая команда для сборки под ASUS AC-66:

                GOOS=linux GOARCH=mipsle go build
                  0
                  если это все действительно заработало, то круто, не зря хоть время потратил
                    0
                    Естественно заработало, я не просто так этот квест проходил. Ну а если вдруг ещё чего всплывёт, то как я сказал
                    далее будем решать проблемы по мере их появления (ага, можно сказать что это получается прям какой-то Test-Driven Development, :-)

                    Но пока проблем тьфу-тьфу-тьфу нет.
                    0

                    Начиналось всё скучно, а вот потом началась драма. Спасибо.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое