Вопрос: Я написал простой C-код для тестирования:
#include <stdio.h>
int main(void){
printf("Translaaaaaatooooor\n");
return 0;
}
И скомпилировал его с gcc -c main.c
, но сгенерировался исполняемый файл (только для Linux) main.o. Если запустить его ./main.o
, он отобразит Translaaaaaatooooor.
Мой вопрос в том, как я могу скомпилировать main.c, чтобы Windows могла запускать его? В основном, как вы создаете *.exe
-файл с GCC в подсистеме Linux?
Экзешник созданный командой gcc -o translaaaatoooor.exe main.c
не работает в Windows.
insolor
47.2k17 золотых знаков56 серебряных знаков99 бронзовых знаков
задан 2 фев 2018 в 13:23
3
Перенесено из вопроса
Чтобы под Linux создать исполняемые файлы для Windows, необходимо
установить кросс-компилятор mingw:sudo apt-get install mingw-w64
Затем вы можете создать 32-разрядную версию .exe для Windows с
помощью:i686-w64-mingw32-gcc -o main32.exe main.c
И 64-разрядную версию .exe для Windows:
x86_64-w64-mingw32-gcc -o main64.exe main.c
Эти исполняемые файлы Windows не будут работать внутри подсистемы
Linux, только за ее пределами.
Перевод ответа @MarkusLaire
2
Компиляция с флагом -с
создает не исполняемый файл, а бинарник. Для компиляции исполняемого файла нужно использовать -o
:
gcc -o 1 1.c -Wall
К тому же если вы просто хотите перенести скомпилированный файл из линукса в винду, то ничего не получится. Для работы в виндоус нужен mingw
, если нужны сторонние библиотеки, то лучше установить еще и msys
.
ответ дан 2 фев 2018 в 17:02
Andrej LevkovitchAndrej Levkovitch
8,0272 золотых знака19 серебряных знаков45 бронзовых знаков
Одна из потрясающих особенностей Linux состоит в том, что, работая в нем, вы можете создавать приложения Windows. Такую возможность обеспечивает пакет mingw-w64. В его состав входят компиляторы для создания 32- и 64-битного кода С и С++ для систем Windows. После установки пакета mingw-w64 генерация кода Windows осуществляется следующим образом:
# C i686-w64-mingw32-gcc hello.c -o hello32.exe # 32-bit x86_64-w64-mingw32-gcc hello.c -o hello64.exe # 64-bit # C++ i686-w64-mingw32-g++ hello.cc -o hello32.exe # 32-bit x86_64-w64-mingw32-g++ hello.cc -o hello64.exe # 64-bit
Компиляторы имеют сложные названия, поэтому лучше всего оформить их в виде make-файла или скрипта.
Компилируемые программы на C будут зависеть от библиотек kernel32.dll и msvcrt.dll. Первая является неотъемлемой частью ОС Windows. Вторая представляет собой стандартную библиотеку времени выполнения C от компании Microsoft и обычно также входит в состав ее операционной системы.
Программы на C++ будут также зависеть от библиотек libstdC++-6.dll и libgcc_s_sjlj-1.dll. Это библиотеки компиляторов Mingw. Они находятся в каталоге /usr/lib/gcc/x86_64-w64-mingw32/. Для того, чтобы статически включить их в состав вашего исполняемого модуля укажите при компиляции ключи:
-static-libgcc -static-libstdc++
P.S. при написании этого поста использовались материалы блога arrayfire (точнее статьи).
P.P.S. кто-нибудь может назвать мне хотя бы один компилятор в Windows, позволяющий создавать двоичный код для Linux?
Время на прочтение
14 мин
Количество просмотров 77K
Библиотека Qt позволяет делать действительно кроссплатформенные приложения. Единожды написанный код можно откомпилировать под многие операционные системы. Но проблема именно в слове «компилировать», т.к. подразумевается, что необходимо перезагрузиться под целевую систему, иметь в ней настроенную среду разработки, установленный и настроенный зоопарк библиотек. Спасает кросс-компиляция — компиляция, производящая исполняемый код для платформы, отличной от той, на которой исполняется.
Кросс-компиляция для Windows 64
Обычно одной из наиболее востребованных проблем является сборка Windows-версии своего приложения, изначально разрабатывающегося под Linux. Пример решения этой проблемы можно увидеть тут или на русском. Необходимо создать mkspecs-конфигурацию, положить файлы Qt в соответствующие директории и всё. Компилировать Qt в таком случае не обязательно, можно скачать бинарники с официального сайта.
У такого подхода есть несколько минусов: 1) QtCreator об установленной таким образом библиотеке ничего не знает; 2) Официальной сборки Qt для Windows x64 не существует. И если с первой проблемой ещё как-то можно бороться, то против второй поможет только компиляция…
Перед кросс-компиляцией не забудьте поставить непосредственно сам кросс-компилятор (ищется в пакетом менеджере по названию «mingw»). И скачать исходники qt-everywhere с официального сайта. В директории mkspecs распакованного архива копируем папку win32-g++ в win64-x-g++ и корректируем содержимое файла qmake.conf. У меня получилось следующее:
Скрытый текст
#
# qmake configuration for win64-x-g++
#
# Written for MinGW
#
# Cross compile example for i686-w64-mingw32-g++:
# configure -xplatform win32-g++ -device-option CROSS_COMPILE=i686-w64-mingw32-
#
MAKEFILE_GENERATOR = MINGW
load(device_config)
equals(QMAKE_HOST.os, Windows): EXE_SUFFIX = .exe
TEMPLATE = app
CONFIG += qt warn_on release link_prl copy_dir_files debug_and_release debug_and_release_target precompile_header
QT += core gui
DEFINES += UNICODE
#QT_LARGEFILE_SUPPORT
QMAKE_COMPILER_DEFINES += __GNUC__ WIN WIN32 WIN64
QMAKE_EXT_OBJ = .o
QMAKE_EXT_RES = _res.o
QMAKE_CC = $${CROSS_COMPILE}gcc
QMAKE_LEX = flex
QMAKE_LEXFLAGS =
QMAKE_YACC = byacc
QMAKE_YACCFLAGS = -d
QMAKE_CFLAGS = -pipe -m64
QMAKE_CFLAGS_DEPS = -M
QMAKE_CFLAGS_WARN_ON = -Wall -Wextra
QMAKE_CFLAGS_WARN_OFF = -w
QMAKE_CFLAGS_RELEASE = -O2
QMAKE_CFLAGS_DEBUG = -g
QMAKE_CFLAGS_YACC = -Wno-unused -Wno-parentheses
QMAKE_CXX = $${CROSS_COMPILE}g++
QMAKE_CXXFLAGS = $$QMAKE_CFLAGS
QMAKE_CXXFLAGS_DEPS = $$QMAKE_CFLAGS_DEPS
QMAKE_CXXFLAGS_WARN_ON = $$QMAKE_CFLAGS_WARN_ON
QMAKE_CXXFLAGS_WARN_OFF = $$QMAKE_CFLAGS_WARN_OFF
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE
QMAKE_CXXFLAGS_DEBUG = $$QMAKE_CFLAGS_DEBUG
QMAKE_CXXFLAGS_YACC = $$QMAKE_CFLAGS_YACC
QMAKE_CXXFLAGS_THREAD = $$QMAKE_CFLAGS_THREAD
QMAKE_CXXFLAGS_RTTI_ON = -frtti
QMAKE_CXXFLAGS_RTTI_OFF = -fno-rtti
QMAKE_CXXFLAGS_EXCEPTIONS_ON = -fexceptions -mthreads
QMAKE_CXXFLAGS_EXCEPTIONS_OFF = -fno-exceptions
QMAKE_INCDIR = /usr/x86_64-w64-mingw32/include
QMAKE_INCDIR_QT = $$[QT_INSTALL_HEADERS]
QMAKE_LIBDIR_QT = $$[QT_INSTALL_LIBS]
QMAKE_RUN_CC = $(CC) -c $(CFLAGS) $(INCPATH) -o $obj $src
QMAKE_RUN_CC_IMP = $(CC) -c $(CFLAGS) $(INCPATH) -o $@ $<
QMAKE_RUN_CXX = $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $obj $src
QMAKE_RUN_CXX_IMP = $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
QMAKE_LINK = $${CROSS_COMPILE}g++
QMAKE_LINK_C = $${CROSS_COMPILE}gcc
#QMAKE_LFLAGS = -m64
QMAKE_LFLAGS = -m64 -mthreads -static-libgcc -static -Wl,-enable-stdcall-fixup -Wl,-enable-auto-import -Wl,-enable-runtime-pseudo-reloc -mwindows
QMAKE_LFLAGS_EXCEPTIONS_ON = -mthreads
QMAKE_LFLAGS_EXCEPTIONS_OFF =
QMAKE_LFLAGS_RELEASE = -Wl,-s
QMAKE_LFLAGS_DEBUG =
QMAKE_LFLAGS_CONSOLE = -Wl,-subsystem,console
QMAKE_LFLAGS_WINDOWS = -Wl,-subsystem,windows
QMAKE_LFLAGS_DLL = -shared
QMAKE_LINK_OBJECT_MAX = 10
QMAKE_LINK_OBJECT_SCRIPT= object_script
QMAKE_PREFIX_STATICLIB = lib
QMAKE_EXTENSION_STATICLIB = a
QMAKE_LIBS =
QMAKE_LIBS_CORE = -lole32 -luuid -lws2_32 -ladvapi32 -lshell32 -luser32 -lkernel32
QMAKE_LIBS_GUI = -lgdi32 -lcomdlg32 -loleaut32 -limm32 -lwinmm -lwinspool -lws2_32 -lole32 -luuid -luser32 -ladvapi32
QMAKE_LIBS_NETWORK = -lws2_32
QMAKE_LIBS_OPENGL = -lglu32 -lopengl32 -lgdi32 -luser32
QMAKE_LIBS_COMPAT = -ladvapi32 -lshell32 -lcomdlg32 -luser32 -lgdi32 -lws2_32
QMAKE_LIBS_QT_ENTRY = -lmingw32 -lqtmain
!isEmpty(QMAKE_SH) {
MINGW_IN_SHELL = 1
QMAKE_DIR_SEP = /
QMAKE_QMAKE ~= s,\\\\,/,
QMAKE_COPY = cp
QMAKE_COPY_DIR = cp -r
QMAKE_MOVE = mv
QMAKE_DEL_FILE = rm
QMAKE_MKDIR = mkdir -p
QMAKE_DEL_DIR = rmdir
QMAKE_CHK_DIR_EXISTS = test -d
} else {
QMAKE_COPY = copy /y
QMAKE_COPY_DIR = xcopy /s /q /y /i
QMAKE_MOVE = move
QMAKE_DEL_FILE = del
QMAKE_MKDIR = mkdir
QMAKE_DEL_DIR = rmdir
QMAKE_CHK_DIR_EXISTS = if not exist
}
QMAKE_MOC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}moc$${EXE_SUFFIX}
QMAKE_UIC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}uic$${EXE_SUFFIX}
QMAKE_IDC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}idc$${EXE_SUFFIX}
QMAKE_IDL = midl
QMAKE_LIB = $${CROSS_COMPILE}ar -ru
QMAKE_RC = $${CROSS_COMPILE}windres
QMAKE_ZIP = zip -r -9
QMAKE_STRIP = $${CROSS_COMPILE}strip
QMAKE_STRIPFLAGS_LIB += --strip-unneeded
load(qt_config)
По сути в файле спецификации были заменены только пути.
Я выполнял configure со следующими параметрами:
./configure -xplatform win64-x-g++ CROSS_COMPILE=x86_64-w64-mingw32- -prefix /usr/local/qt4win64 -no-webkit -no-phonon -no-phonon-backend -no-script -no-scripttools -no-multimedia -no-qt3support -fast -nomake demos -nomake examples -nomake tools -device-option -little-endian -qt-zlib -qt-libpng -qt-libjpeg -openssl-linked -no-fontconfig -no-3dnow -no-ssse3 -continue
Здесь собираю минимальную версию Qt без webkit, phonon, multimedia и т.п. Полный список опций можно посмотреть по команде ./configure —help
Соответственно, для такой сборки должен быть установлен пакет g++-mingw-w64-x86-64, содержащий в себе x86_64-w64-mingw32-g++ (в убунту пакет надо ставить отдельно).
Далее make && sudo make install. На первом этапе компиляции используется родной системный компилятор, он собирает необходимые утилиты для linux, которые будут использоваться для сборки уже windows-бинарников.
После установки у меня в /usr/local/qt4win64/bin лежат PE32+ DLL и несколько ELF 64-bit LSB executable, в том числе: qmake, uic, moc, rcc. Вот они то и пригодятся для QtCreator!
После установки не удаляйте распакованную директорию — она используется.
Кросс-компиляция для Windows 32
Аналогична компиляции для Win64. За исключением того, что есть официальная сборка, и саму библиотеку компилировать не нужно! Достаточно собрать qmake, uic, moc, rcc.
configure:
Скрытый текст
./configure -xplatform win32-x-g++ -prefix /usr/local/qt4win32 CROSS_COMPILE=i686-w64-mingw32- -no-webkit -no-phonon -no-phonon-backend -no-script -no-scripttools -no-multimedia -no-qt3support -fast -nomake demos -nomake examples -nomake tools -device-option -little-endian -qt-zlib -qt-libpng -qt-libjpeg -openssl-linked -no-fontconfig -no-3dnow -no-ssse3 -continue
qmake.conf
Скрытый текст
#
# qmake configuration for win32-x-g++
#
# Written for MinGW
#
# Cross compile example for i686-w64-mingw32-g++:
# configure -xplatform win32-g++ -device-option CROSS_COMPILE=i686-w64-mingw32-
#
MAKEFILE_GENERATOR = MINGW
load(device_config)
equals(QMAKE_HOST.os, Windows): EXE_SUFFIX = .exe
TEMPLATE = app
CONFIG += qt warn_on release link_prl copy_dir_files debug_and_release debug_and_release_target precompile_header
QT += core gui
DEFINES += UNICODE
#QT_LARGEFILE_SUPPORT
QMAKE_COMPILER_DEFINES += __GNUC__ WIN WIN32
QMAKE_EXT_OBJ = .o
QMAKE_EXT_RES = _res.o
QMAKE_CC = $${CROSS_COMPILE}gcc
QMAKE_LEX = flex
QMAKE_LEXFLAGS =
QMAKE_YACC = byacc
QMAKE_YACCFLAGS = -d
QMAKE_CFLAGS = -pipe -m32
QMAKE_CFLAGS_DEPS = -M
QMAKE_CFLAGS_WARN_ON = -Wall -Wextra
QMAKE_CFLAGS_WARN_OFF = -w
QMAKE_CFLAGS_RELEASE = -O2
QMAKE_CFLAGS_DEBUG = -g
QMAKE_CFLAGS_YACC = -Wno-unused -Wno-parentheses
QMAKE_CXX = $${CROSS_COMPILE}g++
QMAKE_CXXFLAGS = $$QMAKE_CFLAGS
QMAKE_CXXFLAGS_DEPS = $$QMAKE_CFLAGS_DEPS
QMAKE_CXXFLAGS_WARN_ON = $$QMAKE_CFLAGS_WARN_ON
QMAKE_CXXFLAGS_WARN_OFF = $$QMAKE_CFLAGS_WARN_OFF
QMAKE_CXXFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE
QMAKE_CXXFLAGS_DEBUG = $$QMAKE_CFLAGS_DEBUG
QMAKE_CXXFLAGS_YACC = $$QMAKE_CFLAGS_YACC
QMAKE_CXXFLAGS_THREAD = $$QMAKE_CFLAGS_THREAD
QMAKE_CXXFLAGS_RTTI_ON = -frtti
QMAKE_CXXFLAGS_RTTI_OFF = -fno-rtti
QMAKE_CXXFLAGS_EXCEPTIONS_ON = -fexceptions -mthreads
QMAKE_CXXFLAGS_EXCEPTIONS_OFF = -fno-exceptions
QMAKE_INCDIR = /usr/i686-w64-mingw32/include
QMAKE_INCDIR_QT = $$[QT_INSTALL_HEADERS]
QMAKE_LIBDIR_QT = $$[QT_INSTALL_LIBS]
QMAKE_RUN_CC = $(CC) -c $(CFLAGS) $(INCPATH) -o $obj $src
QMAKE_RUN_CC_IMP = $(CC) -c $(CFLAGS) $(INCPATH) -o $@ $<
QMAKE_RUN_CXX = $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $obj $src
QMAKE_RUN_CXX_IMP = $(CXX) -c $(CXXFLAGS) $(INCPATH) -o $@ $<
QMAKE_LINK = $${CROSS_COMPILE}g++
QMAKE_LINK_C = $${CROSS_COMPILE}gcc
QMAKE_LFLAGS = -m32 -mthreads -static-libgcc -static -Wl,-enable-stdcall-fixup -Wl,-enable-auto-import -Wl,-enable-runtime-pseudo-reloc -mwindows
QMAKE_LFLAGS_EXCEPTIONS_ON = -mthreads
QMAKE_LFLAGS_EXCEPTIONS_OFF =
QMAKE_LFLAGS_RELEASE = -Wl,-s
QMAKE_LFLAGS_DEBUG =
QMAKE_LFLAGS_CONSOLE = -Wl,-subsystem,console
QMAKE_LFLAGS_WINDOWS = -Wl,-subsystem,windows
QMAKE_LFLAGS_DLL = -shared
QMAKE_LINK_OBJECT_MAX = 10
QMAKE_LINK_OBJECT_SCRIPT= object_script
QMAKE_PREFIX_STATICLIB = lib
QMAKE_EXTENSION_STATICLIB = a
QMAKE_LIBS =
QMAKE_LIBS_CORE = -lole32 -luuid -lws2_32 -ladvapi32 -lshell32 -luser32 -lkernel32
QMAKE_LIBS_GUI = -lgdi32 -lcomdlg32 -loleaut32 -limm32 -lwinmm -lwinspool -lws2_32 -lole32 -luuid -luser32 -ladvapi32
QMAKE_LIBS_NETWORK = -lws2_32
QMAKE_LIBS_OPENGL = -lglu32 -lopengl32 -lgdi32 -luser32
QMAKE_LIBS_COMPAT = -ladvapi32 -lshell32 -lcomdlg32 -luser32 -lgdi32 -lws2_32
QMAKE_LIBS_QT_ENTRY = -lmingw32 -lqtmain
!isEmpty(QMAKE_SH) {
MINGW_IN_SHELL = 1
QMAKE_DIR_SEP = /
QMAKE_QMAKE ~= s,\\\\,/,
QMAKE_COPY = cp
QMAKE_COPY_DIR = cp -r
QMAKE_MOVE = mv
QMAKE_DEL_FILE = rm
QMAKE_MKDIR = mkdir -p
QMAKE_DEL_DIR = rmdir
QMAKE_CHK_DIR_EXISTS = test -d
} else {
QMAKE_COPY = copy /y
QMAKE_COPY_DIR = xcopy /s /q /y /i
QMAKE_MOVE = move
QMAKE_DEL_FILE = del
QMAKE_MKDIR = mkdir
QMAKE_DEL_DIR = rmdir
QMAKE_CHK_DIR_EXISTS = if not exist
}
QMAKE_MOC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}moc$${EXE_SUFFIX}
QMAKE_UIC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}uic$${EXE_SUFFIX}
QMAKE_IDC = $$[QT_INSTALL_BINS]$${DIR_SEPARATOR}idc$${EXE_SUFFIX}
QMAKE_IDL = midl
QMAKE_LIB = $${CROSS_COMPILE}ar -ru
QMAKE_RC = $${CROSS_COMPILE}windres
QMAKE_ZIP = zip -r -9
QMAKE_STRIP = $${CROSS_COMPILE}strip
QMAKE_STRIPFLAGS_LIB += --strip-unneeded
load(qt_config)
Кросс-компиляция для Mac OS X
Кросс-компиляция для мака тоже очень похожа, за исключением того, что надо будет собрать и компилятор. Я собирал по этой инструкции. Это отняло полный день времени и кучу нервов. В процессе будет нужна рабочая Mac OS X (как минимум на виртуальной машине) с установленным XCode, чтобы взять оттуда необходимые файлы. При компилировании своих Qt-приложений запущенная Mac OS X не нужна.
configure:
Скрытый текст
./configure -xplatform macx-x-g++ -prefix /usr/x86_64-apple-darwin11/usr/Qt CROSS_COMPILE=/usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11- -no-webkit -no-phonon -no-phonon-backend -no-script -no-scripttools -no-multimedia -no-qt3support -fast -nomake demos -nomake examples -nomake tools -device-option -little-endian -qt-zlib -qt-libpng -qt-libjpeg -openssl-linked -no-fontconfig -no-3dnow -no-ssse3 -continue
qmake.conf:
Скрытый текст
#macx-x-g++ (different from g++.conf)
#
# qmake configuration for macx-g++
#
# Mac OS X + command-line compiler
#
MAKEFILE_GENERATOR = UNIX
TARGET_PLATFORM = macx
TEMPLATE = app
CONFIG += qt warn_on release app_bundle incremental global_init_link_order lib_version_first plugin_no_soname link_prl
QT += core gui
QMAKE_INCREMENTAL_STYLE = sublib
#include(../common/mac.conf)
#
# qmake configuration for common Mac OS X
#
#QMAKE_RESOURCE = /Developer/Tools/Rez
QMAKE_EXTENSION_SHLIB = dylib
QMAKE_INCDIR = /usr/x86_64-apple-darwin11/usr/include
QMAKE_LIBDIR =
QMAKE_INCDIR_QT = /usr/x86_64-apple-darwin11/usr/Qt/include
QMAKE_LIBDIR_QT = /usr/x86_64-apple-darwin11/usr/Qt/lib
QMAKE_INCDIR_OPENGL = /System/Library/Frameworks/OpenGL.framework/Headers \
/System/Library/Frameworks/AGL.framework/Headers/
QMAKE_FRAMEWORKDIR_QT= /usr/x86_64-apple-darwin11/usr/QtFrameworks
QMAKE_FIX_RPATH = install_name_tool -id
QMAKE_LFLAGS_RPATH =
QMAKE_LIBS_DYNLOAD =
QMAKE_LIBS_OPENGL = -framework OpenGL -framework AGL
QMAKE_LIBS_OPENGL_QT = $$QMAKE_LIBS_OPENGL
QMAKE_LIBS_THREAD =
QMAKE_MOC = $$[QT_INSTALL_BINS]/moc
QMAKE_UIC = $$[QT_INSTALL_BINS]/uic
QMAKE_AR = /usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11-ar cq
QMAKE_RANLIB = /usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11-ranlib -s
QMAKE_TAR = tar -cf
QMAKE_GZIP = gzip -9f
QMAKE_COPY = cp -f
QMAKE_COPY_FILE = $$QMAKE_COPY
QMAKE_COPY_DIR = $$QMAKE_COPY -R
QMAKE_MOVE = mv -f
QMAKE_DEL_FILE = rm -f
QMAKE_DEL_DIR = rmdir
QMAKE_CHK_DIR_EXISTS = test -d
QMAKE_MKDIR = mkdir -p
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.5 # overridden to 10.5 for Cocoa on the compiler command line
#
# qmake configuration for common unix
#
QMAKE_LEX = flex
QMAKE_LEXFLAGS +=
QMAKE_YACC = yacc
QMAKE_YACCFLAGS += -d
QMAKE_YACCFLAGS_MANGLE += -p $base -b $base
QMAKE_YACC_HEADER = $base.tab.h
QMAKE_YACC_SOURCE = $base.tab.c
QMAKE_PREFIX_SHLIB = lib
QMAKE_PREFIX_STATICLIB = lib
QMAKE_EXTENSION_STATICLIB = a
#include(../common/gcc-base-macx.conf)
#
# This file is used as a basis for the following compilers:
#
# - The QNX qcc compiler
#
# The only difference between this and gcc-base.conf is that -pipe is removed
# as it's on by default in qcc (and has an analogous -nopipe option)
#
QMAKE_CFLAGS +=
QMAKE_CFLAGS_DEPS += -M
QMAKE_CFLAGS_WARN_ON += -Wall -W
QMAKE_CFLAGS_WARN_OFF += -w
QMAKE_CFLAGS_RELEASE += -O2
QMAKE_CFLAGS_DEBUG += -g
QMAKE_CFLAGS_SHLIB += -fPIC
QMAKE_CFLAGS_STATIC_LIB += -fPIC
QMAKE_CFLAGS_YACC += -Wno-unused -Wno-parentheses
QMAKE_CFLAGS_HIDESYMS += -fvisibility=hidden
QMAKE_CXXFLAGS += $$QMAKE_CFLAGS
QMAKE_CXXFLAGS_DEPS += $$QMAKE_CFLAGS_DEPS
QMAKE_CXXFLAGS_WARN_ON += $$QMAKE_CFLAGS_WARN_ON
QMAKE_CXXFLAGS_WARN_OFF += $$QMAKE_CFLAGS_WARN_OFF
QMAKE_CXXFLAGS_RELEASE += $$QMAKE_CFLAGS_RELEASE
QMAKE_CXXFLAGS_DEBUG += $$QMAKE_CFLAGS_DEBUG
QMAKE_CXXFLAGS_SHLIB += $$QMAKE_CFLAGS_SHLIB
QMAKE_CXXFLAGS_STATIC_LIB += $$QMAKE_CFLAGS_STATIC_LIB
QMAKE_CXXFLAGS_YACC += $$QMAKE_CFLAGS_YACC
QMAKE_CXXFLAGS_HIDESYMS += $$QMAKE_CFLAGS_HIDESYMS -fvisibility-inlines-hidden
QMAKE_LFLAGS +=
QMAKE_LFLAGS_DEBUG +=
QMAKE_LFLAGS_APP +=
QMAKE_LFLAGS_RELEASE +=
#
# Base qmake configuration for GCC on Mac OS X
#
# Before making changes to this file, please read the comment in
# gcc-base.conf, to make sure the change goes in the right place.
#
# To verify that your change has the desired effect on the final configuration
# you can use the manual test in tests/manual/mkspecs.
#
QMAKE_COMPILER_DEFINES += __APPLE__ __GNUC__
QMAKE_CFLAGS_X86 += -arch i386
QMAKE_CFLAGS_X86_64 += -arch x86_64
QMAKE_CFLAGS_PPC += -arch ppc
QMAKE_CFLAGS_PPC_64 += -arch ppc64
QMAKE_CFLAGS_DWARF2 += -gdwarf-2
QMAKE_CXXFLAGS_X86 += $$QMAKE_CFLAGS_X86
QMAKE_CXXFLAGS_X86_64 += $$QMAKE_CFLAGS_X86_64
QMAKE_CXXFLAGS_PPC += $$QMAKE_CFLAGS_PPC
QMAKE_CXXFLAGS_PPC_64 += $$QMAKE_CFLAGS_PPC_64
QMAKE_CXXFLAGS_DWARF2 += $$QMAKE_CFLAGS_DWARF2
QMAKE_OBJECTIVE_CFLAGS = $$QMAKE_CFLAGS
QMAKE_OBJECTIVE_CFLAGS_WARN_ON = $$QMAKE_CFLAGS_WARN_ON
QMAKE_OBJECTIVE_CFLAGS_WARN_OFF = $$QMAKE_CFLAGS_WARN_OFF
QMAKE_OBJECTIVE_CFLAGS_DEBUG = $$QMAKE_CFLAGS_DEBUG
QMAKE_OBJECTIVE_CFLAGS_RELEASE = $$QMAKE_CFLAGS_RELEASE
QMAKE_OBJECTIVE_CFLAGS_HIDESYMS = $$QMAKE_CXXFLAGS_HIDESYMS
QMAKE_OBJECTIVE_CFLAGS_X86 = $$QMAKE_CFLAGS_X86
QMAKE_OBJECTIVE_CFLAGS_X86_64 = $$QMAKE_CFLAGS_X86_64
QMAKE_OBJECTIVE_CFLAGS_PPC = $$QMAKE_CFLAGS_PPC
QMAKE_OBJECTIVE_CFLAGS_PPC_64 = $$QMAKE_CFLAGS_PPC_64
QMAKE_LFLAGS_X86 += $$QMAKE_CFLAGS_X86
QMAKE_LFLAGS_X86_64 += $$QMAKE_CFLAGS_X86_64
QMAKE_LFLAGS_PPC += $$QMAKE_CFLAGS_PPC
QMAKE_LFLAGS_PPC_64 += $$QMAKE_CFLAGS_PPC_64
QMAKE_LFLAGS += -headerpad_max_install_names
QMAKE_LFLAGS_SHLIB += -single_module -dynamiclib
QMAKE_LFLAGS_PLUGIN += $$QMAKE_LFLAGS_SHLIB
QMAKE_LFLAGS_INCREMENTAL += -undefined suppress -flat_namespace
QMAKE_LFLAGS_SONAME += -install_name$${LITERAL_WHITESPACE}
QMAKE_LFLAGS_VERSION += -current_version$${LITERAL_WHITESPACE}
QMAKE_LFLAGS_COMPAT_VERSION += -compatibility_version$${LITERAL_WHITESPACE}
#
# Qmake configuration for the GNU C++ compiler
#
# Before making changes to this file, please read the comment in
# gcc-base.conf, to make sure the change goes in the right place.
#
# To verify that your change has the desired effect on the final configuration
# you can use the manual test in tests/manual/mkspecs.
#
QMAKE_CC = /usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11-g++
QMAKE_LINK_C = $$QMAKE_CC -F/usr/x86_64-apple-darwin11/usr/Frameworks
QMAKE_LINK_C_SHLIB = $$QMAKE_CC -F/usr/x86_64-apple-darwin11/usr/Frameworks
QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += -O2 -g
QMAKE_CXX = /usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11-g++
QMAKE_LINK = /usr/x86_64-apple-darwin11/usr/bin/x86_64-apple-darwin11-g++ -F/usr/x86_64-apple-darwin11/usr/Frameworks
QMAKE_LINK_SHLIB = $$QMAKE_CXX -F/usr/x86_64-apple-darwin11/usr/Frameworks
QMAKE_CXXFLAGS_RELEASE_WITH_DEBUGINFO += $$QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO
QMAKE_PCH_OUTPUT_EXT = .gch
QMAKE_CFLAGS_PRECOMPILE = -x c-header -c ${QMAKE_PCH_INPUT} -o ${QMAKE_PCH_OUTPUT}
QMAKE_CFLAGS_USE_PRECOMPILE = -include ${QMAKE_PCH_OUTPUT_BASE}
QMAKE_CXXFLAGS_PRECOMPILE = -x c++-header -c ${QMAKE_PCH_INPUT} -o ${QMAKE_PCH_OUTPUT}
QMAKE_CXXFLAGS_USE_PRECOMPILE = $$QMAKE_CFLAGS_USE_PRECOMPILE
#include(../common/g++-macx.conf)
#
# Qmake configuration for the GNU C++ compiler on Mac OS X
#
# Before making changes to this file, please read the comment in
# gcc-base.conf, to make sure the change goes in the right place.
#
# To verify that your change has the desired effect on the final configuration
# you can use the manual test in tests/manual/mkspecs.
#
QMAKE_CFLAGS_RELEASE_WITH_DEBUGINFO += $$QMAKE_CFLAGS_DWARF2
QMAKE_CXXFLAGS_RELEASE_WITH_DEBUGINFO += $$QMAKE_CFLAGS_DWARF2
QMAKE_LFLAGS_RELEASE_WITH_DEBUGINFO += -g $$QMAKE_CFLAGS_DWARF2
QMAKE_LFLAGS_STATIC_LIB += -all_load
QMAKE_CFLAGS_X86_64 += -Xarch_x86_64 -mmacosx-version-min=10.5
QMAKE_CFLAGS_PPC_64 += -Xarch_ppc64 -mmacosx-version-min=10.5
QMAKE_CXXFLAGS_X86_64 = $$QMAKE_CFLAGS_X86_64
QMAKE_CXXFLAGS_PPC_64 = $$QMAKE_CFLAGS_PPC_64
QMAKE_OBJECTIVE_CFLAGS_X86_64 = $$QMAKE_CFLAGS_X86_64
QMAKE_OBJECTIVE_CFLAGS_PPC_64 = $$QMAKE_CFLAGS_PPC_64
QMAKE_LFLAGS_X86_64 = $$QMAKE_CFLAGS_X86_64
QMAKE_LFLAGS_PPC_64 = $$QMAKE_CFLAGS_PPC_64
QMAKE_OBJCFLAGS_PRECOMPILE = -x objective-c-header -c ${QMAKE_PCH_INPUT} -o ${QMAKE_PCH_OUTPUT}
QMAKE_OBJCFLAGS_USE_PRECOMPILE = $$QMAKE_CFLAGS_USE_PRECOMPILE
QMAKE_OBJCXXFLAGS_PRECOMPILE = -x objective-c++-header -c ${QMAKE_PCH_INPUT} -o ${QMAKE_PCH_OUTPUT}
QMAKE_OBJCXXFLAGS_USE_PRECOMPILE = $$QMAKE_CFLAGS_USE_PRECOMPILE
load(qt_config)
Помните, в Mac OS X для линковки с библиотекой .a-файлы не нужны.
Настройка QtCreator
Сначала нужно добавить в список все установленные компиляторы. Инструменты — Параметры — Сборка и запуск — Инструментарии:
QtCreator обычно нормально определяет ABI, но лучше перепроверить. Так же можно заметить, что системный x64 GCC в linux умеет генерировать и 32-битные приложения. Однако это не отменяет того, что также необходимы 32-битные версии библиотек.
После компиляторов можно добавить профили Qt:
Вот при добавлении профиля и пригодятся собранные ранее qmake, uic, moc, rcc, ведь нужно выбрать директорию с qmake. Жёлтый значок с восклицательным знаком слева от профиля означает warning, но QtCreator может использовать такой профиль Qt. А вот если значок красный, то профиль нерабочий. Такое может случиться при неправильной структуре каталогов. Или если удалить директорию, в которой компилировали Qt.
Следующие настройки нужно делать в каждом создаваемом проекте.
Для добавления конкретного профиля Qt надо при активном проекте зайти на вкладку «Проекты» (Ctrl+5):
По умолчанию в списке «Изменить конфигурацию сборки» есть только системный профиль Qt. Зато в списке кнопки «Добавить» есть все профили Qt, добавленные в параметры сборки.
В основных настройках сборки необходимо проверить пару библиотека-компилятор. Чтоб и то и другое было от одной и той же операционной системы.
Этапы сборки «qmake» и «Сборка» QtCreator ставит по умолчанию. А вот особые этапы «upx» и «dmgbuild» я добавил вручную для своего проекта. Этап «upx» выполняется каждый раз при нажатии на кнопку «Собрать проект». Однако если исполняемый файл не был изменён, то upx вернёт ошибку, что файл им уже обработан. В случае ошибки следующий этап не вызывается, т.е. dmg-файл обновится только если upx отработал успешно.
Для работы этапа upx он должен быть установлен в системе. Однако даже работая в linux-окружении и поставленный из пакетного менеджера upx умеет ужимать приложения: linux32/64, win32, macos32/64. Далеко не для всех проектов upx-сжатие реально нужно, этап показан скорее для примера.
Для этапа «dmgbuild» я воспользовался скриптом make_dmg. Ему нужны права root, поэтому добавил скрипт в файл /etc/sudoers
Изменения в проектном файле и использование сторонних библиотек
В моём проекте используется libusb, а это далеко не часть Qt. Также необходимо было включить платформенно-зависимую реализацию HID. В проектный файл были добавлены строки:
macx {
INCLUDEPATH += $$PWD/libusbx/
SOURCES += BootLoader/HIDAPI/mac/hid.c
LIBS += -framework IOKit -framework CoreFoundation -lusb-1.0
ICON = AqPicFlash.icns
}
win32: {
INCLUDEPATH += $$PWD/libusbx/
LIBS += -lsetupapi -lole32
SOURCES += BootLoader/HIDAPI/windows/hid.cpp
RC_FILE = WinIcon.rc
}
win32: !win64-x-g++ {
LIBS += -L$$PWD/libusbx/ -lusb-1.0-32.dll
}
win64-x-g++ {
LIBS += -L$$PWD/libusbx/ -lusb-1.0-64.dll
}
unix: !macx {
CONFIG += link_pkgconfig
PKGCONFIG += libusb-1.0
SOURCES += BootLoader/HIDAPI/linux/hid-libusb.c
}
В Mac OS X и Linux линкуемся с системной libusb, в Windows в зависимости от разрядности линкуемся с libusb-1.0-32.dll.a или libusb-1.0-64.dll.a. Помним, что .a-файл может быть переименован, но зависеть приложение всё-равно будет от libusb-1.0.dll. В Linux параметры для libusb берём через системную утилиту pkgconfig. Кроме libusb подключаем для каждой операционной системы необходимые системные библиотеки и иконки.
Удобно разнести итоговые файлы для разных операционных систем по директориям. Сделать это можно так:
macx {
DESTDIR = mac
OBJECTS_DIR = mac
MOC_DIR = mac
UI_DIR = mac
RCC_DIR = mac
}
unix: !macx {
DESTDIR = linux
OBJECTS_DIR = linux
MOC_DIR = linux
UI_DIR = linux
RCC_DIR = linux
}
win32 {
DESTDIR = windows/release
OBJECTS_DIR = windows
MOC_DIR = windows
UI_DIR = windows
RCC_DIR = windows
}
win64-x-g++ {
DESTDIR = win64/release
OBJECTS_DIR = win64
MOC_DIR = win64
UI_DIR = win64
RCC_DIR = win64
}
Цель win64-x-g++ относится к win32, однако в проектном файле идёт последней и переписывает настройки.
Результат
Теперь чтобы собрать приложение под ту или иную операционную систему достаточно выбрать тип сборки, как показано на самом первом скриншоте, и нажать «Собрать проект»
- Introduction
-
Why?
- Speed
- Cost
- Containers + k8s
-
Rejected Strategies
- Using x86_64-pc-windows-gnu
- Using wine to run the MSVC toolchain
-
How?
- Prerequisites
- 1. Setup toolchain(s)
- 2. Acquire Rust std lib
- 3. Acquire CRT and Windows 10 SDK
- 4. Override cc defaults
- 5. Profit
-
Bonus: Headless testing
- 1. Install
- 2. Specify runner
- 3. Test
- Final image definition
-
Common issues
- CMake
- MASM
- Compiler Target Confusion
- Conclusion
Introduction
Last November I added a new job to our CI to cross compile our project for x86_64-pc-windows-msvc
from an x86_64-unknown-linux-gnu
host. I had wanted to blog about that at the time but never got around to it, but after making some changes and improvements last month to this, in addition to writing a new utility, I figured now was as good of a time as any to share some knowledge in this area for those who might be interested.
Why?
Before we get started with the How, I want to talk about why one might want to do this in the first place, as natively targeting Windows is a «known quantity» with the least amount of surprise. While there are reasons beyond the following, my primary use case for why I want to do cross compilation to Windows is our Continuous Delivery pipeline for my main project at Embark.
Speed
It’s fairly common knowledge that, generally speaking, Linux is faster than Windows on equivalent hardware. From faster file I/O to better utilization of high core count machines, and faster process and thread creation, many operations done in a typical CI job such as compilation and linking tend to be faster on Linux. And since I am lazy, I’ll let another blog post about cross compiling Firefox from Linux to Windows actually present some numbers in defense of this assertion.
Cost
Though we’re now running a Windows VM in our on-premise data center for our normal Windows CD jobs, we actually used to run it in GCP. It was 1 VM with a modest 32 CPU count, but the licensing costs (Windows Server is licensed by core) alone accounted for >20% of our total costs for this particular GCP project.
While this single VM is not a huge deal relative to the total costs of our project, it’s still a budget item that provides no substantive value, and on principle I’d rather have more/better CPUs, RAM, disk, or GPUs, that provide immediate concrete value in our CI, or just for local development.
Containers + k8s
This one is probably the most subjective, so strap in!
While fast CI is a high priority, it really doesn’t matter how fast it is if it gives unreliable results. Since I am the (mostly) sole maintainer, (which yes, we’re trying to fix) for our CD pipeline in a team of almost 40 people, my goal early on was to get it into a reliably working state that I could easily maintain with a minimal amount of my time, since I have other, more fun, things to do.
The primary way I did this was to build buildkite-jobify (we use Buildkite as our CI provider). This is just a small service that spawns Kubernetes (k8s) jobs for each of the CI jobs we run on Linux, based on configuration from the repo itself.
This has a few advantages and disadvantages over a more typical VM approach, which we use for x86_64-pc-windows-msvc
(for now?), x86_64-apple-darwin
, and aarch64-apple-darwin
.
Pros
- Consistency — Every job run from the same container image has the exact same starting environment.
- Versioned — The image definitions are part of our monorepo, as well as the k8s job descriptions, so we get atomic updates of the environment CI jobs execute in with the code itself. This also makes rollbacks trivial if needed.
- Scalability — Scaling a k8s cluster up or down is fairly easily (especially in eg GKE, because $) as long as you have the compute resources. k8s also makes it easy to specify resource requests so that individual jobs can dynamically spin up on the most appropriate node at the time based on the other workloads currently running on the cluster.
- Movability — Since k8s is just running containers, it’s trivial to move build jobs between different clusters, for example in our case, from GKE to our on-premise cluster.
Cons
- Clean builds — Clean builds are quite slow compared to incremental builds, however we mitigate this by using cargo-fetcher for faster crate fetching and sccache for compiler output caching.
- Startup times — Changing the image used for a build job means that every k8s node that runs an image it doesn’t have needs to pull it before running. For example, the pull can take up to almost 2m for our
aarch64-linux-android
which is by far our largest image at almost 3GiB (the Android NDK/SDK are incredibly bloated). However, this is generally a one time cost per image per node and we don’t update images so often that it is actually a problem in practice.
Rejected Strategies
Before we get into the how I just wanted to show two other strategies that could be used for cross compilation that you might want to consider if your needs are different than ours.
Using x86_64-pc-windows-gnu
To be honest, I rejected this one pretty much immediately simply because the gnu
environment is not the «native» msvc
environment for Windows. Targeting x86_64-pc-windows-gnu
would not be representative for actual builds used by users, and it would be different from the local builds built by developers on Windows, which made it an unappealing option. That being said, generally speaking, Rust crates tend to support x86_64-pc-windows-gnu
fairly well, which as we’ll see later is a good thing due to my chosen strategy.
Using wine to run the MSVC toolchain
I briefly considered using wine to run the various components of the MSVC compiler toolchain, as that would be the most accurate way to match the native compilation for x86_64-pc-windows-msvc
. However, we already use LLD when linking on Windows since it is vastly faster than the MSVC linker, so why not just replace the rest of the toolchain while we’re at it? 😉 This kind of contradicts the reasons stated in x86_64-pc-windows-gnu
since we’d be changing to a completely different compiler with different codegen, but this tradeoff is actually ok with me for a couple of reasons.
The first reason is that the driving force behind clang-cl
, lld-link
, and the other parts of LLVM replacing the MSVC toolchain, is so that Chrome can be built with LLVM for all of their target platforms. The size of the Chrome project dwarfs the amount of C/C++ code in our project by a huge margin, and (I assume) includes far more…advanced…C++ code than we depend on, so the risk of mis-compilation or other issues compared to cl.exe seems reasonably low.
And secondly, we’re actively trying to get rid of C/C++ dependencies as the Rust ecosystem matures and provides its own versions of C/C++ libraries we use. For example, at the time of this writing, we use roughly 800k lines of C/C++ code, a large portion of which comes from Physx, which we will, hopefully, be able to replace in the future with something like rapier.
How?
Ok, now that I’ve laid out some reasons why you might want to consider cross compilation to Windows from Linux, let’s see how we can actually do it! I’ll be constructing a container image (in Dockerfile format) as we go that can be used to compile a Rust program. If you’re only targeting C/C++ the broad strokes of this strategy will still be relevant, you’ll just have a tougher time of it because…well, C/C++.
The strategy I chose is to use clang, which, like most compilers based off of LLVM (including rustc), is a native cross compiler, to compile any C/C++ code and assembly. Specifically this means using clang-cl
and lld-link
so that we, generally, don’t need to modify any C/C++ code to take cross compilation into account.
Prerequisites
If you want to follow along at home, you’ll need to be on Linux (though WSL might work?) with something that can build container images, like docker
or podman
.
1. Setup toolchain(s)
First thing we need are the actual toolchains needed to compile and link a full Rust project.
# We'll just use the official Rust image rather than build our own from scratch
FROM docker.io/library/rust:1.54.0-slim-bullseye
ENV KEYRINGS /usr/local/share/keyrings
RUN set -eux; \
mkdir -p $KEYRINGS; \
apt-get update && apt-get install -y gpg curl; \
# clang/lld/llvm
curl --fail https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor > $KEYRINGS/llvm.gpg; \
echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-13 main" > /etc/apt/sources.list.d/llvm.list;
RUN set -eux; \
# Skipping all of the "recommended" cruft reduces total images size by ~300MiB
apt-get update && apt-get install --no-install-recommends -y \
clang-13 \
# llvm-ar
llvm-13 \
lld-13 \
# We're using this in step 3
tar; \
# ensure that clang/clang++ are callable directly
ln -s clang-13 /usr/bin/clang && ln -s clang /usr/bin/clang++ && ln -s lld-13 /usr/bin/ld.lld; \
# We also need to setup symlinks ourselves for the MSVC shims because they aren't in the debian packages
ln -s clang-13 /usr/bin/clang-cl && ln -s llvm-ar-13 /usr/bin/llvm-lib && ln -s lld-link-13 /usr/bin/lld-link; \
# Verify the symlinks are correct
clang++ -v; \
ld.lld -v; \
# Doesn't have an actual -v/--version flag, but it still exits with 0
llvm-lib -v; \
clang-cl -v; \
lld-link --version; \
# Use clang instead of gcc when compiling binaries targeting the host (eg proc macros, build files)
update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100; \
update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++ 100; \
apt-get remove -y --auto-remove; \
rm -rf /var/lib/apt/lists/*;
2. Acquire Rust std lib
By default, rustup only installs the native host target of x86_64-unknown-linux-gnu
, which we still need to compile build scripts and procedural macros, but since we’re cross compiling we need to add the x86_64-pc-windows-msvc
target as well to get the Rust std library. We could also build the standard library ourselves, but that would mean requiring nightly and taking time to compile something that we can just download instead.
# Retrieve the std lib for the target
RUN rustup target add x86_64-pc-windows-msvc
3. Acquire CRT and Windows 10 SDK
In all likelihood, you’ll need the MSVCRT and Windows 10 SDK to compile and link most projects that target Windows. This is problematic because the official way to install them is, frankly, atrocious, in addition to not being redistributable (so no one but Microsoft can provide, say, a tarball with the needed files).
But really, our needs are relatively simple compared to a normal developer on Windows, as we just need the headers and libraries from the typical VS installation. We could if we wanted use the Visual Studio Build Tools from a Windows machine, or if we were feeling adventurous try to get it running under wine (warning: I briefly tried this but it requires .NET shenanigans that at the time were broken under wine) and then create our own tarball with the needed files, but that feels too slow and tedious.
So instead, I just took inspiration from other projects and created my own xwin
program to download, decompress, and repackage the MSVCRT and Windows SDK into a form appropriate for cross compilation. This has several advantages over using the official installation methods.
- No cruft — Since this program is tailored specifically to getting only the files needed for compiling and linking we skip a ton of cruft, some of which you can opt out of, but some of which you cannot with the official installers. For example, even if you never target
aarch64-pc-windows-msvc
, you will still get all of the libraries needed for it. - Faster — In addition to not even downloading stuff we don’t need, all download, decompression, and disk writes are done in parallel. On my home machine with ~11.7MiB download speeds and a Ryzen 3900X I can download, decompress, and «install» the MSVCRT and Windows SDK in about 27 seconds.
- Fixups — While the CRT is generally fine, the Windows SDK headers and libraries are an absolute mess of casing (seriously, what maniac thought it would be a good idea to capitalize the
l
in.lib
!?), making them fairly useless on a case-sensitive file system. Rather than rely on using a case-insensitive file system on Linux,xwin
just adds symlinks as needed, so eg.windows.h
->Windows.h
,kernel32.lib
->kernel32.Lib
etc.
We have two basic options for how we could get the CRT and SDK, either run xwin
directly during image building, or run it separately and tarball the files and upload them to something like GCS and just retrieve them as needed in the future. We’ll just use it directly while building the image since that’s easier.
RUN set -eux; \
xwin_version="0.1.1"; \
xwin_prefix="xwin-$xwin_version-x86_64-unknown-linux-musl"; \
# Install xwin to cargo/bin via github release. Note you could also just use `cargo install xwin`.
curl --fail -L https://github.com/Jake-Shadle/xwin/releases/download/$xwin_version/$xwin_prefix.tar.gz | tar -xzv -C /usr/local/cargo/bin --strip-components=1 $xwin_prefix/xwin; \
# Splat the CRT and SDK files to /xwin/crt and /xwin/sdk respectively
xwin --accept-license 1 splat --output /xwin; \
# Remove unneeded files to reduce image size
rm -rf .xwin-cache /usr/local/cargo/bin/xwin;
4. Override cc
defaults
cc
is the Rust ecosystem’s primary (we’ll get to the most common exception later) way to compile C/C++ code for use in Rust crates. By default it will try and use cl.exe
and friends when targeting the msvc
environment, but since we don’t have that, we need to inform it what we actually want it to use instead. We also need to provide additional compiler options to clang-cl
to avoid common problems when compiling code that assumes that targeting x86_64-pc-windows-msvc
can only be done with the MSVC toolchain.
We also need to tell lld where to search for libraries. We could place the libs in one of the default lib directories lld will search in, but that would mean changing the layout of the CRT and SDK library directories, so it’s generally easier to just specify them explicitly instead. We use RUSTFLAGS
for this, which does mean that if you are specifying things like -Ctarget-feature=+crt-static
in .cargo/config.toml you will need to reapply them in the container image either during image build or by overriding the environment at runtime to get everything working.
# Note that we're using the full target triple for each variable instead of the
# simple CC/CXX/AR shorthands to avoid issues when compiling any C/C++ code for
# build dependencies that need to compile and execute in the host environment
ENV CC_x86_64_pc_windows_msvc="clang-cl" \
CXX_x86_64_pc_windows_msvc="clang-cl" \
AR_x86_64_pc_windows_msvc="llvm-lib" \
# Note that we only disable unused-command-line-argument here since clang-cl
# doesn't implement all of the options supported by cl, but the ones it doesn't
# are _generally_ not interesting.
CL_FLAGS="-Wno-unused-command-line-argument -fuse-ld=lld-link /imsvc/xwin/crt/include /imsvc/xwin/sdk/include/ucrt /imsvc/xwin/sdk/include/um /imsvc/xwin/sdk/include/shared" \
RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64"
# These are separate since docker/podman won't transform environment variables defined in the same ENV block
ENV CFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS" \
CXXFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS"
As already noted above in the reasons why we went this route, we use lld-link
even when compiling on Windows hosts due to its superior speed over link.exe
. So for our project we just set it in our .cargo/config.toml so it’s used regardless of host platform.
[target.x86_64-pc-windows-msvc]
linker = "lld-link" # Note the lack of extension, which means it will work on both Windows and unix style platforms
If you don’t already use lld-link
when targeting Windows, you’ll need to add an additional environment variable so that cargo knows what linker to use, otherwise it will default to link.exe
.
ENV CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER=lld-link
5. Profit
Building a container image from this Dockerfile spec should allow you to run containers capable of compiling and linking a Rust project targeting Windows, including any C/C++ code that might be used as a dependency….mostly.
cargo build --target x86_64-pc-windows-msvc
Bonus: Headless testing
Of course, though compiling and linking a Rust project on Linux is one thing, our CD pipeline also needs to run tests! I’ve mentioned wine
several times so far as a way you could run Windows programs such as the MSVC toolchain under Linux, so naturally, that’s what we’re going to do with our test executables.
1. Install
Debian tends to update packages at a glacial pace, as in the case of wine
where the 5.0.3
version packaged in bullseye is about 9 months out of date. In this case, it actually matters, as some crates, for example mio, rely on relatively recent wine releases to implement features or fix bugs. Since mio is a foundational crate in the Rust ecosystem, we’ll be installing wine’s staging version, which is 6.15 at the time of this writing.
RUN set -eux; \
curl --fail https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor > $KEYRINGS/winehq.gpg; \
echo "deb [signed-by=$KEYRINGS/winehq.gpg] https://dl.winehq.org/wine-builds/debian/ bullseye main" > /etc/apt/sources.list.d/winehq.list; \
# The way the debian package works requires that we add x86 support, even
# though we are only going be running x86_64 executables. We could also
# build from source, but that is out of scope.
dpkg --add-architecture i386; \
apt-get update && apt-get install --no-install-recommends -y winehq-staging; \
apt-get remove -y --auto-remove; \
rm -rf /var/lib/apt/lists/*;
2. Specify runner
By default, cargo
will attempt to run test binaries natively, but luckily this behavior is trivial to override by supplying a single environment variable to tell cargo how it should run each test binary. This method is also how you can run tests for wasm32-unknown-unknown
locally via a wasm runtime like wasmtime. 🙂
ENV \
# wine can be quite spammy with log messages and they're generally uninteresting
WINEDEBUG="-all" \
# Use wine to run test executables
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUNNER="wine"
3. Test
Now we can compile, link, and test Windows executables with just a standard cargo invocation.
cargo test --target x86_64-pc-windows-msvc
Final image definition
Putting it all together, here is an image definition that should allow you to cross compile to Windows and run headless tests, without needing a Windows install at any step.
# We'll just use the official Rust image rather than build our own from scratch
FROM docker.io/library/rust:1.54.0-slim-bullseye
ENV KEYRINGS /usr/local/share/keyrings
RUN set -eux; \
mkdir -p $KEYRINGS; \
apt-get update && apt-get install -y gpg curl; \
# clang/lld/llvm
curl --fail https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor > $KEYRINGS/llvm.gpg; \
# wine
curl --fail https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor > $KEYRINGS/winehq.gpg; \
echo "deb [signed-by=$KEYRINGS/llvm.gpg] http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-13 main" > /etc/apt/sources.list.d/llvm.list; \
echo "deb [signed-by=$KEYRINGS/winehq.gpg] https://dl.winehq.org/wine-builds/debian/ bullseye main" > /etc/apt/sources.list.d/winehq.list;
RUN set -eux; \
dpkg --add-architecture i386; \
# Skipping all of the "recommended" cruft reduces total images size by ~300MiB
apt-get update && apt-get install --no-install-recommends -y \
clang-13 \
# llvm-ar
llvm-13 \
lld-13 \
# get a recent wine so we can run tests
winehq-staging \
# Unpack xwin
tar; \
# ensure that clang/clang++ are callable directly
ln -s clang-13 /usr/bin/clang && ln -s clang /usr/bin/clang++ && ln -s lld-13 /usr/bin/ld.lld; \
# We also need to setup symlinks ourselves for the MSVC shims because they aren't in the debian packages
ln -s clang-13 /usr/bin/clang-cl && ln -s llvm-ar-13 /usr/bin/llvm-lib && ln -s lld-link-13 /usr/bin/lld-link; \
# Verify the symlinks are correct
clang++ -v; \
ld.lld -v; \
# Doesn't have an actual -v/--version flag, but it still exits with 0
llvm-lib -v; \
clang-cl -v; \
lld-link --version; \
# Use clang instead of gcc when compiling binaries targeting the host (eg proc macros, build files)
update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100; \
update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang++ 100; \
apt-get remove -y --auto-remove; \
rm -rf /var/lib/apt/lists/*;
# Retrieve the std lib for the target
RUN rustup target add x86_64-pc-windows-msvc
RUN set -eux; \
xwin_version="0.1.1"; \
xwin_prefix="xwin-$xwin_version-x86_64-unknown-linux-musl"; \
# Install xwin to cargo/bin via github release. Note you could also just use `cargo install xwin`.
curl --fail -L https://github.com/Jake-Shadle/xwin/releases/download/$xwin_version/$xwin_prefix.tar.gz | tar -xzv -C /usr/local/cargo/bin --strip-components=1 $xwin_prefix/xwin; \
# Splat the CRT and SDK files to /xwin/crt and /xwin/sdk respectively
xwin --accept-license 1 splat --output /xwin; \
# Remove unneeded files to reduce image size
rm -rf .xwin-cache /usr/local/cargo/bin/xwin;
# Note that we're using the full target triple for each variable instead of the
# simple CC/CXX/AR shorthands to avoid issues when compiling any C/C++ code for
# build dependencies that need to compile and execute in the host environment
ENV CC_x86_64_pc_windows_msvc="clang-cl" \
CXX_x86_64_pc_windows_msvc="clang-cl" \
AR_x86_64_pc_windows_msvc="llvm-lib" \
# wine can be quite spammy with log messages and they're generally uninteresting
WINEDEBUG="-all" \
# Use wine to run test executables
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUNNER="wine" \
# Note that we only disable unused-command-line-argument here since clang-cl
# doesn't implement all of the options supported by cl, but the ones it doesn't
# are _generally_ not interesting.
CL_FLAGS="-Wno-unused-command-line-argument -fuse-ld=lld-link /imsvc/xwin/crt/include /imsvc/xwin/sdk/include/ucrt /imsvc/xwin/sdk/include/um /imsvc/xwin/sdk/include/shared" \
# Let cargo know what linker to invoke if you haven't already specified it
# in a .cargo/config.toml file
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="lld-link" \
RUSTFLAGS="-Lnative=/xwin/crt/lib/x86_64 -Lnative=/xwin/sdk/lib/um/x86_64 -Lnative=/xwin/sdk/lib/ucrt/x86_64"
# These are separate since docker/podman won't transform environment variables defined in the same ENV block
ENV CFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS" \
CXXFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS"
# Run wineboot just to setup the default WINEPREFIX so we don't do it every
# container run
RUN wine wineboot --init
Here is a gist with the same dockerfile, and an example of how you can build it. I’m using podman
here, but docker
should also work.
curl --fail -L -o xwin.dockerfile https://gist.githubusercontent.com/Jake-Shadle/542dfa000a37c4d3c216c976e0fbb973/raw/bf6cff2bd4ad776d3def8520adb5a5c657140a9f/xwin.dockerfile
podman -t xwin -f xwin.dockerfile .
Common issues
Unfortunately, everything is not sunshine and unicorns where cross compiling is concerned, but all of them are solvable, at least in principle.
CMake
It’s not exactly a secret that I am not a fan of CMake. Inexplicably (to me at least), CMake has become the default way to configure and build open source C/C++ code. As I basically only use Rust now, this would normally not bother me, however, many Rust crates still wrap C/C++ libraries, and due to the ubiquitous nature of CMake, a significant minority of those crates just directly use cmake
(or worse, direct invocation) to let CMake drive the building of the underlying C/C++ code. This is great excusable when it works, however, in my experience, CMake scripts tend to be a house of cards that falls down at the slightest deviation from the «one true path» intended by the author(s) of the CMake scripts and cross compiling to Windows is a big deviation that not only knocks down the cards but also sets them on fire.
The simplest and most effective solution to CMake issues is to replace it with cc. In some cases like Physx or spirv-tools that can be a fair amount of work, but in many cases it’s not much. The benefits of course extend beyond just making cross compilation easier, it also gets rid of the CMake installation dependency, as well as just making it easier for outside contributors to understand how the C/C++ code is built, since they don’t need to actually crawl through some project’s CMake scripts trying to figure out what the hell is going on, they can just look at the build.rs file instead.
MASM
Unfortunately, we don’t need to worry about just Rust, C, and C++ in some Rust projects, there are also a small number of crates here and there which also use assembly. While in the future this will be able to be handled natively in rustc, we have to deal with the present, and unfortunately the present contains multiple assemblers with incompatible syntax. In the Microsoft toolchain, ml64.exe
assembles MASM, and while there are ongoing efforts to get LLVM to assemble MASM via llvm-ml, the fact that the last update I can find is from October 2020, and there is no project page for llvm-ml like there are for other llvm tools, tells me I might be wasting my time trying to get it to work for all assembly that we need to compile.
Luckily, there is a fairly easy workaround for this gap until llvm-ml becomes more mature. Even though we aren’t targeting x86_64-pc-windows-gnu
for the reasons stated above, the few projects that we use that use assembly generally do have both a MASM version as well as a GAS version so that people who want to can target x86_64-pc-windows-gnu
. However, since cross compilation to Windows from a non-Windows platform is fairly rare, you’ll often need to provide PRs to projects to fix up assumptions made about the target and host being the same. And unfortunately, this niche case also comes with a bit of maintenance burden that maintainers of a project might be uncomfortable with taking since they can’t easily provide coverage, which is a totally fair reason to not merge such a PR.
Compiler Target Confusion
This one is the rarest of all, at least anecdotally, as I only encountered this kind of issue in Physx. Basically, the issue boils down to a project assuming that Windows == MSVC toolchain and Clang != Windows, which can result in (typically) preprocessor logic errors.
For example, here we have a clang specific warning being disabled for a single function, except it’s fenced by both using clang as the compiler as well as targeting Linux, which means targeting Windows won’t disable the warning, and if warnings are treated as errors, we’ll get a compile failure.
@see PxCreateFoundation()
*/
#if PX_CLANG
-#if PX_LINUX
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wreturn-type-c-linkage"
-#endif // PX_LINUX
+ #pragma clang diagnostic push
+ #pragma clang diagnostic ignored "-Wreturn-type-c-linkage"
#endif // PX_CLANG
PX_C_EXPORT PX_FOUNDATION_API physx::PxFoundation& PX_CALL_CONV PxGetFoundation();
#if PX_CLANG
-#if PX_LINUX
-#pragma clang diagnostic pop
-#endif // PX_LINUX
+ #pragma clang diagnostic pop
#endif // PX_CLANG
namespace physx
For the most part these kinds of problems won’t occur since clang-cl
very effectively masquerades as cl, including setting predefined macros like _MSC_VER
and others so that a vast majority if C/C++ code that targets Windows «just works».
Conclusion
And there you have it, a practical summary on how to cross compile Rust projects for x86_64-pc-windows
from a Linux host or container, I hope you’ve found at least some of this information useful!
As for next steps, my team is rapidly improving our renderer built on top of Vulkan and rust-gpu, but our non-software rasterization testing is mostly limited to a few basic tests on our Mac VMs since they are the only ones with GPUs. While I am curious about getting rendering tests working for Windows under wine, I am also quite hesitant. While wine and Proton have been making big steps and support a large amount of Windows games, we are using fairly bleeding edge parts of Vulkan like ray tracing, and running rendering tests on Linux means you’re now running on top of the Linux GPU drivers rather than the Windows ones, making test results fairly suspect on whether they are actually detecting issues that might be present in a native Windows environment. It could still be fun though!
While it might seem like I hate Windows due to the content of this post, that’s very much not the case. I am comfortable in Windows having used it for 20+ years or so both personally and professionally, I just prefer Linux these days, especially for automated infrastructure like CD which this post is geared towards…
..however, the same cannot be said for Apple/Macs, as I do hate them with the fiery passion of a thousand suns. Maintaining «automated» Mac machines is one of the most deeply unpleasant experiences of my career, one I wouldn’t wish on my worst enemy, but since Macs are one of our primary targets (thankfully iOS is off the table due to «reasons»), we do need to build and test it along with our other targets. So maybe cross compiling to Macs will be in a future post. 😅
Компиляция для Windows из под Linux с загрузкой динамической библиотеки
Компиляция программного кода для различных операционных систем может быть сложной задачей. Особенно если речь идет о компиляции кода под Windows, ведь Windows и Linux имеют различные наборы библиотек и разные наборы инструментов разработки.
Однако, современные системы разработки предоставляют набор инструментов и технологий, которые позволяют с легкостью компилировать и собирать программы под различные платформы. В данной статье мы рассмотрим процесс компиляции программы для Windows из под Linux с загрузкой динамической библиотеки.
1. Установка необходимых инструментов
Перед тем, как начать процесс компиляции, нам необходимо установить необходимые инструменты на нашей Linux-машине. В качестве компилятора мы будем использовать GCC — один из самых популярных и распространенных компиляторов для C и C++. Он широко доступен в большинстве дистрибутивов Linux. Для установки используем команду
sudo apt-get install build-essential
Также нам потребуется установить MinGW — набор инструментов разработки для Windows. Он включает компилятор GCC для Windows и другие необходимые утилиты. Для установки MinGW выполните следующие команды:
sudo apt-get install mingw-w64
2. Подготовка исходного кода
Прежде чем приступить к самой компиляции, необходимо подготовить исходный код программы. Убедитесь, что весь код, который вы будете компилировать, находится в одной папке. Если в вашем проекте присутствуют внешние библиотеки, которые требуется загрузить динамически, убедитесь, что они присутствуют в вашей папке с исходным кодом.
3. Создание скрипта сборки
Для упрощения процесса компиляции рекомендуется создать скрипт сборки. В данном случае мы будем использовать скрипт на языке bash. Создайте новый текстовый файл с расширением .sh (например, «build.sh») и откройте его в редакторе.
В скрипте сборки мы будем использовать утилиту make для автоматизации процесса компиляции. Make — это инструмент, предназначенный для построения программного кода из файлов исходного кода на основе описания зависимостей между ними. Вот пример простого скрипта сборки:
# Определение переменных CC=i686-w64-mingw32-gcc CXX=i686-w64-mingw32-g++ LIBS=-lmylibrary # Включение директив CFLAGS=-I/path/to/headers LDFLAGS=-L/path/to/libs # Цель по умолчанию default: $(CC) $(CFLAGS) $(LDFLAGS) -o myprogram.exe file1.c file2.c $(LIBS)
— В переменной CC мы указываем используемый компилятор для Windows — «i686-w64-mingw32-gcc». Аналогично в переменной CXX указываем компилятор для C++.
— LIBS — это переменная, в которой перечислены внешние библиотеки, которые необходимо включить в процесс компиляции программы (например, -lmylibrary, где mylibrary — имя вашей библиотеки).
— В CFLAGS и LDFLAGS указываются пути к заголовочным файлам и библиотекам соответственно. Замените «/path/to/headers» и «/path/to/libs» на актуальные пути на вашей системе.
— В конечной строке цели «default» происходит непосредственно компиляция программы. Замените «myprogram.exe» и «file1.c file2.c» на актуальные имена вашей программы и файлов исходного кода соответственно.
4. Запуск скрипта сборки
Скрипт сборки готов. Осталось только запустить его для запуска процесса компиляции. Откройте терминал Linux и перейдите в директорию с вашим скриптом сборки. Выполните команду:
chmod +x build.sh ./build.sh
— Первая команда «chmod +x build.sh» устанавливает разрешение на выполнение скрипта сборки.
— Вторая команда «./build.sh» запускает наш скрипт сборки.
5. Загрузка динамической библиотеки
Если в вашей программе присутствуют внешние динамические библиотеки, они также должны быть загружены при запуске программы. В случае Windows, копируйте все необходимые библиотеки в ту же папку, где находится скомпилированная программа (myprogram.exe), и убедитесь, что файлы библиотек имеют расширение .dll. Затем перенесите эту папку на компьютер с Windows и запустите программу. Все необходимые библиотеки будут загружены автоматически.
6. Проверка программы на Windows
Перенесите скомпилированную программу и необходимые библиотеки на компьютер с Windows и запустите программу. Проверьте, что программа работает корректно и загружает все необходимые библиотеки.
Компиляция программного кода для Windows из под Linux с загрузкой динамической библиотеки может быть сложным процессом, но с перечисленными выше шагами он может быть значительно упрощен. При этом необходимо учитывать различия в системах, и компиляторе, и наборе библиотек. Но в целом, правильно настроив процесс компиляции, вы сможете создавать программы, которые успешно работают на операционной системе Windows, используя вашу Linux-машину для разработки.