Что такое dfu-util и зачем будить это лихо?
Что это такое и где мы можем это встретить?
“DFU is intended to download and upload firmware to/from devices connected over USB” - dfu-util manual page
dfu-util — это в первую очередь утилита, о чём я не очень‑то и задумывался, поĸа не начал работать с её ĸодом. Она разработана для работы с устройствами, находящимися в DFU (Device Firmware Upgrade) режиме, т. е. в режиме, позволяющим работать непосредственно с прошивĸой. Таĸими устройствами может быть всё от миĸроĸонтроллеров до смартфонов, а самыми примитивными целями работы может быть выгрузĸа и загрузĸа прошивĸи.
Бытовой пример работы с DFU на уровне пользователя
Собственно, пример из жизни, ĸоторый со мной случился буĸвально во время написания статьи.
Решил я сбросить старый айфон ĸ заводсĸому виду прошивĸи. И вот, что не сделает ни один адеĸватный человеĸ - не будет создавать во время первого запуска пароль от балды и забывать его через 2 наносекунды после ввода.
Да, именно это я и сделал. Учетная запись Apple ID еще не введена на устройстве, таĸ что через неё сбросить пароль нельзя. После целого дня подбора пароля я заблоĸировал телефон на день. Начал рысĸать в интернете, ĸаĸ сбросить пароль. Вуаля! Можно перевести смартфон в DFU режим, подĸлючить его ĸ macbook-у (либо Windows iTunes) и переставить прошивĸу на новую. Моей радости не было предела. Делов на 15 минут и смартфон готов ĸ работе.
Это пример именно ручного взаимодействия с DFU режимом. Все остальные обновления прошивки смартфона, микроволновок, лаптопов и прочего можно считать автоматическим процессом, связанным с DFU.
Небольшой забавный момент на тему первого упоминания о dfu-util в 2007-м году:
“initial (unfinished) version of new DFU utility (dfu‑programmer just sucks as something generic, device independent)” — Harald Welte в своём первом коммите.
Это ж как должна была не понравиться dfu-programmer утилита, чтобы чувак создал целый проект. Да и такой, что им будут пользоваться сотни тысяч людей и компаний следующие пару десятилетий.
Будить лихо или оно и так гуляет по миру?
dfu-util не шибко-то засыпала. Её по сей день используют компании разного уровня гигантизма от BlackMagic design (dfu-шечка присутствует в их офф. гайдах) и STM32 (dfu-util является одним из основных средством загрузки кода) до каких-нибудь стартапов.
Например, в компании делают какое-то физическое устройство, которое конечно же должно иметь функционал обновления прошивки. Перед разработчиками встанет выбор, писать собственно API для работы с устройством в DFU режиме или просто взять готовый код и по необходимости изменить его. Выбор очевиден.
В дополнение к аргументу упомяну, что одной из самых популярных библиотек для общения с USB портом является LibUsb, а dfu-util как раз её и использует.
Не так давно у меня возникла необходимость работать с DFU устройством, а dfu-util обычно контрибьютится посредством уже собранных бинарей, а не пакета или библиотеки. Отсюда и пошло желание написать прослойку для C++ & CMake, чтобы иметь возможность прямого доступа к исходному коду во время написания проекта.
Планы действий по текущей статье
Возможность подключения утилиты dfu-util как сабмодуля/сабдиректории С++ проекта под CMake сборкой. CMakeList-ы буду писать на достаточно базовом уровне как по причине своих навыков, так и для того, чтобы статья была более ёмкой и читабельной. Поэтому применения ExternalProject, CPM и прочих очень полезных и занимательных модулей здесь вы не увидите;
Конечно же иметь возможность работы как на macOS, так и на Windows. Век кроссплатформенность на дворе всё-таки;
Возможность сборки утилиты dfu-util посредством CMake;
Привести пример класса-прослойки на С++ для собственного использования инструментария dfu-util.
Повествование начнется macOS. Все основные планы будут доведены до конца именно на macOS и только под конец будет показана адаптация под Windows.
Подготовка
Посмотрел гайд от разработчиков dfu-util. Autoconf, autoheader... это что такое вообще... Ладно, ставим любимой brew-шкой
brew install make cmake pkg-config libusb autoconf autoheader
Надо бы подумать над древом проекта, но подумать надо недолго, так что возьмем стандартную структуру любого проекта.
WorDfuUtil
|--- include
| |--- pch.hpp
| ...
|--- src
| |--- main.cpp
| ...
|--- ThirdParty
| |--- dfu-util (submodule)
| |--- ...
| |--- DfuUtilConfiguring.cmake
| |--- CMakeLists.txt
|--- CMakeLists.txt
Основная часть
Топовый CMakeLists.txt мне пока что не интересен, так что пойдем сразу в ThirdParty/CMakeLists.txt
. Подключил и настроил таргеты dfu-util
и dfu-util_exe
.
# WorDfuUtil/ThirdParty/CMakeLists.txt
cmake_minimum_required(VERSION 3.23)
# ---------- #
# libusb #
# ---------- #
find_path(LibusbIncludeDir
NAMES libusb.h
PATH_SUFFIXES "include" "libusb" "libusb-1.0")
find_library(LibusbLib
NAMES libusb-1.0.dylib
PATH_SUFFIXES "lib")
add_library(libusb INTERFACE)
target_link_libraries(libusb
INTERFACE ${LibusbLib})
target_include_directories(libusb
INTERFACE ${LibusbIncludeDir})
# ------------ #
# dfu-util #
# ------------ #
project(dfu-util
LANGUAGES C)
set(DfuUtilRoot ${CMAKE_CURRENT_SOURCE_DIR}/dfu-util)
set(DfuUtilSourceDir ${DfuUtilRoot}/src)
add_library(dfu-util)
set(Sources
${DfuUtilSourceDir}/dfu_load.c
${DfuUtilSourceDir}/dfu_util.c
${DfuUtilSourceDir}/dfuse.c
${DfuUtilSourceDir}/dfuse_mem.c
${DfuUtilSourceDir}/dfu.c
${DfuUtilSourceDir}/dfu_file.c
${DfuUtilSourceDir}/quirks.c)
target_sources(dfu-util
PRIVATE ${Sources})
source_group("dfu-util_sources"
FILES ${Sources})
target_include_directories(dfu-util
PUBLIC
${DfuUtilRoot}/src
${DfuUtilRoot})
target_link_libraries(dfu-util
PUBLIC libusb)
# ---------------- #
# dfu-util_exe #
# ---------------- #
add_executable(dfu-util_exe)
target_sources(dfu-util_exe
PRIVATE
${Sources}
${DfuUtilSourceDir}/main.c)
target_include_directories(dfu-util_exe
PUBLIC
${DfuUtilRoot}/src
${DfuUtilRoot})
target_link_libraries(dfu-util_exe
PUBLIC libusb)
Для сбора сурсов проекта нацеленно использовал список вручную вместо file({GLOB | GLOB_RECURSE} ...)
, потому как в main.cpp
нас ждёт сюрприз, о котором скажу позже. Вообще не понимаю, почему комьюнити так не любит эту команду даже с учетом комментария разработчиков CMake. Отключите автоматизацию, ручками управляйте обновлением CMake и будет вам счастье, НО если у вас в проекте больше сотки файлов, то да - лучше напишите свой сборщик файлов, разложить их по категориям и т.п.
Ремарка по поводу всемогущей LibUsb. Можно, конечно искать её через find_package(PkgConfig REQUIRED)
+ pkg_check_module(libusb REQUIRED libusb-1.0)
, но я не привык пользоваться pkg_config, т.к. это старенькая утилита, да и её зачастую банально нет на машинах.
Dfu-util предоставляет Makefile, но я, как разработчик современного поколения, не знаю, как с ним работать и могу только уловить суть. Постарался плюс-минус соблюсти логику линковки, собрал курсы, линканул LibUsb и с предвкушением нажал на сборку.
По мере изучения, откуда звуки выстрела, понял, что dfu-util-щики генерят config.h
файл, который дефайнит символы в процессе конфигурации. Прикреплю часть символов для понимания, т.к. по их название уже понятно, для чего они нужны.
/**
* ThirdParty/dfu-util/config.h.in
*/
/* config.h.in. Generated from configure.ac by autoheader. */
/* Define to 1 if you have the 'err' function. */
#undef HAVE_ERR
/* Define to 1 if you have the <inttypes.h> header file. */
#undef HAVE_INTTYPES_H
/* Define to 1 if you have the 'usb' library (-lusb). */
#undef HAVE_LIBUSB
/* Define to 1 if you have the 'nanosleep' function. */
#undef HAVE_NANOSLEEP
/* Define to 1 if you have the <stdint.h> header file. */
#undef HAVE_STDINT_H
/* Define to 1 if you have the <stdio.h> header file. */
#undef HAVE_STDIO_H
/* ... */
На основе этого шаблона генерируется следующий заголовочный файл:
/**
* ThirdParty/dfu-util/config.h
*/
/* config.h. Generated from config.h.in by configure. */
/* config.h.in. Generated from configure.ac by autoheader. */
/* Define to 1 if you have the 'err' function. */
#define HAVE_ERR 1
/* Define to 1 if you have the <inttypes.h> header file. */
#define HAVE_INTTYPES_H 1
/* Define to 1 if you have the 'usb' library (-lusb). */
/* #undef HAVE_LIBUSB */
/* Define to 1 if you have the 'nanosleep' function. */
#define HAVE_NANOSLEEP 1
/* Define to 1 if you have the <stdint.h> header file. */
#define HAVE_STDINT_H 1
/* Define to 1 if you have the <stdio.h> header file. */
#define HAVE_STDIO_H 1
/* ... */
Для конфигурации и билда dfu-util нужны команды, но кто я такой, чтобы противиться любви к CMake, так что добавлю команды и выкину их в отдельный DfuUtilConfiguring.cmake
файл:
# WorDfuUtil/ThirdParty/DfuUtilConfiguring.cmake
cmake_minimum_required(VERSION 3.23)
add_custom_target(dfu-util_autogen
COMMAND ./autogen.sh
COMMENT "Creating dfu-util configuration file..."
WORKING_DIRECTORY ${DfuUtilRoot})
add_custom_target(dfu-util_configure
COMMAND ./configure
DEPENDS dfu-util_autogen
COMMENT "Configuring dfu-utils for current OS. Generating makefiles..."
WORKING_DIRECTORY ${DfuUtilRoot})
add_custom_target(dfu-util_build
COMMAND make
DEPENDS dfu-util_configure
COMMENT "Running make for dfu-utils project..."
WORKING_DIRECTORY ${DfuUtilRoot})
Предвкушаю вопрос, зачем мне билдить это Make-ом, если я и так собираю в CMake цель dfu-util_exe
. Не знаю, честно говоря. Пусть оно будет тут и не отсвечивает сильно.
Пробую выполнение configure
и всё ожидаемо отрабатывает стабильно.
Билд dfu-util
таргета снова вывел ту же самую ошибку отсутствия некоторых символов и заголовочников. Это связано с тем, что в коде почти всех исходников есть слудующие строчки:
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
При сборке нужно передать -DHAVE_CONFIG_H
компилятору, чтобы в dfu*.cpp
файлах произошло подключение конфигурационного заголовочника. Передаём, конечно же, публично, чтобы вышестоящая цель имела тот же дефайн.
# ThirdParty/CMakeLists.txt
# ...
add_library(dfu-util)
target_compile_definitions(dfu-util
PUBLIC -DHAVE_CONFIG_H)
# ...
# ThirdParty/CMakeLists.txt
# ...
add_executable(dfu-util_exe)
target_compile_definitions(dfu-util_exe
PUBLIC -DHAVE_CONFIG_H)
# ...
Запускаю снова таргет dfu-util
.
Теперь можно вернуться к CMakeList-у для WorDfuUtil
:
# CMakeLists.txt
cmake_minimun_required(VERSION 3.23)
project(WorDfuUtil
LANGUAGES C)
add_subdirectory(ThirdParty)
# -------------- #
# WorDfuUtil #
# -------------- #
add_executable(WorDfuUtil)
file(GLOB Sources src/*.cpp)
target_sources(WorDfuUtil
PRIVATE ${Sources})
target_compile_features(WorDfuUtil
PRIVATE cxx_std_17)
target_include_directories(WorDfuUtil
PUBLIC include)
target_link_libraries(WorDfuUtil
PRIVATE dfu-util)
Специально сделал пока только исполняемый тип сборки, чтобы тестировать код. После проверки работоспособности исполняемый тип заменится на библиотечный.
Не забываю про pch файл. Удобно иметь это конструкцию, чтобы не писать в каждом файле. Да и вообще у меня эти dfu*.h
заголовочники использоваться будут повсеместно, так что кину их в прекомпиляцию.
/**
* include/pch.hpp
*/
#pragma once
#include "libusb.h"
extern "C" {
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "portable.h"
#include "dfu.h"
#include "dfu_file.h"
#include "dfu_load.h"
#include "dfu_util.h"
#include "dfuse.h"
}
#include "DfuUtilVariables.hpp"
Такой еще момент. Если у нас нет ThirdParty/dfu-util/config.h
при первой инициализации проекта, то CMake падает из-за того, что config.h
указан в pch файле. Добавлю еще условие, чтобы основные таргеты были недоступны, пока не прошла конфигурация dfu-util файлов.
# ThirdParty/DfuUtilConfiguring.cmake
# ...
add_custom_target(dfu-util_build
COMMAND make
DEPENDS dfu-util_configure
COMMENT "Running make for dfu-utils project..."
WORKING_DIRECTORY ${DfuUtilRoot})
if(NOT EXIST ${DfuUtilRoot}/config.h)
message(WARNING "Cannot find dfu-util's config file. Configure dfu-util first.
Run build dfu-util_configure target.")
return()
else ()
set(HaveConfig ON PARENT_SCOPE)
endif()
# ...
# CMakeLists.txt
# ...
add_subdirectory(ThirdParty)
if(NOT ${HaveConfig})
return()
endif()
# ...
# CMakeLists.txt
# ...
target_compile_features(WorDfuUtil
PRIVATE cxx_std_17)
target_precompile_headers(WorDfuUtil
PRIVATE include/pch.h)
# ...
Создал main.cpp
файл для таргета WorDfuUtil
, в котором набросал вызовы пары функций для тестов библиотек.
/**
* src/main.cpp
*/
#include <cstdio>
#include "libusb.h"
#include "pch.hpp"
int main() {
libusb_context *ctx;
const int ec = libusb_init(&ctx);
if (ec < 0) {
std::printf("LibUsb if ducked up.\n");
return -1;
}
probe_devices(ctx);
if (dfu_root == nullptr) {
std::printf("dfu-util is ducked up.\n");
return -2;
}
return 0;
}
На что получил вот такой вывод:
Неопределенные символы... Пошел посмотреть, что это такое.
Разработчики без сарказма прекрасного инструмента даже и не планировали, чтобы утилита была библиотекой, и просто захардкодили глобальные переменные, которые декларированы в ThirdParty/dfu-util/src/main.cpp
. Мне этот файл не нужен, поэтому в голове созрел чудесный и быстродействующий план. Я на всякий случай прикреплю фото своего лица в этот момент.
Невелика потеря. Действую примитивно и действенно - как баран, увидевший хлипкую доску забора.
Создаю файлы ThirdParty/DfuUtilVariables.hpp
и ThirdParty/DfuUtilVariables.cpp
с очевидным содержанием. Не забываю закинуть cpp-шник в CMake таргет dfu-util
.
/**
* ThirdParty/DfuUtilVariables.hpp
*/
#pragma once
extern int verbose;
extern struct dfu_if *dfu_root;
extern char *match_path;
extern int match_vendor;
extern int match_product;
extern int match_vendor_dfu;
extern int match_product_dfu;
extern int match_config_index;
extern int match_iface_index;
extern int match_iface_alt_index;
extern int match_devnum;
extern const char *match_iface_alt_name;
extern const char *match_serial;
extern const char *match_serial_dfu;
/**
* ThirdParty/DfuUtilVariables.cpp
*/
#include "DfuUtilVariables.hpp"
#include <cstddef>
int verbose = 0;
struct dfu_if *dfu_root = nullptr;
char *match_path = nullptr;
int match_vendor = -1;
int match_product = -1;
int match_vendor_dfu = -1;
int match_product_dfu = -1;
int match_config_index = -1;
int match_iface_index = -1;
int match_iface_alt_index = -1;
int match_devnum = -1;
const char *match_iface_alt_name = nullptr;
const char *match_serial = nullptr;
const char *match_serial_dfu = nullptr;
Заветные фразы получены. Нравится.
[12/12] Linking CXX executable WorDfuUtil
Build finished
dfu-util is ducked up.
Process finished with exit code 156
(interrupted by signal 28:SIGWINCH)
Вылет по 156
-му коду мне не интересен, т.к. это скорее всего инициализированная LibUsb ругается, что её не закрыли. Можно считать, что подключение dfu-util успешно завершено.
Пример более высокоуровневого API
Надо бы проверить существующую прослойку на предмет юзабельности. Напишу какой-нибудь объект с примитивным функционалом. Пусть это будет максимально простая загрузка прошивки с устройства. Думается, написание будет создавать некий дискомфорт по следующим причинам:
У меня есть глобальные переменные из
ThirdParty/DfuUtilVariables.hpp
, которые очень невкусно выглядят. Хорошо бы их потом обернуть в какой‑нибудь «контейнер», но пока это не выглядит как необходимость;Есть буквально «инструкция» из файла
ThirdParty/dfu-util/src/main.cpp
, откуда можно брать куски готового кода, но в нём не все так очевидно, как хотелось бы.
Например, если заполнить глобальную переменную вручную, а не вызовомdfuse_parse_options(const char*)
, то она всё равно будет игнорироваться в методеdfuload_do_upload(...)
, т.к. не будет поднят флагint dfuse_address_present
. Можно, конечно, его тоже поднять вручную, но это не единственный подобный момент. Для полноценной работы кода нужно взять прочные штаны и написать полноценную обёртку с устранением всех прелестей, но это выходит за пределы публикации и скорее всего будет выполнено позже и выложено на гите.
Для более короткого представления кода я нацелено пойду на следующие упрощения:
Отсутствие JavaDoc в коде. Я, честно говоря, не знаю, стоит ли писать JavaDoc к коду в статье или нет. Приведу пример без доки, но на гит выложу с докой. Поправьте, пожалуйста, данное решение в комментариях при необходимости, и я внесу правки в статью, добавив JavaDoc;
Вместо создания класса для проверки и управления состоянием USB устройством я просто объявлю функцию в анонимном пространстве имён;
Отсутствие постоянных проверок на статус USB устройства;
Буквально ничего не буду делать, чтобы исправить факт существования глобальных переменных из
ThirdParty/DfuUtilVariables.hpp
.
Погнали.
/**
* include/FirmwareStatus.hpp
*/
#pragma once
#include <cstdint>
namespace WorDfuUtil {
enum class FrameworkStatus
: std::uint8_t {
Deinited = 0b0,
Inited = 0b1,
DeviceOpened = 0b10,
DfuDeviceFound = 0b100,
InterfaceClaimed = 0b1000,
AltInterfaceSelected = 0b10000
};
}
/**
* include/Firmware.hpp
*/
#pragma once
#include <string>
namespace WorDfuUtil {
class Firmware final {
public:
[[nodiscard]]
static bool Upload(const std::string& filePath, int transferLimit, int transferSize = -1) noexcept;
};
}
/**
* src/Firmware.cpp
*/
#include "pch.hpp"
#include "Firmware.hpp"
#include "FirmwareStatus.hpp"
#include <cstdio>
#include <filesystem>
#include <fcntl.h>
#include <sstream>
using namespace WorDfuUtil;
namespace {
libusb_context *ctx;
std::uint8_t frameworkStatus = static_cast<std::uint8_t>(FrameworkStatus::Deinited);
[[nodiscard]]
bool isDfuDeviceFound() noexcept {
std::printf("Cannot find DFU device.\n");
return ::dfu_root != nullptr;
}
[[nodiscard]]
bool checkDfuStatus() {
if (!::isDfuDeviceFound()) {
return false;
}
dfu_status dfuStatus {};
const int ec = dfu_get_status(::dfu_root, &dfuStatus);
if (ec < 0) {
std::printf("Cannot check device status: %s\n", libusb_error_name(ec));
return false;
}
if (dfuStatus.bStatus != 0 || dfuStatus.bState != DFU_STATE_dfuIDLE) {
std::printf("Bad DFU device status: %s\n", dfu_status_to_string(dfuStatus.bStatus));
std::printf("Bad DFU device state: %s\n", dfu_state_to_string(dfuStatus.bState));
return false;
}
return true;
}
void deinitFrameworks() noexcept {
if (::frameworkStatus == static_cast<std::uint8_t>(FrameworkStatus::Deinited)) {
return;
}
if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::DeviceOpened)) {
libusb_close(::dfu_root->dev_handle);
}
if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::DfuDeviceFound)) {
disconnect_devices();
}
if (::frameworkStatus & static_cast<std::uint8_t>(FrameworkStatus::Inited)) {
libusb_exit(::ctx);
}
}
bool prepareFrameworks() {
int ec = libusb_init(&::ctx);
if (ec < 0) {
std::printf("LibUsb initialization error - %s.\n", libusb_error_name(ec));
return false;
}
::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::Inited);
probe_devices(::ctx);
if (!::isDfuDeviceFound()) {
return false;
}
::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::DfuDeviceFound);
ec = libusb_open(::dfu_root->dev, &::dfu_root->dev_handle);
if (ec < 0) {
std::printf("Cannot open device - %s.\n", libusb_error_name(ec));
return false;
}
::frameworkStatus |= static_cast<std::uint8_t>(FrameworkStatus::DeviceOpened);
return ::checkDfuStatus();
}
}
bool Firmware::Upload(const std::string &filePath, int transferLimit, int transferSize) noexcept {
if (!::prepareFrameworks()) {
::deinitFrameworks();
return false;
}
if (std::filesystem::exists(filePath)) {
std::ignore = std::filesystem::remove(filePath);
}
const int fileDescriptor = open(filePath.c_str(), O_WRONLY | O_BINARY | O_CREAT | O_EXCL | O_TRUNC, 0666);
if (fileDescriptor < 0) {
std::printf("Cannot create and open file %s for writing.\n", filePath.c_str());
::deinitFrameworks();
return false;
}
std::stringstream dfuUtilOptions;
dfuUtilOptions << "-s 0x08000000";
dfuUtilOptions << ":" << transferLimit;
if (transferSize < 0) {
transferSize = libusb_le16_to_cpu(::dfu_root->func_dfu.wTransferSize);
if (transferSize == 0) {
std::printf("Device return zero transfer size. Specify it manually.\n");
close(fileDescriptor);
}
if (transferSize < ::dfu_root->bMaxPacketSize0) {
transferSize = ::dfu_root->bMaxPacketSize0;
}
}
const int ec = dfuse_do_upload(::dfu_root,
transferSize,
fileDescriptor,
dfuUtilOptions.str().c_str());
close(fileDescriptor);
if (ec < 0) {
std::printf("Error in firmware uploading.\n");
}
::deinitFrameworks();
return true;
}
Нужно. Побороть. Желание. Написать. Нормально...
Статья. Должна быть. Короткой...
Ладно, после написания статья просто на гите внесу все правки и перепишу код, заполнив все комментарии и разделив всё на объекты.
Ну и протестим это счастье:
/**
* src/main.cpp
*/
#include "Firmware.hpp"
using namespace WorDfuUtil;
int main() {
std::string filePath = "firmware.bin";
int transferLimit = 150'000;
const bool uploadRes = Firmware::upload(filePath, transferLimit);
if(!uploadRes) {
return -50;
}
return 0;
}
В целом, можно было бы заканчивать и предоставить финальный вид кода, но ещё остался Windows, который обещает быть очень весёлым.
Продолжаем. Windows
Сразу же началось веселье из-за того, что autoconf и autoheader нет готовых под Windows. Идём на msys2, скачиваем этот замечательный инструмент и качаем из его среды все инструменты. На билд инструкциях от dfu-util разработчиков есть небольшие подсказки для сборки инструментам. Обычно я ставлю пакет mingw-w64-x86_64-toolchain
, но сейчас можно и точечно поставить только необходимое:
$ pacman -S mingw-w64-x86_64-gcc
$ pacman -S mingw-w64-x86_64-autotools
$ pacman -S mingw-w64-x86_64-liusb
Не забываю добавить в переменную пути окружение msys2:
C:/msys64/mingw64
C:/msys64/mingw64/bin
Возвращаюсь к проекту и пытаюсь запустить конфигурацию dfu-util по билд-инструции. Винда - она такая, лучше сначала проверить работоспособность ручками:
$ bash autogen.sh
$ bash configure USB_CFLAGS="-IC:/msys64/mingw64/include/libusb" \
USB_LIBS="-L C:/msys64/mingw64/lib -lusb-1.0"
Когда я первый раз настраивал dfu-util, возникал целый вагон ошибок. То он не может найти libusb.h
, то он не может найти определение lusb-1.0
библиотеки. Подозреваю, я просто неверно передавал параметры в configure.
Между делом заметил, что в логе от configure
есть вот такой вывод, которого не было на macOS:
Эта строчка носит фиктивный характер. Дефайн HAVE_LIBUSB
не участвует в источниках dfu-util, так что я закрыл на это глаза. В силу бесполезности исправления данной проверки на LibUsb я даже помещу способы решения в скрывающийся блок.
Способы исправления проверки LibUsb
Если же хочется исправить лог вывода, то есть 2 пути:
Путь первый. Путь непредсказуемый.
Переопределить файл для pkg-config libusb-1.0.pc
. Он обычно поставляется, если ставить LibUsb через пакетный менеджер а-ля vcpkg или homebrew.
# libusb-1.0.pc
prefix=/opt/homebrew/Cellar/libusb/1.0.27
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: libusb-1.0
Description: C API for USB device access from Linux, Mac OS X, Windows, OpenBSD/NetBSD and Solaris userspace
Version: 1.0.27
Libs: -L${libdir} -lusb-1.0
Libs.private: -lobjc -Wl,-framework,IOKit -Wl,-framework,CoreFoundation -Wl,-framework,Security
Cflags: -I${includedir}/libusb-1.0
Заменяем 9-ую строку
# libusb-1.0.pc
# ...
Version: 1.0.27
Libs: -L${libdir} -lusb
# ...
Максимально не рекомендую этого делать, т.к. другие библиотеки могут искать -lusb-1.0
и не найдут. Я с подобным не сталкивался, но лучше не играться с файлами поставляемыми через пакетный менеджер.
Зачем я привел заведомо опасный пример? Теперь вы знаете, что из себя представляют файлы для pkg-config и какую информацию они в себе несут.
Путь второй. Путь рекомендуемый.
Этот способ самый безболезненный. Открываем файл ThirdParty/dfu-util/configure.ac
# ThirdParty/configure.ac
# ...
Checks for libraries.
On FreeBSD the libusb-1.0 is called libusb and resides in system location
AC_CHECK_LIB([usb], [libusb_init],, [native_libusb=no],)
AS_IF([test x$native_libusb = xno], [
PKG_CHECK_MODULES([USB], [libusb-1.0 >= 1.0.0],,
AC_MSG_ERROR([*** Required libusb-1.0 >= 1.0.0 not installed ***]))
])
# ...
И заменяем исправляем 18-ую строку
# ThirdParty/configure.ac
# ...
AC_CHECK_LIB([usb-1.0], [libusb_init],, [native_libusb=no],)
# ...
При этом будут некоторые изменения генерации.
Во множестве строчках файла configure
все упоминания lusb
заменятся на lusb-1.0
.
В выходном заголовочном файле config.h
дефайн HAVE_LIBUSB
заменится на HAVE_LIBUSB_1_0
. Этот символ больше нигде не используется... Не знаю, зачем они его генерят.
После повторного запуска конфигурации лог проверки LibUsb уже будет успешным.
Переношу эти команды в CMake:
# ThirdPart/DfuUtilConfiguring.cmake
if (APPLE)
set(AutogenCommand ./autogen.sh)
set(ConfigureCommand ./configure --libdir=${DfuRoot}/lib --includedir=${DfuRoot}/include)
elseif (WIN32)
set(AutogenCommand bash autogen.sh)
set(ConfigureCommand bash configure USB_CFLAGS="-I${LibusbIncludeDir}" USB_LIBS="-L ${LibusbLibDir} -lusb-1.0")
endif ()
add_custom_target(dfu-util_autogen
COMMAND ${AutogenCommand}
COMMENT "Creating dfu-util configuration file..."
WORKING_DIRECTORY ${DfuUtilRoot})
add_custom_target(dfu-util_configure
COMMAND ${ConfigureCommand}
DEPENDS dfu-util_autogen
COMMENT "Configuring dfu-utils for current OS. Generating make files..."
WORKING_DIRECTORY ${DfuUtilRoot})
add_custom_target(dfu-util_build
COMMAND make
DEPENDS dfu-util_configure
COMMENT "Running make for dfu-utils project..."
WORKING_DIRECTORY ${DfuUtilRoot})
На комбинации MinGW + Ninja всё ожидаемо работает корректно и без проблем
Но я хочу адаптировать и на MSVC, так что ныряем в проблемы и продолжаем.
unistd.h
- это файл для POSIX системы, которого естественно нет на Windows. Указывать путь к заголовочникам MinGW безсполезно, т.к. MSVC с ума сойдет, если их увидит.
То, что мне никак не хотелось делать - это менять config.h
файл. Изначально планировалось оставить исходные файлы dfu-util как они есть, но все-таки придется немножко их изменить. После генерации с помощью config.h.in
формируется config.in
, которые в свою очередь декларирует, что в системе есть unistd.h
заголовочник. Подкорректирую этот момент с помощью CMake, чтобы генерация была незаметной для пользователя и не требовала ручного форматирования. Инструкция записи дополнительного кофига должна срабатывать автоматически после конфигурации, так что выносим её в отдельный файл и вызываем как POST_BUILD
после таргета dfu-util_configure
.
# ThirdParty/WriteExtraConfig.cmake
cmake_minimum_required(VERSION 3.23)
set(ConfigComment
"
/**
* Wor:
*/")
file(READ dfu-util/config.h Config)
string(FIND "${Config}" "${ConfigComment}" ConfigCommentIndex REVERSE)
if (${ConfigCommentIndex} EQUAL -1)
file(READ extraConfig.in ExtraConfig)
string(APPEND Config "${ConfigComment}\n${ExtraConfig}")
file(WRITE dfu-util/config.h "${Config}")
endif ()
# ThirdParty/DfuUtilConfiguring.cmake
# ...
add_custom_target(dfu-util_configure
COMMAND ${ConfigureCommand}
DEPENDS dfu-util_autogen
COMMENT "Configuring dfu-utils for current OS. Generating make files..."
WORKING_DIRECTORY ${DfuUtilRoot})
include(WriteExtraConfig.cmake)
add_custom_command(TARGET dfu-util_configure
POST_BUILD
COMMENT "Adding extra config to config.h file..."
COMMAND ${CMAKE_COMMAND}
-P ${CMAKE_CURRENT_SOURCE_DIR}/WriteExtraConfig.cmake
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
# ...
/**
* ThirdPart/extraConfig.in
*/
#if WIN32
#undef HAVE_UNISTD_H
#undef HAVE_NANOSLEEP
#include <basetsd.h>
#include <sys/types.h>
#ifndef SSIZE_MAX
#define SSIZE_MAX INTPTR_MAX
#endif
typedef SSIZE_T ssize_t;
#endif
Возможные проблемы с разыменованием переменных в CMake
Изначально код выглядел вот так.
# ThirdParty/WriteExtraConfig.cmake
cmake_minimum_required(VERSION 3.23)
set(ConfigComment
"
/**
* Wor:
*/")
file(READ dfu-util/config.h Config)
string(FIND "${Config}" "${ConfigComment}" ConfigCommentIndex REVERSE)
if (${ConfigCommentIndex} EQUAL -1)
file(READ extraConfig.in ExtraConfig)
string(APPEND Config ${ConfigComment} "\n" ${ExtraConfig})
string(REPLACE "%" "\;" Config "${Config}")
file(WRITE dfu-util/config.h ${Config})
endif ()
/**
* ThirdPart/extraConfig.in
*/
/* ... */
typedef SSIZE_T ssize_t%
/* ... */
Можете взять паузу и попробовать найти ошибку в нём и причину существование строки 15 в файле ThirdParty/WriteExtraConfig.cmake
и строки 13 файла extraConfig.in
.
* Секции Variable References и Lists из офф. мануала CMake подскажут решение.
Дело достаточно банально. Сначала я испытал проблемы с парсингом символа ';
' в CMake и посчитал, что это и справедливо. Точка с запятой является знаком разделения элементов массива, а строка - это такой же массив. Приняв это как данное, я просто заменил в файле extraConfig.in
символ ';
' на '%
' и меняю обратно их в CMake.
В момент проверки статьи задумался, что не может же быть такой баг в системе сборки, и решил посмотреть внимательнее.
Приведу небольшой пример:
include(CMakePrintHelpers)
set(Var1 1;2;3;4;5)
cmake_print_variables(Var1) -> Var1="1;2;3;4;5"
message(WARNING ${Var1}) -> 12345
message(WARNING "${Var1}") -> 1;2;3;4;5
set(Var2 "1;2;3;4;5")
cmake_print_variables(Var2) -> Var2="1;2;3;4;5"
message(WARNING ${Var2}) -> 12345
message(WARNING "${Var2}") -> 1;2;3;4;5
set(StrVar1 "Hel;lo")
cmake_print_variables(StrVar1) -> StrVar1="Hel;lo"
message(WARNING ${StrVar1}) -> Hello
message(WARNING "${StrVar1}") -> Hel;lo
set(StrVar2 "Hel" "lo")
cmake_print_variables(StrVar1) -> StrVar2="Hel;lo"
message(WARNING ${StrVar2}) -> Hello
message(WARNING "${StrVar2}") -> Hel;lo
Данный фрагмент кода и ссылки на секции из документации CMake опишут происходящее намного лаконичнее, чем я.
Пробую исполнение таргета WorDfuUtil
:
Возвращаем WorDfuUtil
-у тип библиотеки add_library(WorDfuUtil)
и проверяем на компиляцию статического и динамического типов.
Заключение
Какие можно подвести итоги? Для себя я выделял несколько целей написания и публикации статьи:
Когда я столкнулся на рабочих задачах с dfu-util, то основным тормозом
был ябыло как раз подключение инструмента. В принципе, подключение чего-то на языке Си без готового CMake-а не является чем-то необычным, но dfu-util привнесла новые краски в этот процесс. Поэтому первостепенной целью статьи было облегчить жизнь остальным участникам комьюнити. В целом, эту статью можно даже считать псевдо-гайдом по подключению неподключаемого кода Си и написанию CMakeList-ов.Во время написание статьи приходится разбираться в моментах, на которые обычно закрываешь глаза и пропускаешь. Можно назвать это насильственным обучением. Пришлось смотреть и тестить некоторые штуки в CMake и придумывать какие-то пути, чтобы не использовать костыли, которые я привык допускать для скорости выполнения задач.
* Привет моему лиду, который бомбит на дублирование кода,goto
и остальные снасти упрощения жизни.
Код вы можете посмотреть по ссылке, которая ведёт на версию библиотеки, соответствующую статье. Позже будут добавляться новые коммиты как от меня, так и, надеюсь, от других пользователей. Там же можете либо открывать issue на код, либо предлагать собственные варианта развития библиотеки. Я постараюсь периодически вносить обновления и писать новые классы.
dfu-util — прекрасная утилита. Она может показаться достаточно кривой, если смотреть в исходный код, но определенно стоит внимания и внесению вклада со стороны комьюнити.
P.S. Не знаю, как это делается на хабре, так что скажу здесь. Если у вас есть примеры библиотек или утилит, которые будет интересно адаптировать под подключение к CMake, я постараюсь поработать и над ними.