Время на прочтение19 мин
Количество просмотров6K
Эта история началась когда удобный для меня инструмент VSCode из за одного плагина на борту стал не совсем удобен. А именно, «natizyskunk.sftp» плагин, который позволяет подключится по протоколу (S)FTP к серверу, и вносить изменения на сервер, просто сохранив файл. Всё нечего, если бы настройки подключения к серверам не хранились в корне отдельно взятого проекта. Когда у тебя с десяток проектов, ещё можно потерпеть, но более 70 проектов и они все на разных серверах, то перенос и добавление в каждый из проектов актуальной настройки, например пароль, который периодически меняется, заставляет как минимум раздражённо искать подходящую настройку и с комбинацией клавиш Ctrl+C, Ctrl+V, менять устаревшие настройки или добавлять новые. В дальнейшем оказалось, что коллеги пользуются другими идентичными плагинами с названиями «liximomo.sftp», «doujinya.sftp-revived». Идентичность их на столько существенная, что разница заключается в паре символов из интересующего кода. Прочитав лицензионное соглашение на всех трёх плагинах и выяснив что лицензия позволяет вносить изменения в код без спроса. Пробравшись в файл плагина и так как они в VSCode написаны на JS, увидел обфусцированный JS код, состоящий из нескольких строк. Комментарии и большой объём кода без перехода на следующую строку.
Зная, что плагин создаёт файл с настройками в корне проекта в директории с названием .vscode, я решил начать с поиска этого слова в коде.
Видно, что происходит конкатенация значений переменной «r» названия этого каталога и значением (которое можно увидеть перед «t.CONFIG_PATH») константы «t.CONGIF_FILENAME» . Странно что при этом вызывается функция «join()» которая по стандарту js принимает только один аргумент. Если конечно переменная «r» не является массивом. Скорей всего в данном контексте реализация функции «join()» находится в задействованном объекте, не исключаю что цель этой реализации заключается в том чтобы сбить с толку читателя кода. Потому что стандартное объединение строк выглядит не страшно и очень похоже «string.concat(string1, string2, …, stringX)».
Теперь я уверен, что путь к файлу находится в константе объекта «CONFIG_PATH». В поиске найдено два объекта, первый уже мы видели, а во втором случае в объявленной функции, в качестве возвращаемого значения, происходит такая-же конкатенация как в прошлый раз.
Где потом эта функция вызывается искать не очень хочется, да и не думаю что понадобится. Ради эксперимента заменил значение переменной на полный путь до директории где будут храниться настройки подключения к серверам. После перезагрузки VSCode с помощью комбинации клавиш «Ctrl+Shift+p» откроется командная строка и запустив команду «>SFTP: Config» плагин создаст необходимый файл с настройками по заданному пути. Файл стал общим для всех проектов и работает как задумывалось! Это победа! Теперь надо поделится этим открытием с коллегами и избавить их от мучений.
Но я не я если не избавлю их от столь слегка не удобного способа корректировать путь к файлу. Подумав над выбором языка, для того чтобы не терять накопленный опыт, поскольку практики с ним уже нет, остановился на C++. Не простой но интересный путь, при условии что, на уже родных для меня PHP или Python задачу можно выполнить быстрее, проще и понятней, если C# то и графический интерфейс запилить без проблем (при условии если ОП будет Windows). Так как в организации мы работаем на операционной системе Windows, а дома я работаю под Linux, задача становится ещё интересней. Тем более опыта с крос-компиляцией у меня немного. Разведку на неизвестных мне языках, таких как Rust, Go, Ruby я оставлю на потом.
С этого момента хочу сразу предупредить: — я не профессиональный разработчик на C++, и изложенный ниже материал не стоит воспринимать как учебное пособие. В процессе разработки учишься чему-то новому, а документирование пройденного материала в статье, помогает закрепить знания.
Для компиляции программы под систему Windows потребуются установить компилятор GNU C++ для MinGW-w64. Установка в Ubuntu / Linux Mint / Debian выглядит так:
sudo apt update
sudo apt install g++-mingw-w64
Makefile выглядит так:
#Для Windowd 32bit
# CXX := i686-w64-mingw32-g++
#Для Windowd 64bit
# CXX := x86_64-w64-mingw32-g++
# Для Linux
CXX := g++
CXX_FLAGS := -Wall -Wextra -std=c++17 -ggdb
BIN := bin
SRC := src
INCLUDE := include
LIB := lib
ifeq ($(CXX),x86_64-w64-mingw32-g++)
DEFINE := WIN32=WIN32
EXECUTABLE := sftp_pat.exe
else ifeq ($(CXX),i686-w64-mingw32-g++)
DEFINE := WIN32=WIN32
EXECUTABLE := sftp_pat.exe
else ifeq ($(CXX),g++)
DEFINE := LINUX=LINUX
EXECUTABLE := sftp_patch
endif
LIBRARIES := -static-libstdc++ -static-libgcc -lstdc++fs
all: $(BIN)/$(EXECUTABLE)
run: clean all
clear
./$(BIN)/$(EXECUTABLE)
$(BIN)/$(EXECUTABLE): $(SRC)/*.cpp
$(CXX) $(CXX_FLAGS) -I $(INCLUDE) -L $(LIB) $^ -o $@ $(LIBRARIES) -D $(DEFINE)
clean:
-rm $(BIN)/*
Для выбора компилятора нужно раскомментировать нужный CXX и закомментировать остальные.
Теперь можно переходить к точке входа под названием main.cpp. Для начала стоит разбить задачу на ключевые моменты.
-
Определить пользователя и перейти к нему в home директорию
-
Найти нужный файл для правки и найти место правки
-
Получить от пользователя путь к новому месту хранения настроек и узнать какой плагин интересует пользователя
-
Внести изменения в файле
Всё просто. Я разделил задачи поровну на два объекта GetFileTarget и FileRewrite. Финальная версия main.cpp выглядит так:
/**
* @file main.cpp
* @author Tolsedum (tolsedum@gmail.com)
* @brief Entry point
* @version 1.0
* @date 2022-06-12
*
* @copyright Copyright (c) 2022
*
*/
#include "GetFileTarget.hpp"
#include "FileRewrite.hpp"
int main() {
try{
GetFileTarget getFileTarget;
// Определить пользователя и перейти к нему в home дирикторию
getFileTarget.jumpToDirectory();
// Для Windows задает режим преобразования файлов и выводит логотип. Для Linux выводит логотип.
getFileTarget.setMode();
// Найти нужный файл для правки и найти место правки
GenerelInformation fileDate = getFileTarget.getFilePosition();
FileRewrite fileRewrite(fileDate);
// Получить от пользователя путь к новому месту хранения настроек и узнать какой плагин интересует пользователя
fileRewrite.getSettingsPatch();
// Внести изменения в файле
fileRewrite.rewrite();
}catch(const my_char* error){
fn::printString(error);
}catch (my_stryng error) {
fn::printString(error);
}catch(const fs::filesystem_error& e){
fn::printString(e.what());
}catch(std::exception & e) {
fn::printString(e.what());
}
fn::pause();
return 0;
}
Для того чтобы подключать заголовочные файлы, определять функции, константы которые используются во всех объектах мне нравится держать один общий для всех объект. Его я назвал «GlobalIncluds».
/**
* @file GlobalIncluds.hpp
* @author Tolsedum (tolsedum@gmail.com)
* @brief Global include headers, var, structs and functins
* @version 0.1
* @date 2022-06-12
*
* @copyright Copyright (c) 2022
*
*/
#pragma once
#ifndef GLOBAL_INCLUDE
#define GLOBAL_INCLUDE "GLOBAL_INCLUDE"
#include <regex>
#include <iostream>
#include <string>
#include <string.h>
#include <fstream>
#include <experimental/filesystem>
#include <vector>
#ifdef WIN32
#include <direct.h>
#include <fcntl.h>
typedef wchar_t my_char;
typedef std::wstring my_stryng;
#else
#include "unistd.h"
typedef std::string my_stryng;
typedef char my_char;
#endif
namespace fs = std::experimental::filesystem;
// Структура для передачи данных от одного объекта другому
struct GenerelInformation{
public:
std::string tmp_file_name;
std::string file_path;
std::size_t pos_begin = 0;
std::size_t pos_end = 0;
short selected_number;
GenerelInformation(){};
~GenerelInformation(){};
};
namespace fn{
#ifdef WIN32
inline std::string wstringToString(std::wstring w_str_to_convert){
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> w_conv;
std::string ret_value = w_conv.to_bytes(w_str_to_convert);
return ret_value;
}
inline std::wstring stringToWstring(std::string str_to_convert){
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> w_conv;
std::wstring ret_value = w_conv.from_bytes(str_to_convert);
return ret_value;
}
#endif // WIN32
// ----------------------- getLineCin -----------------------
// Конвертация строк. Требуются только в Windows
inline void getLineCin(std::wstring &in_str){
std::wcin >> in_str;
}
inline void getLineCin(std::string &in_str){
std::getline(std::cin, in_str);
}
// ----------------------- getLineCin end -------------------
// ----------------------- printString -----------------------
// Универсальный метод для вывода одного сообщения
inline void printString(wchar_t str_to_print){
std::wcout << str_to_print <<std::endl;
}
inline void printString(char str_to_print){
std::cout << str_to_print <<std::endl;
}
inline void printString(wchar_t* str_to_print){
std::wcout << str_to_print <<std::endl;
}
inline void printString(char* str_to_print){
std::cout << str_to_print <<std::endl;
}
inline void printString(const wchar_t* str_to_print){
std::wcout << str_to_print <<std::endl;
}
inline void printString(const char* str_to_print){
std::cout << str_to_print <<std::endl;
}
inline void printString(std::wstring str_to_print){
std::wcout << str_to_print <<std::endl;
}
inline void printString(std::string str_to_print){
std::cout << str_to_print <<std::endl;
}
inline void printString(std::exception except){
#ifdef WIN32
std::wcout << except.what() <<std::endl;
#else
std::cout << except.what() <<std::endl;
#endif // WIN32
}
// ----------------------- printString end---------------------
// Пауза в конце программы
inline void pause(){
#ifdef WIN32
system("PAUSE");
#else
std::cout<< "Введите символ и нажмите enter" <<std::endl;
char d;
std::cin >> d;
#endif // WIN32
}
}
#endif // !GLOBAL_INCLUDE
Введу того что путь к файлу и сам файл пациент я обрабатываю в std::string, а пути к файлам в Windows содержат кириллицу и её в адекватном виде получить из cmd Windows можно только через std::wstring, пришлось делать функции конвертации.
Для вывода в консоль информации для пользователя, пришлось сделать перегрузку функции printString, так как после изменения режима вывода информации в UTF-16 работает только std::wcout. Про изменение режима чуть позже. За первую и вторую задачи, ответственность несёт объект GetFileTarget.
Заголовочный файл GetFileTarget:
/**
* @file GetFileTarget.hpp
* @author Tolsedum (tolsedum@gmail.com)
* @brief To go to the worcing dirictory
* @version 0.1
* @date 2022-06-12
*
* @copyright Copyright (c) 2022
*
*/
#pragma once
#ifndef GET_FILE_TARGET
#define GET_FILE_TARGET "GET_FILE_TARGET"
#include "GlobalIncluds.hpp"
class GetFileTarget{
private:
/** @brief Имя файла который надо менять*/
std::string name_file_target;
/** @brief Имя временного файла для сохранения в него информации*/
std::string tmp_file_name;
/** @brief Список плагинов в которых изменить путь*/
std::vector<std::string> use_plagin;
/** @brief Список строк для определения позиции начала правки*/
std::vector<std::string> search_begin;
/** @brief Номер плагина из списка use_plagin. Соответствует итератору vector*/
short selected_number;
/** @brief Предложение пользователю о выборе плагина*/
const my_char *ask_user_a_plagin;
/** @brief Сообщение о том что надо ввести число*/
const my_char *invalid_argument;
/** @brief Сообщение о ограничении вводимых чисел*/
const my_char *out_of_range;
/** @brief Сообщение о том что файл не может быть открыт*/
const my_char *file_is_not_open;
/** @brief Сообщение о том что файл не найден*/
const my_char *file_is_not_found;
/** @brief Путь к каталогу с файлом*/
const char *profile_patch;
void getIntCin();
public:
GetFileTarget();
/** Определить пользователя и перейти к нему в home директорию*/
void jumpToDirectory();
/** Найти нужный файл для правки и найти место правки*/
GenerelInformation getFilePosition();
// Для Windows задает режим преобразования файлов и выводит логотип. Для Linux выводит логотип.
void setMode();
};
#endif // !GET_FILE_TARGET
В конструкторе класса GetFileTarget инициализация переменных организована для каждой ОС в соответствии с типом.
GetFileTarget::GetFileTarget(){
tmp_file_name = "tmp_file.tmp";
use_plagin = {
"natizyskunk.sftp",
"liximomo.sftp",
"doujinya.sftp-revived"
};
search_begin = {
"function d(e){return Object.assign({},h,e)}", // for natizyskunk.sftp
"function p(e){return Object.assign({},h,e)}", // for liximomo.sftp
"function d(e){return Object.assign({},h,e)}" // for doujinya.sftp-revived
};
#ifdef WIN32
file_is_not_found = L"Файл не найден";
file_is_not_open = L"Файл не может быть открыт";
out_of_range = L"Число не должно быть больше 3 и меньше 1";
invalid_argument = L"Введите число: ";
ask_user_a_plagin = L"\
Каким плагином вы пользуетесь: \n \
1) natizyskunk.sftp \n \
2) liximomo.sftp \n \
3) doujinya.sftp-revived \n\
Введите номер: ";
name_file_target = "\\\\dist\\\\extension.js";
profile_patch = ".vscode\\\\extensions";
#else
file_is_not_found = "Файл не найден";
file_is_not_open = "Файл не может быть открыт";
out_of_range = "Число не должно быть больше 3 и меньше 1";
invalid_argument = "Введите число: ";
ask_user_a_plagin = "\
Каким плагином вы пользуетесь: \n \
1) natizyskunk.sftp \n \
2) liximomo.sftp \n \
3) doujinya.sftp-revived \n \
Введите номер: ";
name_file_target = "/dist/extension.js";
profile_patch = ".vscode/extensions";
#endif // WIN32
}
За переход в директорию пользователя отвечает функция jumpToDirectory.
void GetFileTarget::jumpToDirectory(){
#ifdef WIN32
// Изменение кодировки в cmd на UTF-8
system("chcp 65001");
std::string current_patch;
// Команда на сохранение списка файла и каталогов во временный файл tmp_file_name
std::string cmd = "dir %UserProfile% >> "+tmp_file_name;
// Выполнение заданной команды в cmd Windous
system(cmd.c_str());
// Получить из временного файла путь к пользовательскому каталогу
std::ifstream tmp_file_point(tmp_file_name);
if(tmp_file_point.is_open()){
std::string search_str = " Directory of ";
for(std::string line; getline(tmp_file_point, line);){
std::size_t pos = line.find(search_str);
if(pos != std::string::npos){
// Убрать то что находится перед нужной информацией
current_patch = line.replace(0, search_str.size(), "");
// Приводим в соответствие сам путь к директории. Должен выглядеть например вот так (C:\\Users\\user_himself)
current_patch = std::regex_replace(current_patch, std::regex("\\\\"), "\\\\");
break;
}
}
tmp_file_point.close();
if(remove(tmp_file_name.c_str()) != 0){
throw L"Удалить временный файл не удалось, удалите его сами.";
}
}else{
throw L"Временный файл открыть не удалось";
}
// Переход в домашнюю директорию пользователя
_wchdir(fn::stringToWstring(current_patch).c_str());
#else
// Переход в домашнюю директорию пользователя
chdir(getenv("HOME"));
#endif // WIN32
}
Перейти в домашнюю директорию под Linux как видно очень просто. Что касается Windows, я не знаю как красиво можно это сделать, не используя при этом Visual C++, примеров в интернете так и не нашёл. Абстрагировавшись от задачи и попив чаю «молочный улун» вприкуску с бубликом я вспомнил, что в cmd Windows есть возможность использовать распознаваемые переменные среды. При помощи переменной %UserProfile% и команды DIR получится вывести список содержимого домашней директории пользователя и выдернуть от туда путь к нему.
Изменение кодировки в cmd на UTF-8 (system(«chcp 65001»);) приводит к тому, что во временный файл будет сохранены данные не на кириллице, а на латинице. Если этого не делать и сохранить на кириллице, то выйдет то, что изображено на скриншоте. Остаётся дело за малым, читаем файл, находим нужную строку (по наличию в ней содержимого переменной search_str), убираем из неё всё лишнее и приводим в соответствие, чтобы было два слэша (\\).
Всё что делает функция «setMode» это вывод моего логотипа и для Windows меняет режим преобразования файлов.
void GetFileTarget::setMode(){
#ifdef WIN32
// Задает режим преобразования файлов
if(_setmode(_fileno(stdout), _O_U16TEXT) < 0)throw L"Не удалось установить режим преобразования файла!";
auto const & sz_message
{
L" __________________________________________ \n"
L"| |\n"
L"| ╭━━━━┳━━━┳╮╱╱╭━━━┳━━━┳━━━┳╮╱╭┳━╮╭━╮ |\n"
L"| ┃╭╮╭╮┃╭━╮┃┃╱╱┃╭━╮┃╭━━┻╮╭╮┃┃╱┃┃┃╰╯┃┃ |\n"
L"| ╰╯┃┃╰┫┃╱┃┃┃╱╱┃╰━━┫╰━━╮┃┃┃┃┃╱┃┃╭╮╭╮┃ |\n"
L"| ╱╱┃┃╱┃┃╱┃┃┃╱╭╋━━╮┃╭━━╯┃┃┃┃┃╱┃┃┃┃┃┃┃ |\n"
L"| ╱╱┃┃╱┃╰━╯┃╰━╯┃╰━╯┃╰━━┳╯╰╯┃╰━╯┃┃┃┃┃┃ |\n"
L"| ╱╱╰╯╱╰━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻╯╰╯╰╯ |\n"
L"|__________________________________________|\n"
L"| |\n"
L"| Permission is hereby granted, free of |\n"
L"| charge, to any person obtaining a copy of|\n"
L"| of this software and accompanying files, |\n"
L"| to use them without restriction, |\n"
L"| including, without limitation, the |\n"
L"| rights to use, copy, modify, merge, |\n"
L"| publish, distribute, sublicense and/or |\n"
L"| sell copies of the software. The authors |\n"
L"| or copyright holders shall not be liable |\n"
L"| for any claims, damages or other |\n"
L"| liability, whether in contract, tort or |\n"
L"| otherwise, arising out of or in |\n"
L"| connection with the software or your use |\n"
L"| or other dealings with the software. |\n"
L"|__________________________________________|\n"
L"| website: *************** |\n"
L"| email: tolsedum@gmail.com |\n"
L"|__________________________________________|\n"
};
std::wcout << sz_message << std::endl;
#else
auto const &sz_message{
" __________________________________________ \n"
"| |\n"
"| ╭━━━━┳━━━┳╮╱╱╭━━━┳━━━┳━━━┳╮╱╭┳━╮╭━╮ |\n"
"| ┃╭╮╭╮┃╭━╮┃┃╱╱┃╭━╮┃╭━━┻╮╭╮┃┃╱┃┃┃╰╯┃┃ |\n"
"| ╰╯┃┃╰┫┃╱┃┃┃╱╱┃╰━━┫╰━━╮┃┃┃┃┃╱┃┃╭╮╭╮┃ |\n"
"| ╱╱┃┃╱┃┃╱┃┃┃╱╭╋━━╮┃╭━━╯┃┃┃┃┃╱┃┃┃┃┃┃┃ |\n"
"| ╱╱┃┃╱┃╰━╯┃╰━╯┃╰━╯┃╰━━┳╯╰╯┃╰━╯┃┃┃┃┃┃ |\n"
"| ╱╱╰╯╱╰━━━┻━━━┻━━━┻━━━┻━━━┻━━━┻╯╰╯╰╯ |\n"
"|__________________________________________|\n"
"| |\n"
"| Permission is hereby granted, free of |\n"
"| charge, to any person obtaining a copy of|\n"
"| of this software and accompanying files, |\n"
"| to use them without restriction, |\n"
"| including, without limitation, the |\n"
"| rights to use, copy, modify, merge, |\n"
"| publish, distribute, sublicense and/or |\n"
"| sell copies of the software. The authors |\n"
"| or copyright holders shall not be liable |\n"
"| for any claims, damages or other |\n"
"| liability, whether in contract, tort or |\n"
"| otherwise, arising out of or in |\n"
"| connection with the software or your use |\n"
"| or other dealings with the software. |\n"
"|__________________________________________|\n"
"| website: *************** |\n"
"| email: tolsedum@gmail.com |\n"
"|__________________________________________|\n"
};
std::cout << sz_message << std::endl;
#endif // WIN32
}
С задачей найти нужный файл для правки и найти в нём место правки, справляется функция «getFilePosition» которая возвращает структуру с собранными данными.
GenerelInformation GetFileTarget::getFilePosition(){
// Получить номер который соответствует выбранному плагину
getIntCin();
std::ifstream file_point;
GenerelInformation info;
info.tmp_file_name = tmp_file_name;
bool file_finde = false;
// Поиск подходящей директории плагина
for (auto const& p : fs::directory_iterator(profile_patch)){
std::size_t pos = p.path().string().find(use_plagin[selected_number]);
if(pos != std::string::npos){
name_file_target = p.path().string() + name_file_target;
file_point.open(name_file_target);
info.file_path = name_file_target;
file_finde = true;
break;
}
}
if(!file_finde){
throw file_is_not_found;
}else if (!file_point.is_open()){
throw file_is_not_open;
}
int pos_len = 0;
// Определение позиции в файле для его правки
for (std::string line; getline(file_point, line);){
std::size_t pos = line.find(search_begin[selected_number]);
if(pos != std::string::npos){
pos_len += pos + search_begin[selected_number].size();
info.pos_begin = pos_len;
std::string name_searching_str = "function";
std::size_t pos_end = line.find(name_searching_str, pos + search_begin[selected_number].size()+name_searching_str.size());
if(pos_end != std::string::npos){
info.pos_end = pos_len + (pos_end-(pos + search_begin[selected_number].size()));
break;
}
}else{
// Так как в Linux новая строка обозначается \n а в Windows \r\n при работе со строками в системе Win надо увеличить размер строки на 1 символ
pos_len += line.size();
#ifdef WIN32
pos_len +=1;
#endif // WIN32
}
}
file_point.close();
info.selected_number = selected_number;
return info;
}
Так как в Linux новая строка обозначается \n а в Windows \r\n при работе со строками в системе Windows приходится после новой строки продвигать каретку ещё на один символ (pos_len += 1).
Ну и получение номера соответствующего выбранному плагину реализовано в функции «getIntCin»
void GetFileTarget::getIntCin(){
fn::printString(ask_user_a_plagin);
my_stryng tmp_str;
bool iter = true;
while (iter){
try{
fn::getLineCin(tmp_str);
selected_number = std::stoi(tmp_str);
if(selected_number > 0 && selected_number < 4){
selected_number--;
iter = false;
}else{
fn::printString(out_of_range);
}
}catch(const std::invalid_argument &e){
fn::printString(invalid_argument);
}catch(const std::out_of_range &e){
fn::printString(out_of_range);
}
}
}
Две задачи по изменению файла взял на себя класс «FileRewrite». Ему досталось:
-
Получить от пользователя путь к новому месту хранения настроек, и узнать какой плагин интересует пользователя;
-
Заголовочный файл выглядит так:
/**
* @file FileRewrite.hpp
* @author Tolsedum (tolsedum@gmail.com)
* @brief For to change file
* @version 0.1
* @date 2022-06-12
*
* @copyright Copyright (c) 2022
*
*/
#pragma once
#ifndef FILE_REWRITE
#define FILE_REWRITE "FILE_REWRITE"
#include "GlobalIncluds.hpp"
class FileRewrite{
private:
/** @brief Информация о файле*/
GenerelInformation info;
/** @brief Строка с готовым js кодом*/
std::string str_to_insert;
/** @brief Варианты заготовок с js кодом для разных плагинов*/
std::vector<std::string> function_variant;
/** @brief Сообщение о неудачном удалении*/
const my_char *remove_file_failed;
/** @brief Сообщение о удачном изменении файла*/
const my_char *file_patched;
/** @brief Прощание с пользователем*/
const my_char *say_goodby;
/** @brief Спросить пользователя путь где хранить файл*/
const my_char *ask_user_a_path;
public:
FileRewrite(GenerelInformation &info);
/** Получить от пользователя путь к новому месту хранения настроек*/
void getSettingsPatch();
/** Редактирование файла*/
void rewrite();
};
#endif // !FILE_REWRITE
Я решил вынести в переменные класса сообщения пользователю о ошибках, и вопросах для того, чтобы их инициализировать в конструкторе класса в зависимости от операционной системы, также как и в предыдущем объекте.
Конструктор класса FileRewrite выглядит следующем образом:
FileRewrite::FileRewrite(GenerelInformation &info){
this->info = info;
#ifdef WIN32
remove_file_failed = L"Удаление файла не удалось, удалите его сами.";
file_patched = L"Файл изменён.";
say_goodby = L"Файл изменён. Наслаждайтесь!";
ask_user_a_path = L"Введите путь к месту хранения настроек: ";
#else
remove_file_failed = "Удаление файла не удалось, удалите его сами.";
file_patched = "Файл изменён.";
say_goodby = "Файл изменён. Наслаждайтесь!";
ask_user_a_path = "Введите путь к месту хранения настроек: ";
#endif // WIN32
function_variant = {
"function p(e){e='%s';return o.join(e,a.CONFIG_PATH)}",// for natizyskunk.sftp
"function d(e){e='%s';return o.join(e,a.CONFIG_PATH)}", // for liximomo.sftp
"function p(e){e='%s';return o.join(e,a.CONFIG_PATH)}" // for doujinya.sftp-revived
};
}
Переменная function_variant хранит варианты шаблонов для замены в файле. Место для вставке введённого пользователем пути обозначено «%s». Первый и второй вариант идентичны, но принадлежат разным плагинам. В дальнейшем, возможно, надо будет добавлять шаблон в зависимости от версии плагина.
Получение от пользователя пути к новому месту хранения настроек реализовано в функции «getSettingsPatch»:
void FileRewrite::getSettingsPatch(){
my_stryng tmp_path_settings_file;
fn::printString(ask_user_a_path);
// Получение от пользователя пути хранения настроек
fn::getLineCin(tmp_path_settings_file);
fn::printString(' ');
std::string path_settings;
#ifdef WIN32
path_settings = fn::wstringToString(tmp_path_settings_file);
path_settings = std::regex_replace(path_settings, std::regex("\\\\"), "\\\\");
#else
path_settings = tmp_path_settings_file;
#endif // WIN32
str_to_insert = function_variant[info.selected_number];
// Вставка пути в шаблон
str_to_insert = std::regex_replace(str_to_insert, std::regex("%s"), path_settings);
}
И последняя функция которая делает то ради чего всё затевалось «rewrite»:
void FileRewrite::rewrite(){
if(info.pos_begin > 0){
std::ifstream file_target(info.file_path, std::ios::in | std::ios::binary);
std::ofstream tmp_file(info.tmp_file_name, std::ios::out | std::ios::binary | std::ios::trunc);
bool is_opened = tmp_file.is_open() && file_target.is_open();
if(is_opened){
// Пмшем во временный файл данный из файла с кодом до позиции вставки
char *big_buffer = new char[info.pos_begin];
file_target.read(big_buffer, info.pos_begin);
tmp_file.write(big_buffer, info.pos_begin);
delete[] big_buffer;
// Вставляем во временный файл новую запись
tmp_file.write(str_to_insert.c_str(), str_to_insert.size());
file_target.seekg(0, file_target.end);
int size_buffer = file_target.tellg();
// Определяем сколько надо пропустить
size_buffer -= info.pos_begin+(info.pos_end-info.pos_begin);
// Пропускаем старую запись
file_target.seekg(info.pos_end);
// Записываем всё что осталось
char *buffer = new char[size_buffer];
file_target.read(buffer, size_buffer);
tmp_file.write(buffer, size_buffer);
delete[] buffer;
}
file_target.close();
tmp_file.close();
if(is_opened){
std::ifstream tmp_file_if(info.tmp_file_name, std::ios::in | std::ios::binary);
std::ofstream file_target_in(info.file_path.c_str(), std::ios::out | std::ios::binary | std::ios::trunc);
// Переносим содержимое из временного файла в оригинальный
file_target_in << tmp_file_if.rdbuf();
tmp_file_if.close();
file_target_in.close();
// Удаляем временный файл
if (remove(info.tmp_file_name.c_str()) != 0) {
throw remove_file_failed;
}else{
fn::printString(file_patched);
}
}
fn::printString(say_goodby);
}
}
Теперь попробую сравнить две программы, заглянув в их «нутро». Что скажет «Ghidra» о программах. Моё внимание привлекла строчка с размерами файлов. Программа для операционной системой Linux весит намного меньше чем для Windows.
Но это сравнение не совсем точно, если посмотреть на объём файлов, то разница будет ещё более существенная.
Открываем оба файла и сравниваем.
Функция main в варианте Windows мало отличается от оригинала. В процедуре «__main()» находится ещё одна процедура под названием «__do_global_ctors()» она вызывает конструкторы для всех статических объектов. А в варианте с Linux такого нет, но есть кое что другое.
Объявляются две дополнительные переменные «long lVar1, in_FS_OFFSET;». Учитывая то что сначала в «lVarl» ложем определённые данные «lVar1 = *(long *)(in_FS_OFFSET + 0x28);», а перед выходом из функции проверяем на идентичность «if (lVar1 != *(long *)(in_FS_OFFSET + 0x28))» и если не совпало вызываем «__stack_chk_fail()» (сбой проверки стека), можно смело утверждать, что это проверка на переполнение стека перед выходом из функции. Такая конструкция часто встречается в методах, функциях, процедурах, конструкторах приложения для Linux. По какой-то причине «Ghidra» не нравится, что метод «__stack_chk_fail» ничего не возвращает, возможно это связано с тем, что анализ не может пройти дальше содержимого функции. Если проверка на переполнение стека и применение мер по его защите это хорошо, то почему тогда такая проверка отсутствует в приложении для Windows?
Сравнивая функцию «jumpToDirectory» в приложении для Linux, короткая запись, что не удивительно поскольку способ решения задачи перехода в домашнюю директорию там проще.
А в приложении для Windows есть что посмотреть и над чем порассуждать. Например конкатенация строк. Оговорюсь, конкатенация строк не отличается не там не там, просто рассматриваю жучков под ногами.
Исходный код склейки строк выглядит так:
std::string cmd = "dir %UserProfile% >> "+this→tmp_file_name;
В «Ghidra» так:
std::operator+<char,_std::char_traits<char>,_std::allocator<char>_>(&cmd,"dir %UserProfile% >> ",&this→tmp_file_name);
А вызов функции «system(cmd.c_str());» с передачей параметра (const char*) выглядит так:
pcVar4 = std::__cxx11::basic_string<char,
_std::char_traits<char>,
_std::allocator<char>_>::c_str(&cmd);
system(pcVar4);
Это может значить то, что между такой записью:
std::string str_main = "Some text";
const char* c_test_str = str_main.c_str();
system(str);
И такой:
std::string str_main = "Some text";
system(str_main.c_str());
Разница в том, что во втором случае, переменная «c_test_str» есть но нам не доступна, и компилятор её применяет там, где идёт следующий вызов функции «c_str()».
Рассматривая и сравнивая код, я наткнулся на необычные функции, которые находились исключительно в приложении для Windows с названием на подобие таких: «FUN_0046cd10», «FUN_0046cd40». Когда «заглядываешь внутрь» она выглядит так: «void FUN_0046cd10(void){return;}». Ничего не делает и возвращает NULL. Взглянув на те же строки в конце конструктора «FileRewrite», на скриншоте можно увидеть в приложении для Linux деструкторы класса «std::allocator<char>», а в приложении для Windows три одинаковые строки на том-же месте с вызовом функции «FUN_0046cd40()»
Это говорит о том, что «Ghidra» не смогла опознать смысл конструкции написанной ниже:
Linux |
Windows |
LEA RAX=>local_99,[RBP + -0x91] |
LEA RAX=>local_19,[RBP + -0x1] |
MOV this,RAX |
MOV this,RAX |
CALL std::allocator<char>::~allocator |
CALL FUN_0046cd40 |
Я сначала подумал, что если запустить «Ghidra» из Windows, то функции определятся нормально. Ради удовлетворения своего интереса пришлось зайти в давно заброшенную мной, установленную рядом с Linux операционную систему Windows. Тридцать минут ожидания установки обновлений и неизвестно чем обоснованный процесс загрузки жесткого диска, оперативной памяти и процессорного ресурса, заставили меня понервничать. Но поставленная задача была выполнена и я убедился в том что ошибался. Ничего не изменилось. Возможно, причина кроется в самой «Ghidra». После вынесенного вердикта было принято решение, завершить свою разведку кросс-компиляции и подвести итоги.
Во первых, прежде чем браться за подобные задачи и тратить своё драгоценное время, желательно:
-
спросить окружающих решается ли задача в настройках;
-
спросить разработчика, пойдут ли они на добавление необходимого функционала;
-
и вообще оно кому нибудь надо?
Последний пункт не обязателен для тех кто учится и хочет узнать что-то новое.
Во вторых, прежде чем лезть в чужой код, ознакомьтесь с лицензией разработчика.
В третьих, к выбору языка программирования стоит подходить с учётом некоторых факторов:
-
соотношение ресурсов железа против качества написания логики и компиляции или интерпретации кода;
-
сроки выполнения задачи;
К примеру: я наверняка уверен, что с этой задачей можно было справится на Java быстрей, и размер кода был бы на порядок короче. Производительность выполнения бинарника на C++ выше (возможно стоит это проверить) но с учётом мощности железа и не критичности к скорости выполнения программы, Java выиграл бы не по одному пункту.
P.S. Ходит много слухов о том, что Rust лучше по многим пунктам чем C++. Что для написания программы достаточно постучать лицом по клавиатуре для того чтобы что нибудь да получилось. По сему, есть у меня мысли заняться разведкой этого языка, как минимум для расширения своего кругозора, а может, дойдёт до того что и проект какой на нём настучу. Будем посмотреть!
Этот патч находится в свободном доступе на GitHub
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
- 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. 😅
Compiling C++ code designed for Windows while using a Linux system can be a tricky endeavor, especially when dealing with dependencies not included as header-only libraries. If you’re working with libraries like TGUI, SFML, and CUDA on an Arch Linux environment and find yourself needing .lib and .dll files, you’re not alone. Here, we’ll explore the specific steps and strategies you can implement to successfully compile your C++ code for Windows using CMake, while overcoming dependency hurdles.
Understanding the Challenges
When you compile C++ code designed for Windows on a Linux machine, particularly using a tool like CMake, the primary challenges arise from the dependencies that are not header-only. Libraries such as TGUI and SFML require specific binary files (.lib, .dll) that are generally tailored for the Windows operating system. Additionally, if you’re using the CUDA Toolkit version 12.8, ensuring compatibility and obtaining the right versions of these libraries becomes even more critical.
Setting Up Your Environment
Before you begin the cross-compilation process, make sure you have the necessary tools installed on your Arch Linux machine:
-
CMake: Make sure you have CMake installed. You can install it using the package manager:
sudo pacman -S cmake
-
A Cross-Compiler: You’ll need a cross-compiler that can generate Windows executables from Linux. The
mingw-w64
package is a popular choice for this. Install it with:sudo pacman -S mingw-w64-gcc
- Windows Libraries: Download the required .lib and .dll files for the libraries you’re using (TGUI, SFML, CUDA). You can find these on the respective library’s official website or GitHub repository.
Configuring CMake for Cross-Compilation
In your CMake project, you’ll need to set up a toolchain file to instruct CMake to use the Windows cross-compiler. Create a file called toolchain.cmake
with the following content:
set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_C_COMPILER /usr/bin/x86_64-w64-mingw32-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/x86_64-w64-mingw32-g++)
set(CMAKE_FIND_ROOT_PATH /path/to/your/win/libs)
# Search for programs in the build host directories
set(CMAKE_FIND_PROGRAMS TRUE)
# Search for CMake modules and packages in the build host directories
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll")
# Include directories of the libraries
include_directories(/path/to/your/windows/include)
# Add the necessary directories for the libraries
link_directories(/path/to/your/windows/libs)
This configuration file tells CMake to use the MinGW cross-compiler and sets the search paths for the necessary libraries.
Running CMake with the Toolchain
To compile your project using the toolchain you just created, run the following commands in your terminal, replacing <your_project_directory>
with the path to your project:
mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake
make
This process will generate the makefiles needed to compile your C++ code into a Windows executable.
Managing Dependencies
Since your project depends on several libraries, make sure to link against them properly in your CMakeLists.txt
. For example:
find_library(TGUI_LIBRARY tgui HINTS /path/to/your/windows/libs)
find_library(SFML_LIBRARY sfml-graphics sfml-window sfml-system HINTS /path/to/your/windows/libs)
find_library(CUDA_LIBRARY cuda HINTS /path/to/your/windows/libs)
target_link_libraries(your_project ${TGUI_LIBRARY} ${SFML_LIBRARY} ${CUDA_LIBRARY})
Make sure to replace your_project
with the actual target name of your CMake project. This code will link the libraries directly to your project, allowing your Windows executable to run with the necessary capabilities.
Testing Your Windows Executable
Once the compilation is successful, you should have a Windows executable file in the build
directory. To test it, you can transfer the resulting .exe
file along with the required .dll
files to a Windows environment. Make sure the .dll files are in the same directory as the executable when you attempt to run it.
Frequently Asked Questions (FAQ)
Can I compile any C++ project for Windows on Linux?
Yes, as long as you have a cross-compiler set up and the necessary Windows libraries.
What if I don’t have .lib or .dll files for a library?
You may need to download or compile the Windows versions of the libraries from their official sources.
Is cross-compiling with CMake difficult?
It can be challenging initially, but once you set up the toolchain properly, it becomes straightforward to manage.
Will my CUDA code work on Windows compiled from Linux?
Yes, as long as you use the correct Windows compatible libraries while compiling.
In conclusion, cross-compiling C++ projects from Linux to Windows can be seamlessly managed with CMake and the appropriate toolchain setup. By following the steps outlined, you should be able to successfully compile your projects and run them on Windows, ensuring all dependencies are correctly linked and functional.
Cross-compiling for windows (from linux) — Part 1
This article explains how to cross-compile a 64-bit windows application
from linux. This means that you don’t need to have a windows build-machine,
and you can use all linux based development tools. This is great for
developers who feel more comfortable in linux.
For some time, I knew that this was technically possible, but always
assumed it would be either too difficult, or otherwise buggy and cumbersome.
That I would eventually give up, and porting the compilation to windows,
using a build machine running windows.
After two days of working at this problem, I’m eager to explain and
share my experience with it. I’ll try to do so in two parts:
Part 1:
- Install the mingw cross-compilation tools.
- Compile hello_world.exe.
- A SCons based build system, that will be used in part 2.
Part 2:
- Cross-compile SDL. Cross-compile a program that links to,
and initializes SDL. - Repeat the process for similar libraries:
- SDL_image, together with libjpg and libpng.
- SDL_mixer, with libogg and libvorbis
- SDL_ttf, with libfreetype
- GLEW
1. Getting mingw, the cross-compiler tools
To cross-compile, I’ll use the mingw-project, in
partuclar, the 64 bit gcc-like build tools. On ubuntu, I installed these with
If you’re relying on c++11 features, you might want to verify that
the mingw-g++ compiler is version 4.7 or newer.
To check out which version you have:
2. Hello_World.exe
In order to cross-compile a hello world program, all you need to do is:
It works fine.
The
—static
makes sure all necessary libraries get
packed into the
exe
.
Without it, you’ll get
an error message.
3. Simple SCons build system
Since a project usually consists of more than a main.cpp,
you want an exensible build system. Preferably, one with the ability
to easy switch between target platforms, and manage the differences between
them.
I’ll be using SCons.
It is based on python, and easy to understand and customize. In fact, I
don’t think you need to know anything about SCons or python
to make sense of the scripts (which rarely is the case for make and cmake)
To follow along, grab a
copy of the repo
(and check out the «basic» branch)
The SCons «Makefile», aka SConsctruct
This sets it up so we can use —win64 flag to specify that we
want a windows build. With this enabled, x86_64-w64-mingw32-g++ is
used as the compiler, instead of g++.
Additionally, the SConstruct script asks SCons to
put all files created by the build in ./build/linux/ or ./build/windows/. This
keeps things tidy, and we also don’t need to rebuild everything when alternating
building for the two platforms.
Lastly, is does whatherver the SCons build script src/SConscript
(together with the environment we set up) tells it to do. But, before I show that, I want to show
the two python scripts that will be used to differentiate the details
between the target platforms:
Linux (utils/linux_build.py)
Windows (utils/win_build.py)
The SCons build script, src/SConscript
Building with SCons
Using the SCons setup, we can build a
linux executable (./bin/main) with:
To build a windows executable (
./bin/main.exe
):
If you want to clean up, add
-c
at the end, e.g.:
scons —win64 -c
To be continued … (part 2)
Building for i386 on an x86_64 machine
Reminder: If when compiling from source you decide you’d like to recompile with a different set of options, or stop the build in order to change some of the options you’ve selected, you must first run `make distclean` (`make clean` only removes object files) in order to remove generated configuration files before you run the configure script again with your new options. If you accidentally stop the build (by hitting ctrl-c or something) and do not wish to change any of the options, you can safely restart the build by simply typing the `make` command and it will continue right where it left off.
%debian
if ./configure complains about not being able to find gtk-config, install the ‘libgtk2.0-dev’ package
%%
If the compile fails with the error «had to relocate PCH», try adding «—disable-precomp-headers’ to your ./configure line.
You can create windows binaries without even booting to Windows! see Cross-Compiling Under Linux.
See also this page about Build System Management.
You may need to build 32-bit apps on an x86_64 Linux box (Suse or RHEL WS3 which is what I have).
On my box at least, the 32-bit gtk libs aren’t installed, so I opted to build the plain X11 version,
but the config below should work for either one.
Configure like this:
mkdir build_ia32 cd build_ia32 ../configure CC='gcc -m32' CXX='g++ -m32' --x-libraries=/usr/X11R6/lib ...options... make
On RHEL WS 3, there are some missing lib symlinks in /usr/lib, e.g. no libX11.so (should be a symlink to libX11.so.X.Y). You’ll have to make those manually. They should be pretty obvious config or build failures.
Also, the configure script tries to search in /usr/lib64 and /usr/X11R6/lib64; remove those from SEARCH_LIBS near the top of the configure script (not doing this would make it try to link with e.g. -lXinerama, which exists but only in the 64 bit world.)
Cross-compiling under Linux for MS Windows
- Install The Mingw Cross-Compiler.
- Compile wxWidgets
- Download wxWidgets source
- Compile with ./configure —prefix=/usr/local/i586-mingw32 —host=i586-mingw32msvc —build=i686-linux —your_optional_switches
make make install
Host string differs depending on mingw installations, you should check your mingw cross compiler for the exact value. Build option can also be different if you’re not using Linux on x86, of course. For example, if you use mingw32 from Debian or Ubuntu packages under Linux on x86-64 architecture the command would be:
./configure —prefix=/usr/local/i586-mingw32msvc —host=i586-mingw32msvc —build=x86_64-linux —your_optional_switches
You may use ‘checkinstall make install’ instead of ‘make install’ in order to create a package and allow clean removal of the binaries.
See also:
- Embedding icons in your cross-compiled binary: Cross-Compiling Windows Resources
- Embedding other files: Embedding PNG Images
- Several tips, tricks and workarounds as well as Eclipse configuration: wxWindows application compile (for Linux) and cross-compile (for Windows) under Linux/Eclipse/CDT
- IBM DeveloperWorks article
- Cross compiling RLS AVON with MinGW on Linux (simple instructions for installing environment, compiling wxWidgets, and compiling an certain wxWidgets application)
Note: The SDL scripts and these directions cannot be mixed.
Note: By disabling threads (with —disable-threads), you can avoid a dependency on the ming dll
Example usage
Once installed, save the following file as winhello.c (stolen from Installing and Using the MinGW Cross-Compiler on Mac OS X):
/* * Hello, World for Win32 * gcc winhello.c -o winhello.exe */ #include <windows.h> int main(int argc, char *argv[]) { MessageBox(NULL, "Hello, world!", "Hello, world!", MB_OK); return 0; }
To build the example, execute the following command:
$ i386-mingw32-gcc winhello.c -o winhello.exe
and run it, for example, with wine: wine winhello.exe
Contrib libraries
Couldn’t compile the contrib directory with the wxMSW 2.4.2 sources, use the latest cvs.
Flags
You might need these flags when compiling:
-Wl,--subsystem,windows -mwindows \ -DWINVER=0x0400 -D__WIN95__ -D__GNUWIN32__ \ -DSTRICT -DHAVE_W32API_H -D__WXMSW__ -D__WINDOWS__
And these while linking:
-lregex -lpng -ljpeg -lzlib -ltiff -lstdc++ -lgcc -lodbc32 -lwsock32 -lwinspool -lwinmm -lshell32 \ -lcomctl32 -lctl3d32 -lodbc32 -ladvapi32 -lodbc32 -lwsock32 -lopengl32 -lglu32 -lole32 -loleaut32 \ -luuid
environment variables
VZ: Note that this is unnecessary when using autoconf cross-compilation support.
In order to use the cross-compiler tools you need to replace the normal tools in makefiles. This is easier to do just exporting some environment variables before running configure/make:
export CC=i586-mingw32msvc-gcc export CXX=i586-mingw32msvc-c++ export LD=i586-mingw32msvc-ld export AR=i586-mingw32msvc-ar export AS=i586-mingw32msvc-as export NM=i586-mingw32msvc-nm export STRIP=i586-mingw32msvc-strip export RANLIB=i586-mingw32msvc-ranlib export DLLTOOL=i586-mingw32msvc-dlltool export OBJDUMP=i586-mingw32msvc-objdump export RESCOMP=i586-mingw32msvc-windres export WINDRES=i586-mingw32msvc-windres
SDL’s Script
See also: BuildCVS.txt in the tar of the SDL scripts
http://www.libsdl.org/extras/win32/cross/ contains scripts that automate the compiler build process described above.
Download build-cross.sh, cross-configure.sh, and cross-make.sh.
Run the script build-cross.sh.
Download the CVS version wxAll and uncompress it.
Copy cross-configure.sh and cross-make.sh to the wxWidgets-2.5.2 directory.
Run cross-configure.sh and cross-make.sh and you should be done.
MXE
From MXE page — «MXE (M cross environment) is a GNU Makefile that compiles a cross compiler and cross compiles many free libraries such as SDL and Qt.»
MXE can be used to cross-compile wxWidgets projects. You can install MXE according to the steps in the tutorial, go to the MXE directory and issue the following command:
make cc wxwidgets
Now, ensure that you have <MXE location>/usr/bin
in the PATH
and compile your project with the following command:
i686-w64-mingw32.static-gcc main.cpp $(i686-w64-mingw32.static-wx-config --cxxflags --libs) -o main.exe -std=c++11 -lstdc++
Note about WINE
Make sure to turn off binfmt support before running configure (Debian: /etc/init.d/binfmt-support stop), which invokes wine for .exe files; otherwise configure will think it does NOT use a cross-compiler.
Autoconf/Automake unit testing suites
(maybe this section should go somewhere else?)
It is possible to autotest your code using wine (you are using unit tests, right?). This makes it very easy to script code under Unix to build multiple platforms, then test, without intervention. This section focusses on testing console-able objects.
First familiarise yourself with building test binaries with autoconf and automake. I recommend cppunit (for C++ systems). There’s plentiful documentation on cppunit’s website on integrating cppunit with Makefile.am. One show-stopping step is the ability to test msw binaries in the same way (make check) one tests unix binaries.
To take advantage of wine (running your tests automatically with wine), first make sure that wine may run headless. If you have access to a graphical terminal then this isn’t important (if you’re Ok with having wine spout gobbledygock to a window with every run). Make sure your test-directory Makefile.am’s have all TESTS tokens suffixed with $(EXEEXT):
TESTS = TestFoo$(EXEEXT)
check_PROGRAMS = TestFoo
…and so on.
Next add a configure.ac (you have upgraded to using .ac instead of .in, right?) line manipulating the macro TESTS_ENVIRONMENT:
AC_SUBST([TESTS_ENVIRONMENT], [$WINE])
In this I assume that WINE has been set with AC_CHECK_PROGS or something (even `WINE = wine’). This will have all tests run in the following format:
$(TESTS_ENVIRONMENT) $${dir}$$tst
Where dir is the path and tst is the test name (remember that EXEEXT). That’s it: wine will return the exit code of your running binary. You can also put a special shell or other token in there, but that exceeds the focus of this documentation. This assumed automake-1.9 and autoconf-2.57 but I’m fairly certain it works in earlier versions of both (uncertain about autoconf-2.13 style).
Testing the created executables
To test your mingw32 installation with a sample not using wxWidgets, look above («Example usage»).
To test your mingw32 installation with a real wxWidgets example, take minimal.cpp from the official examples. Fortunately, I didn’t have to fiddle around with the flags myself, I use wx-config for that — not the system-wide one, but the one compiled with the cross-Windows libraries below /usr/local/i586-mingw32!
i586-mingw32msvc-g++ -c -o minimal.o minimal.cpp \ `/usr/local/i586-mingw32/bin/wx-config --cxxflags` i586-mingw32msvc-g++ -o minimal.exe minimal.o \ `/usr/local/i586-mingw32/bin/wx-config --libs`
Attention, there are are two caveats here that could cost you lots of time (it did cost me lots of time, hope I’ll save yours
- When compiling, be sure to include the -c option. -o with an .o object file isn’t enough, mingw32 will try to link!
- When linking, be sure to include first your object files and then the libraries (given by wx-config). When compiling, the order isn’t too important, but if you exchange libraries and object files at the linking stage, you’ll get lots of undefined references. The reason is that a linker processes the libraries in order of their appearance (see documentation of «-l» at http://sourceware.org/binutils/docs/ld/Options.html).
Finally, when executing your program with wine, qemu (works great!) or even on a real Windows box, don’t forget that even if you linked everything statically, you’ll also need mingwm10.dll in addition to your executable. Put in in the same directory and execute the binary from there, and everything works. I found mingwm10.dll gzipped in /usr/share/doc/mingw32-runtime/