Русский
Русский
English
Статистика
Реклама

Spm

SPM модуляризация проекта для увеличения скорости сборки

11.11.2020 12:15:25 | Автор: admin
Привет, Хабр! Меня зовут Эрик Басаргин, я iOS-разработчик в Surf.

На одном большом проекте мы столкнулись с низкой скоростью сборки от трёх минут и более. Обычно в таких случаях студии практикуют модуляризацию проектов, чтобы не работать с огромными монолитами. Мы в Surf решили поэкспериментировать и модуляризовать проект с помощью Swift Package Manager менеджера зависимостей от Apple.

О результатах расскажем в другой статье, а сейчас ответим на главные вопросы: зачем это всё нужно, почему мы выбрали SPM и как делали первые шаги.



Почему именно SPM


Ответ прост это нативно и ново. Он не создает overhead в виде xcworkspace, как Cocoapods, к примеру. К тому же SPM open-source проект, который активно развивается. Apple и сообщество исправляют в нем баги, устраняют уязвимости, обновляют вслед за Swift.

Делает ли это сборку проекта быстрее


Теоретически сборка ускорится из-за самого факта разделения приложения на модули framework'и. Это значит, что каждый модуль будет собираться только в том случае, когда в него внесли изменения. Но утверждать точно можно будет только по окончанию эксперимента.

Note: Эффективность модуляризации напрямую зависит от правильного разбиения проекта на модули.

Как сделать эффективное разбиение


Метод разбиения зависит от выбранной архитектуры, типа приложения, его объема и планов на дальнейшее развитие. Поэтому я расскажу о трёх правилах, которых мы стараемся придерживаться при разбиении.

Разделять обобщённую функциональность. Каждый модуль отвечает за какую-то категорию, к примеру:

  • CommonAssets набор ваших Assets'ов и public интерфейс для доступа к ним. Обычно он генерируется с помощью SwiftGen.
  • CommonExtensions набор расширений, к примеру Foundation, UIKit, дополнительные зависимости.

Разделять flow'ы приложения. Рассмотрим древовидную структуру, где MainFlow главное flow приложения. Представим, что у нас новостное приложение.

  • NewFlow экраны новостей и обзора конкретной новости.
  • FavoritesFlow экран со списком избранных новостей и экран обзора конкретной новости с дополнительным функционалом.
  • SettingsFlow экраны настроек приложения, аккаунта, категорий и т. д.

Выносить reusable компоненты в отдельные модули:

  • CommonUIComponents модуль, который содержит в себе любые небольшие UI-компоненты. Обычно они влезают в один файл.
  • Далее список бесконечен и зависит от количества внутренних кастомных UI компонентов. К примеру, нужно создать модули для генерации экранов результата, кастомной коллекции, билдера кастомных алертов и т. д.

Когда нужно выносить компонент в отдельный модуль


Допустим, у нас есть конкретный flow, и мы хотим добавить в него новые функции. Если компонент будет переиспользоваться или это теоретически возможно в будущем лучше вынести его в отдельный модуль или аналог CommonUIComponents. В других случаях можно оставить компонент локальным.

Такой подход решает проблему с потерей компонентов. Это происходит в больших проектах, и если компонент не задокументировали, то его поддержка и отладка впоследствии станет убыточной.

Создаём проект с использованием SPM


Рассмотрим создание тривиального тестового проекта. Я использую Multiplatform App project на SwiftUI. Платформа и интерфейс тут не имеют значения.

Note: Чтобы быстро создать Multiplatform App, нужен XCode 12.2 beta.

Создаём проект и видим следующее:



Теперь создадим первый модуль Common:

  • добавляем папку Frameworks без создания директории;
  • создаём SPM-пакет Common.



  • Добавляем поддерживаемые платформы в файл Package.swift. У нас это platforms: [.iOS(.v14), .macOS(.v10_15)]



  • Теперь добавляем наш модуль в каждый таргет. У нас это SPMExampleProject для iOS и SPMExampleProject для macOS.



Note: Достаточно подключать к таргетам только корневые модули. Они не добавлены как подмодули.

Подключение завершено. Теперь достаточно настроить модуль с public интерфейсом и вуаля, первый модуль готов.

Как подключить зависимость у локального SPM-пакета


Добавим пакет AdditionalInfo как Common, но без добавления к таргетам. Теперь изменим Package.swift у Common пакета.



Добавлять больше ничего не нужно. Можно использовать.

Пример, приближенный к реальности


Подключим к нашему тестовому проекту SwiftGen и добавим модуль Palette он будет отвечать за доступ к палитре цветов, утвержденной дизайнером.

  1. Создаем новый корневой модуль по инструкции выше.
  2. Добавляем к нему корневые каталоги Scripts и Templates.
  3. Добавляем в корень модуля файл Palette.xcassets и пропишем какие-либо color set'ы.
  4. Добавляем пустой файл Palette.swift в Sources/Palette.
  5. Добавим в папку Templates шаблон palette.stencil.
  6. Теперь нужно прописать конфигурационный файл для SwiftGen. Для этого добавим файл swiftgen.yml в папку Scripts и пропишем в нем следующее:

xcassets:  inputs:    - ${SRCROOT}/Palette/Sources/Palette/Palette.xcassets  outputs:    - templatePath: ${SRCROOT}/Palette/Templates/palette.stencil      params:        bundle: .module        publicAccess: true      output: ${SRCROOT}/Palette/Sources/Palette/Palette.swift


Итоговый внешний вид модуля Palette

Модуль Palette мы с вами настроили. Теперь надо настроить запуск SwiftGen, чтобы палитра генерировалась при старте сборки. Для этого заходим в конфигурацию каждого таргета и создаем новую Build Phase назовём ее Palette generator. Не забудьте перенести эту Build Phase на максимально высокую позицию.

Теперь прописываем вызов для SwiftGen:

cd ${SRCROOT}/Palette/usr/bin/xcrun --sdk macosx swift run -c release swiftgen config run --config ./Scripts/swiftgen.yml

Note: /usr/bin/xcrun --sdk macosx очень важный префикс. Без него при сборке вылетит ошибка: unable to load standard library for target 'x86_64-apple-macosx10.15.


Пример вызова для SwiftGen

Готово доступ к цветам можно получить следующим образом:
Palette.myGreen (Color type in SwiftUI) и PaletteCore.myGreen (UIColor/NSColor).

Подводные камни


Перечислю то, с чем мы успели столкнуться.

  • Всплывают ошибки архитектуры и портят всю логику разбиения на модули.
  • SwiftLint & SwiftGen не уживаются вместе при подгрузке их через SPM. Причина в разных версиях yml.
  • В крупных проектах не получится сразу избавиться от Cocoapods. А разбивать уже созданный проект с закреплёнными версиями подов настоящее испытание, потому что SPM только развивается и не везде поддерживается. Но SPM и Cocoapods более-менее работают параллельно: разве что поды могут кидать ошибку MergeSwiftModule failed with a nonzero exit code. Это происходит довольно редко, а решается очисткой и пересборкой проекта.
  • На данный момент SPM не позволяет прописать пути поиска библиотек. Приходится явно указывать их с завязкой на -L$(BUILD_DIR).

SPM замена Bundler?


В этом вопросе предлагаю помечтать и подискутировать в комментариях. Тему нужно хорошо изучить, но выглядит она очень интересно. Кстати, уже есть интересная довольно близкая статья о плюсах и минусах SPM.

SPM дает возможность вызывать swift run, если добавить Package.swift в корень вашего проекта. Что это нам дает? К примеру, можно вызвать fastlane или swiftlint. Пример вызова:

swift run swiftlint --autocorrect.
Подробнее..

Создаем Swift Package на основе Cбиблиотеки

13.01.2021 02:07:50 | Автор: admin
Фото Kira auf der HeideнаUnsplashФото Kira auf der HeideнаUnsplash

Данная статья поможет вам создать свой первый Swift Package. Мы воспользуемся популярной C++ библиотекой для линейной алгебры Eigen, чтобы продемонстрировать, как можно обращаться к ней из Swift. Для простоты, мы портируем только часть возможностей Eigen.


Трудности взаимодействия C++ и Swift

Использование C++ кода из Swift в общем случае достаточно трудная задача. Все сильно зависит от того, какой именно код вы хотите портировать. Данные 2 языка не имеют соответствия API один-к-одному. Для подмножества языка C++ существуют автоматические генераторы Swift интерфейса (например,Scapix,Gluecodium). Они могут помочь вам, если разрабатывая библиотеку, вы готовы следовать некоторым ограничениям, чтобы ваш код просто конвертировался в другие языки. Тем не менее, если вы хотите портировать чужую библиотеку, то, как правило, это будет не просто. В таких ситуациях зачастую ваш единственный выбор: написать обертку вручную.

Команда Swift уже предоставляет interop дляCиObjective-Cв их инструментарии. В то же время,C++ interopтолько запланирован и не имеет четких временных рамок по реализации. Одна из сложно портируемых возможностей C++ шаблоны. Может показаться, что темплейты в C++ и дженерики в Swift схожи. Тем не менее, у них есть важные отличия. На момент написания данной статьи, Swift не поддерживает параметры шаблона не являющиеся типом, template template параметры и variadic параметры. Также, дженерики в Swift определяются для типов параметров, которые соблюдают объявленные ограничения (похоже на C++20 concepts). Также, в C++ шаблоны подставляют конкретный тип в месте вызова шаблона и проверяют поддерживает ли тип используемый синтаксис внутри шаблона.

Итого, если вам нужно портировать C++ библиотеку с обилием шаблонов, то ожидайте сложностей!


Постановка задачи

Давайте попробуем портировать вручную С++ библиотеку Eigen, в которой активно используются шаблоны. Эта популярная библиотека для линейной алгебры содержит определения для матриц, векторов и численных алгоритмов над ними. Базовой стратегией нашей обертки будет: выбрать конкретный тип, обернуть его в Objective-C класс, который будет импортироваться в Swift.

Один из способов импортировать Objective-C API в Swift это добавить C++ библиотеку напрямую в Xcode проект и написатьbridging header. Тем не менее, обычно удобнее, когда обертка компилируется в качестве отдельного модуля. В этом случае, вам понадобится помощь менеджера пакетов. Команда Swift активно продвигаетSwift Package Manager (SPM). Исторически, в SPM отсутствовали некоторые важные возможности, из-за чего многие разработчики не могли перейти на него. Однако, SPM активно улучшался с момента его создания. В Xcode 12, вы можете добавлять в пакет произвольные ресурсы и даже попробовать пакет в Swift playground.

В данной статье мы создадим SPM пакетSwiftyEigen. В качестве конкретного типа мы возьмем вещественную float матрицу с произвольным числом строк и колонок. КлассMatrixбудет иметь конструктор, индексатор и метод вычисляющий обратную матрицу. Полный проект можно найти наGitHub.


Структура проекта

SPM имеет удобный шаблон для создания новой библиотеки:

foo@bar:~$ mkdir SwiftyEigen && cd SwiftyEigenfoo@bar:~/SwiftyEigen$ swift package initfoo@bar:~/SwiftyEigen$ git init && git add . && git commit -m 'Initial commit'

Далее, мы добавляем стороннюю библиотеку (Eigen) в качестве сабмодуля:

foo@bar:~/SwiftyEigen$ git submodule add https://gitlab.com/libeigen/eigen Sources/CPPfoo@bar:~/SwiftyEigen$ cd Sources/CPP && git checkout 3.3.9

Отредактируем манифест нашего пакета,Package.swift:

// swift-tools-version:5.3import PackageDescriptionlet package = Package(    name: "SwiftyEigen",    products: [        .library(            name: "SwiftyEigen",            targets: ["ObjCEigen", "SwiftyEigen"]        )    ],    dependencies: [],    targets: [        .target(            name: "ObjCEigen",            path: "Sources/ObjC",            cxxSettings: [                .headerSearchPath("../CPP/"),                .define("EIGEN_MPL2_ONLY")            ]        ),        .target(            name: "SwiftyEigen",            dependencies: ["ObjCEigen"],            path: "Sources/Swift"        )    ])

Манифест является рецептом для компиляции пакета. Сборочная система Swift соберет два отдельных таргета для Objective-C и Swift кода. SPM не позволяет смешивать несколько языков в одном таргете. Таргет ObjCEigen использует файлы из папкиSources/ObjC, добавляет папкуSources/CPP в header search paths, и опеделяетEIGEN_MPL2_ONLY, чтобы гарантировать лицензию MPL2 при использовании Eigen. ТаргетSwiftyEigen зависит отObjCEigen и использует файлы из папкиSources/Swift.


Ручная обертка

Теперь напишем заголовочный файл для Objective-C класса в папкеSources/ObjCEigen/include:

#pragma once#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface EIGMatrix: NSObject@property (readonly) ptrdiff_t rows;@property (readonly) ptrdiff_t cols;- (instancetype)init NS_UNAVAILABLE;+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(zeros(rows:cols:));+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)colsNS_SWIFT_NAME(identity(rows:cols:));- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(value(row:col:));- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)colNS_SWIFT_NAME(setValue(_:row:col:));- (EIGMatrix*)inverse;@endNS_ASSUME_NONNULL_END

У класса есть readonly свойства rows и cols, конструктор для нулевой и единичной матрицы, способы получить и изменить отдельные значения, и метод вычисления обратной матрицы.

Дальше напишем файл реализации вSources/ObjCEigen:

#import "EIGMatrix.h"#pragma clang diagnostic push#pragma clang diagnostic ignored "-Wdocumentation"#import <Eigen/Dense>#pragma clang diagnostic pop#import <iostream>using Matrix = Eigen::Matrix<float, Eigen::Dynamic, Eigen::Dynamic>;using Map = Eigen::Map<Matrix>;@interface EIGMatrix ()@property (readonly) Matrix matrix;- (instancetype)initWithMatrix:(Matrix)matrix;@end@implementation EIGMatrix- (instancetype)initWithMatrix:(Matrix)matrix {    self = [super init];    _matrix = matrix;    return self;}- (ptrdiff_t)rows {    return _matrix.rows();}- (ptrdiff_t)cols {    return _matrix.cols();}+ (instancetype)matrixWithZeros:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Zero(rows, cols)];}+ (instancetype)matrixWithIdentity:(ptrdiff_t)rows cols:(ptrdiff_t)cols {    return [[EIGMatrix alloc] initWithMatrix:Matrix::Identity(rows, cols)];}- (float)valueAtRow:(ptrdiff_t)row col:(ptrdiff_t)col {    return _matrix(row, col);}- (void)setValue:(float)value row:(ptrdiff_t)row col:(ptrdiff_t)col {    _matrix(row, col) = value;}- (instancetype)inverse {    const Matrix result = _matrix.inverse();    return [[EIGMatrix alloc] initWithMatrix:result];}- (NSString*)description {    std::stringstream buffer;    buffer << _matrix;    const std::string string = buffer.str();    return [NSString stringWithUTF8String:string.c_str()];}@end

Теперь сделаем Objective-C код видимым из Swift с помощью файла вSources/Swift(смотритеSwift Forums):

@_exported import ObjCEigen

И добавим индексирование для более чистого API:

extension EIGMatrix {    public subscript(row: Int, col: Int) -> Float {        get { return value(row: row, col: col) }        set { setValue(newValue, row: row, col: col) }    }}

Пример использования

Теперь мы можем воспользоваться классом вот так:

import SwiftyEigen// Create a new 3x3 identity matrixlet matrix = EIGMatrix.identity(rows: 3, cols: 3)// Change a specific valuelet row = 0let col = 1matrix[row, col] = -2// Calculate the inverse of a matrixlet inverseMatrix = matrix.inverse()

Наконец, мы можем составить простой проект, который продемонстрирует возможности нашего пакета, SwiftyEigen. Приложение позволит вносить значения в матрицу 2x2 и вычислять обратную матрицу. Для этого, создаем новый iOS проект в Xcode, перетаскиваем папку с пакетом из Finder в project navigator, чтобы добавить локальную зависимость, и добавляем фреймворк SwiftyEigen в общие настройки проекта. Далее пишем UI и радуемся:

Смотрите полный проект наGitHub.


Ссылки

Спасибо за внимание!

Подробнее..

ZERG что за зверь?

19.02.2021 14:12:20 | Автор: admin


Когда мы говорим о CI&CD, мы часто углубляемся в базовые инструменты автоматизации сборки, тестирования и доставки приложения фокусируемся на инструментах, но забываем осветить процессы, которые протекают во время отрезания и стабилизации релизов. Однако, не все готовые инструменты одинаково полезны, а какие-то кастомные процессы не укладываются в их покрытие. Приходится исследовать процессы и находить пути автоматизации для их оптимизации.

В нашей компании QA-инженеры используют Zephyr для отслеживания хода регресса, так как ручное и исследовательское тестирование нам не заменить автотестами. Но несмотря на это, автотесты у нас гоняются часто и в больших количествах, поэтому хочется иметь возможность опустить какие-то банальные проверки, которые были автоматизированы и дать тестировщикам заниматься более производительной и полезной работой.

У нас есть ночные прогоны, когда гоняются полные наборы тестов. Но на самой заре освоения Zephyr, нашим тестировщикам во время регресса приходилось скачивать xcresult, или ещё ранее plist, или junit xml, а затем проставлять соответствия зелёных и красных тестов в зефире руками. Это довольно рутинная операция, да и занимает она много времени, чтобы руками пройти 500-600 тестов. Такие вещи хочется отдать на откуп бездушной машине. Так родился ZERG.


Рождение зерга


Zephyr Enterprise Report Generator небольшая утилита, которая изначально умела только искать соответствия в отчёте тестов и отправлять в Zephyr их актуальные статусы. Позже утилита получила новые функции, но сегодня мы остановимся на поиске и отправке отчётов.
В Zephyr нам предлагается оперировать версиями, циклами и проходами (execution) тест кейсов. Каждая версия содержит произвольное количество циклов, а каждый цикл содержит в себе проходы кейсов. Такие проходы содержат в себе информацию о задаче (zephyr прекрасно интегрируется с jira и тест кейс это, по сути, задачка в jira), авторе, о статусе кейса, а также о том, кто занимается этим кейсом и о других необходимых деталях.
Для автоматизации проблемы, которую мы обозначили выше, нам важно разобраться в проставлении статуса кейса.

Работа с кодом


Но как соотнести тест в коде и тест в зефире? Здесь мы выбрали довольно простой и прямолинейный подход: для каждого теста мы добавляем в секцию комментариев ссылки на задачи в jira.


В комментариях также могут размещаться дополнительные параметры, но об этом позже.
Так, один тест в коде может покрывать несколько задач. Но работает и обратная логика. Для одной задачи может быть написано несколько тестов в коде. Их статусы будут учитываться при составлении отчёта.
Нам надо пройти по исходникам, извлечь все тест-классы и тесты, слинковать задачи с методами и соотнести это с отчётом о прохождении тестов ( xcresult или junit ).
Работать с самим кодом можно разными путями:
просто читать файлы и через регулярные выражения извлекать информацию
использовать SourceKit

Как бы то ни было, даже при использовании SourceKit мы не обойдёмся без регулярных выражений для извлечения идентификаторов задач из ссылок в комментариях.
На данном этапе нас не интересуют детали, поэтому отгородимся от них протоколом:



Нам надо получить тесты. Для этого опишем структуры:



Затем нам надо прочитать отчёт о прохождении тестов. ZERG был рождён ещё до переезда на xcresult, и поэтому умеет парсить plist и junit. Детали в этой статье нас всё ещё не интересуют, они будут приложены в коде. Поэтому отгородимся протоколам



Остается только слинковать тесты в коде с результатами тестов из отчетов.



Проверяем соответствие по имени класса и имени теста (в разных классах могут быть методы с одинаковыми именами) и нужен ли рефакторинг для теста. Если он нужен, то считаем, что он ненадёжный и выкидываем из статистики.

Работаем с зефиром


Теперь, когда мы прочитали отчёты о тестировании, нам надо их перевести в контекст zephyr. Для этого надо получить список версий проекта, соотнести с версией приложения (чтобы это так работало, необходимо, чтобы версия в зефире совпадала с версией в Info.plist вашего приложения, например, 2.56), выкачать циклы и проходы. А дальше соотнести проходы с нашими уже имеющимися отчётами.
Для этого нам надо реализовать в ZephyrAPI следующие методы:



Cпецификацию можно увидеть здесь: getzephyr.docs.apiary.io, а реализацию клиента в нашем репозитории.
Общий алгоритм довольно простой:



На этапе сопоставления проходов с отчётами есть тонкий момент, который необходимо учитывать: в zephyr api обновление execution отправлять удобнее всего пачками, где передаётся общий статус и список идентификаторов проходов. Нам нужно развернуть наши отчёты относительно тикетов и учесть n-m соотношение. Для одного кейса в зефире может быть несколько тестов в коде. Один тест в коде может покрывать несколько кейсов. Если для одного кейса есть n тестов в коде и один из них красный, то для такого кейса общий статус красный, однако если один из таких тестов покрывает m кейсов и он зелёный, то остальные кейсы не должны стать красными.
Поэтому мы оперируем сетами и ищем пересечение красных и зелёных. Всё, что попадает в пересечение, мы отнимаем из зелёных результатов и отправляем отредактированные сведения в zephyr.



Здесь ещё нужно отметить, что внутри команды мы договорились, что zerg не будет менять статус прохода, если:
1. Текущий статус blocked или failed (раньше для failed мы меняли статус, но сейчас отказались от практики, потому что хотим, чтобы тестировщики обращали внимание на красные автотесты во время регресса).
2. Если текущий статус pass и его поставил человек, а не zerg.
3. Если тест помечен как флакающий.

Интересности Zephyr API


При запросе циклов нам приходит json, который с первого взгляда невозможно систематизировать для десериализации. Всё дело в том, что запрос на получение циклов для версии, хотя по смыслу и должен возвращать массив циклов, на самом деле возвращает один объект, где каждое поле уникально и называется идентификатором цикла, который лежит в значении. Чтобы такое обработать, мы применяем несложные хаки:



Статусы прохождения тестов приходят в одном из запросов рядом с объектом запроса. Но их можно вынести заранее в enum:



При запросе циклов нужно передавать в параметры запроса версию и целочисленный идентификатор проекта. Но в запросе проходов для цикла надо этот же идентификатор проекта передавать в формате строки.



Вместо заключения


Если есть много рутинной работы, то, скорее всего, что-то можно автоматизировать. Синхронизация прохождения автотестов с системой ведения тестов один из таких кейсов.
Таким образом, каждую ночь у нас проходит полный прогон тестов, а во время регресса отчёт отправляется QA-инженерам. Это снижает время регресса и даёт время на исследовательское тестирование.
Если правильно реализовать парсер андроид-исходников, то его можно с тем же успехом применять и для второй платформы.
Наш Zerg помимо сопоставления тестов ещё умеет в начальный импакт анализ, но об этом, возможно, в следующий раз.
Подробнее..

7 Кругов SPM или как сделать модульное приложение на Swift Package Manager

28.03.2021 20:06:28 | Автор: admin
Спасибо Jackie Zhao @jiaweizhao за фото на Unsplash Спасибо Jackie Zhao @jiaweizhao за фото на Unsplash

Думаю, многие разработчики сталкивались с задачей, когда нужно разбить проект на модули. В этой статье нет информации о том, как решать циклические зависимости или выделять по слоям функционал, подразумевается, что это уже реализовано. Цель статьи описать проблемы с которыми можно столкнуться при разбиении проекта на сабмодули с инструментом SPM, и помочь вам определиться стоит ли переходить на него.

Возможно, у многих возникнет вопрос: Зачем разбивать с помощью SPM?. Ведь можно просто создавать подпроекты. Можно, но в использовании SPM для разбиения есть несколько преимуществ:

  • С помощью SPM мы избавляемся от .xcodeproj файлов (забываем про конфликты в них);

  • Добавляем нативную возможность в будущем распространять наши модули. Допустим, вашему новому приложению понадобится использовать уже существующую авторизацию, тогда вам практически ничего не будет стоить распространение этого функционального модуля;

  • Нет альтернатив для проектов под разные операционные системы Linux/Windows;

  • Пакеты не требуют xcode для разработки.

Как выглядит сам процесс?

Для начала создаём Package.swift файл, в котором будут храниться все необходимые зависимости и информация о том, что из себя этот модуль представляет.

Пример файла с параметрами, с которыми вы скорей всего столкнётесь при работе:

import PackageDescription let package = Package(    // 1. Название нашего пакета    name: "Resources",    // 2. Платформы, которые поддерживаются нашим пакетом    platforms: [        .iOS(.v11),    ],    // 3. То, что другие программы будут брать в себя    // Продуктов может быть огромное колличество, хороший пример для этого Firebase SPM пакет    products: [        .library(            name: "Resources",            // Динамический или статический продукт          // по дефолту значение nil - SPM сам будет понимать что лучше подходит            //  преференция скорей всего будет отдаваться .static            type: .dynamic,            targets: ["Resources"]),    ],    // 4. Зависимости необходимые для работы нашего пакета,  // здесь они просто загружаются, добавляются они в targets    dependencies: [        // name - это название пакета(пункт 1), здесь нельзя указать кастомное название, необязательный параметр        .package(name: "R.swift.Library", url: "https://github.com/mac-cain13/R.swift.Library", .branch("master")),        // Пример подключения локального пакета        .package(path: "../Core")    ],    targets: [        // Это то из чего мы будем складывать наш продукт        // Для таргета обязательно нужно создать папку       // в Sources/имя_таргета для его работы      // либо если мы не хотим размещать его в Sources, можем указать "path:"        .target(            name: "Resources",            dependencies: [                // Здесь мы указываем зависимости которые мы хотим использовать в таргете                // name(пункт 3), package(пункт 1)                .product(name: "RswiftDynamic", package: "R.swift.Library")            ],            resources: [                // Все ресурсы которые мы хотим использовать нужно явно указать                // Путь к ним относительный от Sources/имя_пакета/то_что_мы_указали                // Если указываем папку, поиск идет рекурсивно                .process("Resources")            ])    ])

После создания модуля, подключаем его к проекту (либо к другому модулю, если модуль который вы делали лишь вспомогательный для других). Для удобства можно добавить пакет в workspace, просто drag and drop корневую папку с пакетом в Project navigator.

Введение на этом можно закончить и перейти к основной части статьи, где мы рассмотрим проблемы, с которыми вы можете столкнуться. На момент написания статьи мною использовались swift-tools-version:5.3, Xcode Version 12.2

Круг 1. Небольшое комьюнити.

Часто сложно найти информацию о проблемах с которыми можно столкнуться при работе с SPM. Из-за этого время затраченное на решение проблемы может значительно увеличиваться. Именно так и родилась идея для этой статьи: хочется поделится решениями, которые помогут сэкономить время.

Круг 2. Отсутствует фаза скриптов SPM пакета.

Встроенное решение скоро будет доступно, вот, например, предложение решения этой проблемы. Пока же, если вы хотите добавить линтинг или что-то похожее, есть несколько вариантов: добавлять их в главный таргет вашего проекта, ранить их по хукам в гите (например, на checkout) или просто из терминала.

Круг 3. R.swift и SPM.

Так как у нас нет .xcodeproject файла, вызвать R.swift скрипт для генерации не получится, для этого нам нужно этот файл создать.

Для создания можно использовать XcodeGen и его аналоги. Либо swift package generate-xcodeproj, стандартного скрипта для генерации .xcodeproj файла в SPM.

Думаю, что лучше использовать стандартное средство и не добавлять новых зависимостей. Но, к сожалению, такая генерация не совсем подходит для R.swift. Если попробовать вызвать скрипт на сгенеренном файле, то получим:

error: [R.swift] Project file at 'file:///Users/.../Resources.xcodeproj/' could not be parsed, is this a valid Xcode project file ending in *.xcodeproj?

Чтобы локализовать проблему и найти почему файл проекта не может распарситься нужен XcodeEdit фреймворк который R.swift использует для парсинга. Билдим его и получаем exec файл, которому нужно передать .xcodeproj. Вызываем его:

pathtoexec/XcodeEdit-Example Resources.xcodeproj

и видим

Fatal error: 'try!' expression unexpectedly raised an error: XcodeEdit_Example.AllObjectsError.fieldMissing(key: "buildRules"): file XcodeEdit_Example/main.swift, line 21

Вот как можно это решить:

sed -i '' -e 's/isa = "PBXNativeTarget";/isa = "PBXNativeTarget";buildRules = ();/' Resources.xcodeproj/project.pbxproj

К сожалению, на этом проблемы не заканчиваются. generate-xcodeproj не добавляет файлы ресурсов в проект. То есть R.swift будет парсить .xcodeproj/.pbproject, но нужных файлов ресурсов там нет.

Это можно решить подключением ruby gem xcodeproj, с помощью которого можно добавить необходимые файлы. Но всё же такой подход более трудозатратный, и лучше вернуться к варианту с .xcodeproj генераторам по типу XcodeGen.

Пример минимального конфиг файла для XcodeGen:

# Название генерируемого xcodeprojname: Resourcestargets: Resources:   # Не важно что вы тут укажите, но это обязательные параметры   type: framework   platform: iOS   # Root folder с которого начинать генерировать   sources:     - Sources

Команда которая создаст .xcodeproj файл:

xcodegen generate --spec Resources.yml

Проблема с .xcodeproj решена, вот пример полного скрипта для генерации R.swift:

# Создание xcodeprojgenerateXcodeProject() { xcodegen generate --spec Resources.yml} # Получаем buildSettings с помощью логов xcodebuild команды, лучше проверять, создан ли этот файл# Чтобы сократить время скрипта и не билдить каждый разgetBuildSettings() { xcodebuild -project "Resources.xcodeproj" -target "Resources" -showBuildSettings > buildSettings.txt} # Создание enviroment переменных без которых R.swift скрипт работать не будетparseEnvironmentVariables() { export SRCROOT="$(cat buildSettings.txt | grep -m1 "SRCROOT" | sed 's/^.*= //' )" export TARGET_NAME="$(cat buildSettings.txt | grep -m1 "TARGET_NAME" | sed 's/^.*= //' )" export PROJECT_FILE_PATH="$(cat buildSettings.txt | grep -m1 "PROJECT_FILE_PATH" | sed 's/^.*= //' )" export TARGET_NAME="$(cat buildSettings.txt | grep -m1 "TARGET_NAME" | sed 's/^.*= //' )" export PRODUCT_BUNDLE_IDENTIFIER="$(cat buildSettings.txt | grep -m1 "PRODUCT_BUNDLE_IDENTIFIER" | sed 's/^.*= //' )" export PRODUCT_MODULE_NAME="$(cat buildSettings.txt | grep -m1 "PRODUCT_MODULE_NAME" | sed 's/^.*= //' )" export TEMP_DIR="$(cat buildSettings.txt | grep -m1 "TEMP_DIR" | sed 's/^.*= //' )" export BUILT_PRODUCTS_DIR="$(cat buildSettings.txt | grep -m1 "BUILT_PRODUCTS_DIR" | sed 's/^.*= //' )" export DEVELOPER_DIR="$(cat buildSettings.txt | grep -m1 "DEVELOPER_DIR" | sed 's/^.*= //' )" export SOURCE_ROOT="$(cat buildSettings.txt | grep -m1 "SOURCE_ROOT" | sed 's/^.*= //' )" export SDKROOT="$(cat buildSettings.txt | grep -m1 "SDKROOT" | sed 's/^.*= //' )" export PLATFORM_DIR="$(cat buildSettings.txt | grep -m1 "PLATFORM_DIR" | sed 's/^.*= //' )" export INFOPLIST_FILE="$(cat buildSettings.txt | grep -m1 "INFOPLIST_FILE" | sed 's/^.*= //' )" export SCRIPT_INPUT_FILE_COUNT=1 export SCRIPT_INPUT_FILE_0="$TEMP_DIR/rswift-lastrun" export SCRIPT_OUTPUT_FILE_COUNT=1 export SCRIPT_OUTPUT_FILE_0="$SRCROOT/Sources/Resources/Generated/R.generated.swift"} # Вызов скрипта для генерацииrswift() { R.swift generate --accessLevel public "$SCRIPT_OUTPUT_FILE_0"} # Заменяем бандл в котором R.swift пытается найти ресурсы по дефолту# Bundle.module - расширение генерируется SPM, если вы в пакете указываете для таргета ресурсные зависимостиreplaceRSwiftHostingBundle() { sed -i '' -e 's/Bundle(for: R.Class.self)/Bundle.module/' ./Sources/Resources/Generated/R.generated.swift} mkdir Sources/Resources/GeneratedgenerateXcodeProjectgetBuildSettingsparseEnvironmentVariablesrswiftreplaceRSwiftHostingBundle

Круг 4. Cocoapods зависимость в SPM пакете.

Основная сложность в том, что нет нативного способа подключения к пакету Pod или fat фреймворков, а некоторые разработчики свои библиотеки на SPM переводить не торопятся. Здесь нам поможет proxy пакет-обертка с XCFramework. Если Pod, который нужно подключить к SPM опенсорсный, то можно скачать исходный код библиотеки и собрать XCFramework по этому гайду.

Если Pod распространяется как собранный бинарник через .framework, то придется действовать немного по-другому.

Как и в предыдущем случае, нужно собрать XCFramework. Для начала из fat фреймворка, который поставляется Pod-ом необходимо собрать 2 фреймворка для симулятора и для девайса. После этого объединим их в универсальный фреймворк. Вот скрипт, который делает XCFramework из фремворка с архитектурами arm64 и x86_64. Узнать архитектуры, которые поддерживает фреймворк можно с помощью команды lipo -info pathtoframework.

# Делаем 2 копии фреймворка для симулятора и устройстваcp -a YandexMapsMobile YandexMapsMobile_simcp -a YandexMapsMobile YandexMapsMobile_device cd YandexMapsMobile_sim/YandexMapsMobile.framework/Versions/A# Здесь мы выбираем какая архитектура нам нужна из fat фреймворка и выносим ее в отдельный фреймворкlipo -thin x86_64 YandexMapsMobile -output YandexMapsMobile_x86_64# Создание universal framework-a для симулятора# Если вы хотите собрать не под одну архитектуру симулятора# а под несколько (i386), для этого нужно будет сделать два раза thin# и соединить их в createlipo -create YandexMapsMobile_x86_64 -output YandexMapsMobile_simrm -rf YandexMapsMobile YandexMapsMobile_x86_64mv YandexMapsMobile_sim YandexMapsMobilecd ../../../.. # Просто дублирование кода, но сборка под девайсcd YandexMapsMobile_device/YandexMapsMobile.framework/Versions/Alipo -thin arm64 YandexMapsMobile -output YandexMapsMobile_arm64lipo -create YandexMapsMobile_arm64 -output YandexMapsMobile_devicerm -rf YandexMapsMobile YandexMapsMobile_arm64mv YandexMapsMobile_device YandexMapsMobilecd ../../../.. # Объединяем в xcframeworkxcodebuild -create-xcframework -framework YandexMapsMobile_sim/YandexMapsMobile.framework -framework YandexMapsMobile_device/YandexMapsMobile.framework -output YandexMapsMobile.xcframework

И после того как был сделан XCFramework, необходимо обернуть его в пакет с помощью binaryTarget. Также можно дополнительно обернуть его необходимыми зависимостями, подключив его в другой таргет:

let package = Package(    name: "YandexMapsMobileWrapper",    platforms: [        .iOS(.v11),    ],    products: [        .library(            name: "YandexMapsMobileWrapper",            type: .static,            // Используем таргет обертку над XCFramework и его зависимостями            targets: ["YandexMapsMobileWrapper"]),    ],    dependencies: [    ],    targets: [        // Подключаем наш фреймворк локально, также его можно добавлять и через url        .binaryTarget(name: "YandexMapsMobileBinary", path: "YandexMapsMobile.xcframework"),        // обертываем наш фреймворк зависимостями        .target(            name: "YandexMapsMobileWrapper",            dependencies: [                .target(name: "YandexMapsMobileBinary"),            ],            linkerSettings: [                .linkedFramework("CoreLocation"),                .linkedFramework("CoreTelephony"),                .linkedFramework("SystemConfiguration"),                .linkedLibrary("c++"),                .unsafeFlags(["-ObjC"]),            ]),    ])

Круг 5. Краш при попытке получить бандл SPM пакета (Bundle.module).

К сожалению, мне не удалось понять причину почему так происходит, но бывают случаи когда бандл с ресурсами, который на текущий момент называется yourpackagename_yourpackagename, не попадает в директорию, где его ждет код авто-сгенерированный пакетом. Как вариант, можно самостоятельно написать код, который делает автогенератор и добавить туда ещё один возможный путь.

private class BundleFinder {}  // Это копия автосгенерированого SPM расширения, есть 2 отличияpublic extension Bundle {        // Отличие 1, другое название, чтобы не было конфликтов    static var resourceBundle: Bundle = {         let bundleName = "Resources_Resources"        let candidates = [            Bundle.main.resourceURL,            Bundle(for: BundleFinder.self).resourceURL,            Bundle.main.bundleURL,            // Отличие 2, еще один путь где может лежать бандл с ресурсами            Bundle(for: BundleFinder.self).resourceURL?.deletingLastPathComponent().deletingLastPathComponent(),        ]        for candidate in candidates {            let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")            if let bundle = bundlePath.flatMap(Bundle.init(url:)) {                return bundle            }        }        fatalError("unable to find bundle named \(bundleName)")    }()    }

Круг 6. Блокирующая Resolve Swift Packages стадия.

Проблема проявляется когда все ваши SPM фреймворки подкачены проектом, и вы открываете ваш проект после его закрытия. Если SPM зависимостей много, то Xcode может резолвить их больше минуты. В течение этого времени Xcode может зависнуть и не отвечать на интеракции.

Круг 7. Other Linker Flags и SPM.

Может возникнуть ситуация, когда пакет должен линковаться со специальными флагами. Частый пример это ObjC флаг. Эти флаги для линковщика можно указать с помощью likerSettings: [.unsafeFlags([ObjC])] в таргете вашего пакета. К сожалению, если ваш пакет использует unsafeFlags, то его нельзя подключить к проекту напрямую, только через прокси-пакет, и только с указанием его как локальной или branch зависимости.

Вывод

Swift Package Manager удобный и интуитивно понятный нативный менеджер зависимостей. Однако, если вы планируете использовать его для разбивки приложения на подпроекты-пакеты, то нужно быть готовым к временным рискам и проблемам. К счастью, почти любая проблема связанная с SPM имеет свой workaround, но готовы ли вы идти на них и искать их?

Надеюсь решения проблем, с которыми я столкнулся помогут вам!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru