Все началось с того, что нашей команде прилетел жирный намек на покачаться в сторону системной разработки под яблочную платформу из за наклевывающихся контрактов. А мы все на виндофс пишем и вижуал студию одобряем который год — так что разнообразие не повредит.
Ну а чтобы покачаться в разработке под платформу самое лучше – написать какой-нить системный утиль, а тут Fugu14 выкатили поэтому я решил написать небольшую систему дампа фримвари для айфонов. И в качестве начала было решено переписать igetnonce на swift.
Почему swift? – ну неповторимый оригинал уже на Си, так что этот вариант отпадает, а красоту синтаксиса Objective-C я чет так и не оценил.
Посмотрев как что нынче носят в swift — я был крайне впечатлен концепцией пакетов и SPM – лаконичное описание для сборки проекта – это всегда приятно. По этой причине было решено реализовывать проект в виде пакета.
Чистый swift это конечно хорошо, однако igetnonce в качестве зависимостей тащит ряд Си-шных библиотек среди которых широко известная в кругах любителей jailbreak -ов libimobiledevice. С нее то и начались мои проблемы 🙂
Swift и Си-библиотеки
Но для начала давайте обсудим как вообще связать swift и Си-шную библиотеку. Толковой информации об этом в интернете не то чтобы много – могу порекомендовать эту и эту, однако и они не достаточно полно описывают то как это правильно сделать.
Но тут (внезапно) на помощь приходит официальная документация – которая гуглится через коленку, и содержит пару ошибок… Так что думаю ничего страшного не будет от того, что я тут продублирую шаги документации с некоторыми исправлениями.
Для работы с Си-шными библиотеками в swift требуется создать специальный пакет-обертку.
mkdir Clibimobiledevice # конвенция именований в формате Clibname описана в официальной доке так что не будем ее нарушать cd Clibimobiledevice swift package init --type system-module
В результате получаем следующую структуру файлов:
Clibimobiledevice ├── Package.swift ├── README.md └── module.modulemap
Однако данная структура ошибочна, о чем расскажу чуть позже, сейчас давайте просто приведем ее к правильному виду.
Clibimobiledevice % mkdir -p ./Source/Clibimobiledevice Clibimobiledevice % mv module.modulemap ./Source/Clibimobiledevice
В результате имеем следующую структуру:
Clibimobiledevice ├── Package.swift ├── README.md └── Source └── Clibimobiledevice └── module.modulemap
Так теперь следует отредактировать module.modulemap. Подробное описание формата modulemap приведено в официальной документации, но нам достаточно небольшого сабсета всех возможностей модулей, а именно нам нужно обьявить один системный модуль и выдернуть все содержимое заголовочных файлов libimobiledevice.
Получить путь до папки с заголовочными файлами можно с помощью команды brew --prefix libimobiledevice. В итоге module.modulemap будет иметь следующее содержимое:
Clibimobiledevice % cat > Source/Clibimobiledevice/module.modulemap module Clibimobiledevice [system] { header "/usr/local/opt/libimobiledevice/include/libimobiledevice/libimobiledevice.h" export * } ^D
Некоторые из озвученных далее проблем можно решить с помощью файла module.modulemap, однако это будут полумеры не до конца решающие проблему.
Теперь перейдем к файлу Package.swift. И отредактируем его следующим образом:
Clibimobiledevice % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Clibimobiledevice", products: [ .library(name: "Clibimobiledevice", targets: ["Clibimobiledevice"]), ], targets: [ .systemLibrary( name: "Clibimobiledevice", // path: pkgConfig: "libimobiledevice-1.0", providers: [ .brew(["libimobiledevice"]) ] ) ] ) ^D
Для взаимодействия с Си-шными библиотеками у SPM существует специальный таргет врапер – systemLibrary. Как можно увидеть из документации — параметр path по умолчанию смотрит в [PackageRoot]/Sources/[TargetName] — как раз поэтому нам и пришлось изменить структуру каталогов проекта ранее.
Так же данный таргет опционально готов получить на вход источник пакетов – в нашем случае brew и имя (именно имя, без полного пути и без расширения) pkg-config-а используемой Си-шной библиотеки. Об этом конфиге и о том причем тут brew дальше и пойдет речь.
В целом наш врапер уже готов и теперь надо создать проект использующий его функциональность:
Clibimobiledevice % cd .. % mkdir foo % cd foo % swift package init --type executable
В результате получаем следующую структуру файлов:
foo ├── Package.swift ├── README.md ├── Sources │ └── foo │ └── main.swift └── Tests └── fooTests └── fooTests.swift
Отредактируем Package.swift, чтобы добавить в зависимости Clibimobiledevice:
foo % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "foo", dependencies: [ .package(name: "Clibimobiledevice", path: "../Clibimobiledevice"), ], targets: [ .executableTarget( name: "foo", dependencies: [ .product(name: "Clibimobiledevice", package: "Clibimobiledevice") ]), .testTarget( name: "fooTests", dependencies: ["foo"]), ] ) ^D
И вызовем в main.swift какую-нибудь функцию libimobiledevice:
foo % cat > Sources/foo/main.swift import Clibimobiledevice idevice_set_debug_level(1) ^D
И попробуем собрать что получилось:
foo % swift build warning: you may be able to install libimobiledevice-1.0 using your system-packager: brew install libimobiledevice Undefined symbols for architecture x86_64: "_idevice_set_debug_level", referenced from: _foo_main in main.swift.o ld: symbol(s) not found for architecture x86_64 [2/3] Linking foo
И так у нас на лицо проблема линковки, плюс странный варнинг о том, что мы не установили libimobiledevice. Что могло пойти не так? Может у нас какая-то проблема с версией библиотеки? Может у нас армовая версия? Проверим это:
foo % ARCH=x86_64 jtool2 -S /usr/local/opt/libimobiledevice/lib/libimobiledevice-1.0.dylib | grep idevice_set_debug_level 0000000000003e89 T _idevice_set_debug_level
Да нет – нужный символы на месте. Тогда попробуем руками указать линковщику где искать нужные символы
foo % swift build -Xlinker -L/usr/local/opt/libimobiledevice/lib -Xlinker -limobiledevice-1.0 warning: you may be able to install libimobiledevice-1.0 using your system-packager: brew install libimobiledevice ld: warning: dylib (/usr/local/opt/libimobiledevice/lib/libimobiledevice-1.0.dylib) was built for newer macOS version (12.0) than being linked (10.10) [1/1] Build complete!
Все собралось. Можно считать это победой – передать флаги через LinkerSetting.unsafeFlags и пойти пить чай, но это же не наши методы.
Помните функцию systemLibrary формирующую таргет для Си-шных библотек?
static func systemLibrary( name: String, path: String? = nil, pkgConfig: String? = nil, providers: [SystemPackageProvider]? = nil ) -> Target
Нам интересен ее параметр pkg-config. Что такое pkg-config? если коротко – это утилита определяющая формат в котором библиотеки указывают необходимые для их сборки зависимости и флаги компиляции. Файлы для pkg-config имеют расширение .pc.
Мы в качестве такого файла указали libimobiledevice-1.0. Давайте взглянем на него чтобы немного освежить/познакомиться с форматом:
foo % cat /usr/local/opt/libimobiledevice/lib/pkgconfig/libimobiledevice-1.0.pc # объвляются константы сокращающие запись prefix=/usr/local/Cellar/libimobiledevice/1.3.0 exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir=${prefix}/include Name: libimobiledevice Description: A library to communicate with services running on Apple iOS devices. Version: 1.3.0 Libs: -L${libdir} -limobiledevice-1.0# флаги для ld Cflags: -I${includedir}# флаги для копилятора Requires: libplist-2.0 >= 2.2.0# зависимости Requires.private: libusbmuxd-2.0 >= 2.0.2 openssl >= 0.9.8
Как видим из конфига – флаги для ld аналогичны тем что использовали мы для успешной сборки и по замыслу лежащему в основе pkg-config SPM должен был сам вытащить эти флаги из конфига и подставить куда надо. А раз он этого не сделал то что-то пошло не так, да и варнинг выведенный при сборки только закрепляет мысль о том, что SPM не обработал наш .pc файл.
Таким образом возникает резонный вопрос где SPM ищет .pc файлы?
Для поиска ответа пришлось идти в исходники SPM. После некоторого времени, потраченного на поиски стало ясно что за обработку pkg-config-ов отвечает (ВНИМАНИЕ!) PkgConfig.swift, а за поиск – расположенная в нем структура PCFileFinder, в особенности функция locatePCFile.
Строчка 417 показывает все источники путей используемые SPM для поиска .pc файлов. А именно:
-
PCFileFinder.searchPaths– константа заданная в структуре-
/usr/local/lib/pkgconfig
-
/usr/local/share/pkgconfig
-
/usr/lib/pkgconfig
-
/usr/share/pkgconfig
-
-
PCFileFinder.pkgConfigPaths– является результатом выполнения командыpkg-config --variable pc_path pkg-configи на моей системе имело следующее содержимое:-
/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig
-
/usr/lib/pkgconfig
-
/usr/local/Homebrew/Library/Homebrew/os/mac/pkgconfig/10.15
-
-
customSearchPaths– складывается из содержимого переменной окруженияPKG_CONFIG_PATHи внешнего аргументаadditionalSearchPathsкоторый в случай использованияbrewбудет содержать/usr/local/opt/(NAME)/lib/pkgconfigсм тут и тут-
PKG_CONFIG_PATH -
/usr/local/opt/(NAME)/lib/pkgconfig
-
Исходя из собранных путей становиться ясно, что libimobiledevice-1.0.pc должен был быть найден еще на по пути /usr/local/lib/pkgconfig
foo % file /usr/local/lib/pkgconfig/libimobiledevice-1.0.pc /usr/local/lib/pkgconfig/libimobiledevice-1.0.pc: ASCII text
Но что же тогда пошло не так? Для того чтобы разобраться в этом было решено создать небольшой проект, который создаст экземпляр PkgConfig напрямую.
foo % cd .. % mkdir spm_test % cd spm_test spm_test % swift package init --type executable spm_test % cat > Package.swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "spm_test", platforms: [ .macOS("10.15.4") ], dependencies: [ // Dependencies declare other packages that this package depends on. .package(name: "SwiftPM", url: "https://github.com/apple/swift-package-manager.git", .revision("658654765f5a7dfb3456c37dafd3ed8cd8b363b4")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .executableTarget( name: "spm_test", dependencies: [ "SwiftPM" ]) ] ) ^D spm_test %cat > Sources/spm_test/main.swift import Basics import PackageLoading import PackageModel import TSCBasic typealias Diagnostic = Basics.Diagnostic // Подспер из тестов spm ))) struct Collector: ObservabilityHandlerProvider, DiagnosticsHandler { private let _diagnostics = ThreadSafeArrayStore<Diagnostic>() var diagnosticsHandler: DiagnosticsHandler { self } var diagnostics: [Diagnostic] { self._diagnostics.get() } func clear() { self._diagnostics.clear() } func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) { self._diagnostics.append(diagnostic) } } let collector = Collector() let observabilitySystem = ObservabilitySystem(collector) let observability = observabilitySystem.topScope.makeChildScope(description: "test") let result = try PkgConfig(name: "libimobiledevice-1.0", additionalSearchPaths: [], fileSystem: localFileSystem, observabilityScope: observability) print(result) ^D
Собираем и запускаем:
spm_test % ./.build/x86_64-apple-macosx/debug/spm_test ... [1086/1086] Build complete! spm_test % ./.build/x86_64-apple-macosx/debug/spm_test Swift/ErrorType.swift:200: Fatal error: Error raised at top level: couldn't find pc file for openssl zsh: illegal hardware instruction ./.build/x86_64-apple-macosx/debug/spm_test
Иииии вот она ошибка! Проблема в том что мы не можем найти .pc для openssl. Openssl действительно находился в списке зависимостей для libimobiledevice. Получается что PkgConfig рекурсивно ищет и разбирает .pc файлы для всех зависимостей и если с одной из них произойдет какая-то проблема то никаких внятных сообщений об ошибках в консоли не появиться, а только бесполезный варнинг о том что исходный пакет не установлен.
Попробуем установить openssl через brew:
spm_test % brew install openssl Running `brew update --preinstall`... … openssl@3 is keg-only, which means it was not symlinked into /usr/local, because macOS provides LibreSSL. If you need to have openssl@3 first in your PATH, run: echo 'export PATH="/usr/local/opt/openssl@3/bin:$PATH"' >> ~/.zshrc For compilers to find openssl@3 you may need to set: export LDFLAGS="-L/usr/local/opt/openssl@3/lib" export CPPFLAGS="-I/usr/local/opt/openssl@3/include" For pkg-config to find openssl@3 you may need to set: export PKG_CONFIG_PATH="/usr/local/opt/openssl@3/lib/pkgconfig" …
Как видно из логов brew установка нам не сильно поможет, так как brew не создает линков на необходимые нам .pc файлы. Тут есть два варианта:
-
создать линки самому – но это потребует аналогичных манипуляций при использовании пакета на другой машине, что гемор
-
использовать
PKG_CONFIG_PATH– этот вариант очевидно более гуманный если мы сможем прописать это в коде
Изменим наш тестовый проект:
spm_test %cat > Sources/spm_test/main.swift import Basics import Basics import PackageLoading import PackageModel import TSCBasic import Foundation typealias Diagnostic = Basics.Diagnostic // Подспер из тестов spm ))) struct Collector: ObservabilityHandlerProvider, DiagnosticsHandler { private let _diagnostics = ThreadSafeArrayStore<Diagnostic>() var diagnosticsHandler: DiagnosticsHandler { self } var diagnostics: [Diagnostic] { self._diagnostics.get() } func clear() { self._diagnostics.clear() } func handleDiagnostic(scope: ObservabilityScope, diagnostic: Diagnostic) { self._diagnostics.append(diagnostic) } } let collector = Collector() let observabilitySystem = ObservabilitySystem(collector) let observability = observabilitySystem.topScope.makeChildScope(description: "test") let pkg_config_path_env = "PKG_CONFIG_PATH" var pkg_config_path = "/usr/local/opt/openssl@3/lib/pkgconfig" if let current_pkg_config_path = ProcessInfo.processInfo.environment[pkg_config_path_env] { pkg_config_path = current_pkg_config_path + ":" + pkg_config_path } setenv(pkg_config_path_env, pkg_config_path, 1) let result = try PkgConfig(name: "libimobiledevice-1.0", additionalSearchPaths: [], fileSystem: localFileSystem, observabilityScope: observability) print(result) ^D
Соберем и запустим:
spm_test % swift build [3/3] Build complete! spm_test % ./.build/x86_64-apple-macosx/debug/spm_test PkgConfig(name: "libimobiledevice-1.0", pcFile: <AbsolutePath:"/usr/local/lib/pkgconfig/libimobiledevice-1.0.pc">, cFlags: ["-I/usr/local/Cellar/libimobiledevice/1.3.0/include", "-I/usr/local/Cellar/libplist/2.2.0/include", "-I/usr/local/Cellar/libusbmuxd/2.0.2/include", "-I/usr/local/Cellar/libplist/2.2.0/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include", "-I/usr/local/Cellar/openssl@3/3.0.1/include"], libs: ["-L/usr/local/Cellar/libimobiledevice/1.3.0/lib", "-limobiledevice-1.0", "-L/usr/local/Cellar/libplist/2.2.0/lib", "-lplist-2.0"])
БИНГО!!! Осталось реализовать аналогичную логику для пакета. Ииии это оказалось невозможно. Нет, правильнее сказать – я так и не понял как можно в пакете установить переменную окружения. Если кто-то знает как – буду рад такой информации.
Таким образом у нас остается только один путь:
spm_test % cat /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/openssl.pc prefix=/usr/local/Cellar/openssl@3/3.0.1 exec_prefix=${prefix} libdir=/usr/local/Cellar/openssl@3/3.0.1/lib includedir=${prefix}/include Name: OpenSSL Description: Secure Sockets Layer and cryptography libraries and tools Version: 3.0.1 Requires: libssl libcrypto spm_test % ln /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/openssl.pc /usr/local/lib/pkgconfig/openssl.pc spm_test % ln /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/libcrypto.pc /usr/local/lib/pkgconfig/libcrypto.pc spm_test % ln /usr/local/Cellar/openssl@3/3.0.1/lib/pkgconfig/libssl.pc /usr/local/lib/pkgconfig/libssl.pc spm_test % cd ../foo foo % swift build [0/0] Build complete!
Надеюсь данный материал поможет другим быстрее разобраться с проблемами линковки Си и Swift.
ссылка на оригинал статьи https://habr.com/ru/post/651885/
Добавить комментарий