Все началось с того, что нашей команде прилетел жирный намек на покачаться в сторону системной разработки под яблочную платформу из за наклевывающихся контрактов. А мы все на виндофс пишем и вижуал студию одобряем который год - так что разнообразие не повредит.
Ну а чтобы покачаться в разработке под платформу самое лучше – написать какой-нить системный утиль, а тут 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.
domix32
Звучит это все конечно как необходимость написать какую-то утилиту, которая в интерактивном режиме сделает все как надо.
chepaika Автор
Самым простым решением, как мне кажется, будет добавление в SPM systemLibrary доп параметра который будет расширять PKG_CONFIG_PATH.
Хотя в SPM 5.6 вроде добавили Context который тащит окружение но пока не даёт менять. Мб позже через него будет можно переменные окружения выставлять