Как написать и отладить плагин Swift Package Command
В этом году Apple выпустила новую функцию для диспетчера пакетов Swift: подключаемые модули пакетов Swift. Теперь мы можем написать два типа плагинов, которые автоматически интегрируются с Xcode:
- Создавайте (и предварительно создавайте) плагины.
- Командные плагины.
Я уже рассказывал о создании плагинов в нескольких статьях: Внедрите свой первый плагин для сборки пакетов Swift и Как использовать плагины Xcode в вашем приложении для iOS.
Сегодня я хочу поделиться с вами тем, какие шаги необходимы для создания командного плагина. Опыт разработки этих плагинов невелик, поэтому я также хочу поделиться методом их отладки.
Дополнительную информацию о подключаемых модулях пакетов Swift можно найти в следующих видеороликах WWDC22: Знакомство с подключаемыми модулями пакетов Swift, Создание подключаемых модулей пакетов Swift.
Плагин
В этой статье вы создадите плагин Command, который генерирует некоторый код Swift из спецификации JSON.
Чтобы создать плагин Command, вам понадобятся различные компоненты:
- Файл
Package.swift
для определения структуры плагина. - Правильная структура папок.
- Swift
struct
, соответствующий протоколуCommandPlugin
. - Бизнес-логика для реализации плагина.
- По желанию, дополнительный Package для тестирования плагина.
Пакет.swift
Плагины пакета Swift определяются как пакеты Swift. Вам нужно создать новый пакет для вашего плагина.
Типичная структура пакета командного плагина следующая:
Важные детали, на которые стоит обратить внимание:
swift-tool-version
в строке 1 должно быть не меньше5.7
.target
относится к типу.plugin
с возможностью.command
.
Чтобы полностью определить подключаемый модуль команды, нам нужно добавить еще пару свойств: intent
и набор permissions
.
intent
является причиной существования плагина. Он состоит из verb
и описания.
- Плагины команд также можно вызывать с помощью инструмента командной строки
swift package
. Свойствоverb
— это аргумент, который мы можем использовать в командной строке для вызова этого плагина Command. Синтаксис следующий:
swift package plugin <verb> [args...]
- Свойство
description
представляет собой удобочитаемое описание плагина.
Набор permissions
происходит из перечисления, определенного в Package API. Перечисление не имеет явных случаев, но предлагает единственную статическую функцию: writeToPackageDirectory
. Это сообщает Xcode, что вашему плагину требуется доступ для записи, и IDE будет предлагать пользователю сообщение всякий раз, когда вызывается команда. В приглашении будет отображаться строка reason
: удобочитаемое описание, объясняющее вашим пользователям, что плагин будет делать с этим разрешением.
Структура папок
Как и все пакеты Swift, плагины должны соответствовать правильной структуре папок для правильной сборки. Вы можете настроить структуру в Package.swift
, используя свойство path
, но структура папок по умолчанию такова:
CodeGeneratorPlugin ├── Package.swift └── Plugins └── CodeGenerator └── CodeGenerator.swift
CodeGeneratorPlugin
— это папка, содержащая текущий пакет. Папка Plugins
является домом для всех плагинов, определенных в папке Package.swift
. Код каждого плагина должен находиться в папке с тем же именем, что и у плагина, в данном примере CodeGenerator
.
Файл CodeGenerator.swift
является точкой входа для плагина и будет содержать его бизнес-логику. В отличие от других папок, ее не обязательно называть CodeGenerator
: файл Swift может иметь любое имя.
CodeGenerator.swift
Этот struct
является точкой входа плагина. Базовая структура примерно такая:
Важными фрагментами являются:
- Заявление
import PackagePlugin
. Он импортирует фреймворк со всеми новыми API для плагинов. - Аннотация
@main
. Он определяет точку входа для плагина. - Соответствие
CommandPlugin
. Он помечает структуру как правильный плагин Command и заставляет реализовать ее методы. - Метод
performCommand
. Он содержит логику плагина.
performCommand
имеет два параметра: context
и arguments
. context
можно использовать для чтения информации из пакета, например его пути. arguments
— это список аргументов, которые можно передать командам из Xcode или из командной строки.
Логика генерации кода
Это последний шаг для создания плагина. Вам нужно написать код, который генерирует код Swift, исходя из спецификаций JSON. Для этого вам могут понадобиться вспомогательные классы и некоторые функции.
Спецификация JSON
Первый шаг — определить данные, которые представляют ваши сущности JSON. В этом примере вы хотите сгенерировать некоторые структуры Swift. Эти структуры данных очень просты: они имеют только let
свойств.
JSON, который вы хотите проанализировать, имеет следующую структуру:
{ "fields": [{ "label": "<variable name>", "type": "<variable type>", ["subtype": "<variable type>"] }] }
Это объект JSON, представляющий одну структуру. У него есть свойство fields
, которое содержит другой объект, полностью определяющий свойство Swift. label
— это имя свойства в структуре; type
— это основной тип свойства. В случае дженериков требуется subtype
для указания типа дженерика.
Имя структуры будет именем файла. Таким образом, допустимым JSON будет следующий файл Person.json
:
{ "fields": [ { "label": "name", "type": "String", }, { "label": "surname", "type": "String", }, { "label": "age", "type": "Int", }, { "label": "family", "type": "Array", "subtype": "Person" } ] }
Этот тип Person
имеет имя, фамилию, возраст и семью, которая представляет собой список других типов Person
. После выполнения плагина вы ожидаете получить следующую структуру swift:
struct Person { let name: String, let surname: String, let age: Int let family: [Person] }
Модель данных
Чтобы правильно обрабатывать этот JSON в логике плагина, вам нужно правильно смоделировать его, чтобы вы могли его декодировать.
Для этого вам понадобятся эти две структуры:
Первый struct
— это оболочка, содержащая список полей. Он представляет объект JSON верхнего уровня.
Структура Field
— это модель данных, которая определяет внутренние объекты. У него есть свойство для label
, свойство для type
и необязательное свойство для subtype
на случай, если вам придется иметь дело с дженериком.
Логика
Наконец, мы можем реализовать логику. Вы можете разделить его на различные функции внутри самого плагина, чтобы упростить его.
Первая функция — это performCommand
, точка входа плагина:
Вы можете рассматривать его как Корень композиции вашего плагина: вы можете получить все соответствующие данные из контекста, создать экземпляры зависимостей и передать их остальной части кода.
performCommand
вызывает executeCommand
:
Эти методы извлекают все структуры, которые необходимо сгенерировать, используя метод drillDown
. Если структур нет, возвращается.
Затем он записывает структуры в файл с именем Struct.swift
: для простоты все структуры будут содержаться в одном файле.
Метод drillDown
отвечает за сканирование структуры папок пакета для поиска спецификаций JSON рекурсивным способом:
В этом примере предполагается, что все спецификации JSON
находятся в папке с именем Definitions
.
Метод drillDown
начинается с получения содержимого свойства directory
, которое по умолчанию является основной папкой пакета. Затем, если последним компонентом пути directory
является Definitions
, он извлекает полный путь каждого элемента, содержащегося в папке, и для каждого из них вызывает функцию createSwiftStruct
.
В противном случае он продолжает обход дерева: для каждого элемента в текущей папке он проверяет, является ли он папкой или нет. Если это папка, то он пытается drillDown
в нее аккумулировать результат в переменной, которая будет возвращена в конце рекурсии.
Последний метод — createSwiftStructure
:
Этот метод считывает содержимое файла, переданного в качестве параметра. Затем он пытается декодировать его, используя модель данных, определенную выше.
Затем он извлекает имя struct
из имени файла и создает список полей.
Наконец, он возвращает String
, который является допустимым Swift struct
.
Как использовать
Теперь пришло время попробовать ваш командный плагин в другом пакете.
Сначала создайте новый пакет в папке Package.swift
. Для этого вы можете просто добавить .target
к файлу Package.swift
:
Пакет также требует правильной структуры папок. Это должно быть что-то вроде этого:
CodeGeneratorPlugin ├── Package.swift └── Plugins │ └── CodeGenerator │ └── CodeGenerator.swift └── Sources └── MyCode └── HelloWorld.swift
HelloWorld.swift
— это просто пустой файл Swift: каждый пакет Swift должен иметь в своей папке хотя бы один файл Swift.
В этот момент, если вы щелкните правой кнопкой мыши проект CodeGeneratorPlugin
, вы уже увидите, что Xcode показывает ваш пользовательский плагин CodeGenerator
в контекстном меню!
Следующим шагом будет добавление папки Definitions
с файлом Person.json
, который мы описали выше.
Как только это будет сделано, щелкнув пункт меню CodeGenerator
в контекстном меню, Xcode представит диалоговое окно, позволяющее вам:
- Выберите, на какой цели должен работать плагин.
- Передайте дополнительные аргументы, если они необходимы.
В этом случае нам не нужен лишний аргумент и мы можем смело нажимать на Run
.
Теперь Xcode спрашивает, разрешено ли ему запускать команду.
Строка, начинающаяся с From the author
этого диалогового окна, показывает reason
, который вы установили в Package.swift
для вашего плагина на первом этапе этой статьи.
Нажав на Allow Command to Change Files
, Xcode выполнит команду. Через несколько секунд вы должны увидеть файл Structs.swift
под файлом HelloWorld.swift
.
Новый файл должен иметь следующее содержимое:
Поздравляем! Вы создали свой первый командный плагин и применили его к другой цели с помощью Xcode.
Отладка плагина
К сожалению, никто не может написать идеальный код без проб и ошибок. При разработке этого плагина я часто запускал его, чтобы проверить, работает ли он, но опыт разработчиков меня очень разочаровал:
- В начале плагин не генерировал никаких выходных данных, потому что он не работал.
- Xcode не выдавал никаких ошибок, с которыми я мог бы действовать.
- Не удалось подключить отладчик, чтобы посмотреть, что происходит.
- Функция Swift
print
никуда ничего не писала.
Мое решение для отладки этого плагина состояло в том, чтобы записывать каждый шаг в файл журнала. Для этого я выполнил следующие шаги:
- Я создал глобальную переменную
var log: [String]
variable. Он воссоздается каждый раз при запуске команды, поэтому проблем с разделением памяти между процессами не возникает. - Я создал функцию
log(_ message: String)
для добавления сообщений в переменнуюlog
. - Я создал функцию
printLog()
дляjoin
всех журналов и записи их в файлlogs
, который я могу проверить после выполнения. - Наконец, я дополнил свой код вызовами функции
log(_:)
, чтобы посмотреть, что происходит.
Примечание: это быстрая реализация и неэффективная. Он воссоздает файл каждый раз, перезаписывая все содержимое. Лучшим решением было бы добавить новые журналы в существующий файл.
С помощью этого трюка я смог зарегистрировать различные ошибки, с которыми я столкнулся, и успешно внедрить плагин.
Выводы
В сегодняшней статье вы узнали, как настроить плагин Command для Swift 5.7.
Вы узнали, как структурировать пакет и основные принципы его реализации. Вы также узнали, как запустить его в Xcode. Из-за плохого опыта разработчика вы узнали, как создать базовое решение для ведения журнала, чтобы увидеть различные этапы выполнения.
Командные плагины весьма полезны, но они были бы еще полезнее при использовании из командной строки: например, мы могли бы интегрировать пользовательские команды в наши среды непрерывной интеграции.
Я с нетерпением жду возможности увидеть, что сообщество создаст с помощью этих новых мощных инструментов!