Xcode 11 и XCFrameworks: новый формат упаковки фреймворков


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


    В случае разработки под платформу iOS, да и в целом, экосистему Apple, есть два варианта подключать библиотеки в качестве зависимостей:


    1. Собирать их каждый раз при сборке приложения.
    2. Собирать их заранее, используя уже собранные зависимости.

    При выборе второго подхода становится логичным использовать CI/CD системы для сборки библиотек в готовые к употреблению артефакты.


    Однако, необходимость собирать библиотеки под несколько платформ или архитектур процессора в экосистеме Apple, зачастую, требует проводить не всегда тривиальные операции, как при сборке библиотеки, так и конечного продукта, который её использует.


    На этом фоне, было сложно не заметить и крайне интересно изучить, одно из нововведений от Apple, представленное на WWDC 2019 в рамках презентации Binary Frameworks in Swift — формат упаковки фреймворков — XCFramework.


    XCFramework имеет несколько преимуществ, в сравнении с устоявшимися подходами:


    1. Упаковка зависимостей под все целевые платформы и архитектуры в единый bundle из коробки.
    2. Подключение bundle в формате XCFramework, как единой зависимости для всех целевых платформ и архитектур.
    3. Отсутствие необходимости в сборке fat/universal фреймворка.
    4. Нет необходимости избавляться от x86_64 слайсов (slice) перед загрузкой конечных приложений в AppStore.

    В этой статье мы расскажем, зачем был внедрён этот новый формат, что он из себя представляет, а также, что он даёт разработчику.


    Как появился новый формат


    Ранее компанией Apple был выпущен менеджер зависимостей Swift Package Manager.
    Суть в том, что Swift PM позволяет выполнять поставку библиотек в форме открытого исходного кода с описанием зависимостей.


    С позиции разработчика, поставляющего библиотеку, хотелось бы выделить два аспекта Swift PM.


    • Очевидный минус — по тем или иным причинам, не все поставщики библиотек хотели бы открывать их исходный код потребителям.
    • Очевидный плюс — при компиляции зависимостей из исходников мы избавляемся от необходимости соблюдать бинарную совместимость библиотек.

    XCFramework Apple предлагает, как новый бинарный формат упаковки библиотек, рассматривая его как альтернативу Swift Packages.


    Этот формат, как и возможность подключения собранной в XCFramework библиотеки, доступен, начиная с Xcode 11 и его beta-версий.


    Что из себя представляет XCFramework


    По своей сути, XCFramework — новый способ упаковки и поставки библиотек, в их различных вариантах.


    Кроме прочего, новый формат также позволяет производить и упаковку статических библиотек вместе с их заголовочными файлами, в том числе и написанных на С/Objective-C.


    Рассмотрим формат несколько подробнее.


    1. Упаковка зависимостей под все целевые платформы и архитектуры в единый bundle из коробки


      Все сборки библиотеки под каждую из целевых платформ и архитектур теперь могут быть упакованы в единый bundle с расширением .xcframework.
      Однако, для этого, на данный момент времени, приходится использовать скрипты для вызова команды xcodebuild с новым для Xcode 11 ключом -create-xcframework.


      Процесс сборки и упаковки рассмотрим далее.


    2. Подключение bundle в формате XCFramework, как единой зависимости для всех целевых платформ и архитектур


      Поскольку bundle .xcframework содержит все необходимые варианты сборки зависимости, нам не нужно заботиться о его архитектуре и целевой платформе.


      В Xcode 11 библиотека, упакованная в .xcframework подключается точно также, как и обычный .framework.
      Говоря конкретнее, этого можно добиться следующими способами в настройках target’а:


      • добавляя .xcframework в раздел «Frameworks And Libraries» на вкладке «General»
      • добавляя .xcframework в «Link Binary With Libraries» на вкладке «Build Phases»

    3. Отсутствие необходимости в сборке fat/universal фреймворка


      Ранее для поддержки в подключаемой библиотеке нескольких платформ и нескольких архитектур приходилось подготавливать так называемые fat или universal фреймворки.
      Производилось это с помощью команды lipo для сшития всех вариантов собранного фреймворка в единый толстый бинарник.


      Подробнее с этим можно ознакомиться, к примеру, в следующих статьях:



    4. Нет необходимости избавляться от x86_64 слайсов (slice) перед загрузкой конечных приложений в AppStore


      Обычно такой слайс используется для обеспечения работы библиотек в симуляторе iOS.
      При попытке загрузить приложение с зависимостями, содержащими x86_64 слайс в AppStore можно столкнуться с небезызвестной ошибкой ITMS-90087.



    Создание и упаковка XCFramework: теория


    В упомянутой ранее презентации, описаны несколько шагов, которые требуются для сборки и упаковки библиотеки в формате XCFramework:


    1. Подготовка проекта


      Для начала во всех target’ах проекта, которые отвечают за сборку библиотеки под целевые платформы, нужно включить новую для Xcode 11 настройку — «Build Libraries for Distribution».



    2. Сборка проекта под целевые платформы и архитектуры


      Далее нам предстоит собрать все таргеты под целевые платформы и архитектуры.
      Рассмотрим примеры вызова команд на примере определённой конфигурации проекта.


      Допустим, что в проекте у нас есть две схемы «XCFrameworkExample-iOS» и «XCFramework-macOS».



      Также в проекте есть два target’а, которые собирают библиотеку под iOS и под macOS.



      Для сборки всех требуемых конфигураций библиотеки нам потребуется собрать оба target’а, используя соответствующие им схемы.
      Однако, для iOS нам потребуется две сборки: одна под конечные устройства (ARM), а другая под симулятор (x86_64).


      Итого нам потребуется собрать 3 фреймворка.


      Для этого можно воспользоваться командой xcodebuild:


      # iOS devices
      xcodebuild archive \
          -scheme XCFrameworkExample-iOS \
          -archivePath "./build/ios.xcarchive" \
          -sdk iphoneos \
          SKIP_INSTALL=NO
      # iOS simulator
      xcodebuild archive \
          -scheme XCFrameworkExample-iOS \
          -archivePath "./build/ios_sim.xcarchive" \
          -sdk iphonesimulator \
          SKIP_INSTALL=NO
      # macOS
      xcodebuild archive \
          -scheme XCFrameworkExample-macOS \
          -archivePath "./build/macos.xcarchive" \
          SKIP_INSTALL=NO

      В итоге у нас получилось 3 собранных фреймворка, которые далее мы будем упаковывать в контейнер .xcframework.


    3. Упаковка собранных .framework в .xcframework


      Сделать это можно следующей командой:


      xcodebuild -create-xcframework \
          -framework "./build/ios.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
          -framework "./build/ios_sim.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
          -framework "./build/macos.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
          -output "./build/XCFrameworkExample.xcframework"

      Здесь может быть множество указаний параметра -framework, которые указывают на все сборки .framework, которые требуется вложить в контейнер .xcframework.



    Подготовка проекта библиотеки для будущей сборки и упаковки XCFramework


    TL;DR: готовый проект можно скачать в репозитории на Github.


    В качестве примера реализуем библиотеку, которая будет доступна под две платформы: iOS и macOS.
    Воспользуемся упомянутой в предыдущем разделе статьи конфигурацией проекта: две схемы и два соответствующих им Framework target’а для платформ iOS и macOS.


    Сама же библиотека будет предоставлять нам простенький extension для String? (Optional where Wrapped == String), с единственным свойством.


    Назовём это свойство isNilOrEmpty и, как следует из названия, оно позволит нам узнать, когда внутри String? отсутствует значение или хранимая внутри строка является пустой.


    Код можно реализовать следующим образом:


    public extension Optional where Wrapped == String {
    
        var isNilOrEmpty: Bool {
            if case let .some(string) = self {
                return string.isEmpty
            }
            return true
        }
    
    }

    Приступим непосредственно к созданию и конфигурации проекта.


    1. Для начала нам нужно создать проект типа «Framework» под одну из двух целевых платформ на ваш выбор: iOS или macOS.


      Сделать это в Xcode можно через пункт меню «File» => «New» => «Project», либо сочетанием клавиш ⇧ + ⌘ + N (по умолчанию).


      Далее наверху диалога выбираем нужную платформу (iOS или macOS), выбираем тип проекта Framework и переходим далее по кнопке «Next».


      На следующем экране нам нужно задать имя проекта в поле «Product Name».


      Как вариант можно использовать «базовое» имя проекта, в упомянутой ранее конфигурации это «XCFrameworkExample».


      В дальнейшем, при конфигурации проекта к базовому имени, использованному в имени target’ов мы добавим суффиксы, обозначающие платформы.


    2. После этого требуется создать ещё один Target типа «Framework» в проекте под другую из перечисленных платформ (кроме той, под которую проект был создан изначально).


      Для этого можно воспользоваться пунктом меню «File» => «New» => «Target».


      Далее выбираем в диалоге другую (относительно выбранной в пункте 1) из двух платформ, после чего снова выбираем тип проекта «Framework».


      Для поля «Product Name» мы можем сразу использовать название с суффиксом платформы, для которой мы в данном пункте добавляем target. Так, если платформа — это macOS, то имя может быть «XCFrameworkExample-macOS» (%base_name%-%platform%).


    3. Настроим таргеты и схемы, чтобы их было проще отличать.


      Для начала переименуем наши схемы и привязанные к ним target’ы таким образом, чтобы их имена отображали платформы, например так:


      • «XCFrameworkExample-iOS»
      • «XCFrameworkExample-macOS»

    4. Далее добавляем в проект .swift файл с кодом нашего extension’а для String?


      Добавим в проект новый .swift файл с именем «Optional.swift».
      И в сам файл помещаем ранее упомянутый extension для Optional.


      Важно не забыть добавить файл с кодом к обеим target’ам.




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


    Процесс сборки и упаковки библиотеки в формат .xcframework


    Для сборки библиотеки и упаковки её в формат .xcframework на данном этапе можно воспользоваться bash-скриптом в отдельном файле. К тому же, это позволит в будущем использовать эти наработки для интеграции решения в CI/CD системы.


    Скрипт выглядит до безобразия просто и, по факту, сводит воедино ранее упомянутые команды для сборки:


    #!/bin/sh
    
    # ----------------------------------
    # BUILD PLATFORM SPECIFIC FRAMEWORKS
    # ----------------------------------
    
    # iOS devices
    xcodebuild archive \
        -scheme XCFrameworkExample-iOS \
        -archivePath "./build/ios.xcarchive" \
        -sdk iphoneos \
        SKIP_INSTALL=NO
    # iOS simulator
    xcodebuild archive \
        -scheme XCFrameworkExample-iOS \
        -archivePath "./build/ios_sim.xcarchive" \
        -sdk iphonesimulator \
        SKIP_INSTALL=NO
    # macOS
    xcodebuild archive \
        -scheme XCFrameworkExample-macOS \
        -archivePath "./build/macos.xcarchive" \
        SKIP_INSTALL=NO
    
    # -------------------
    # PACKAGE XCFRAMEWORK
    # -------------------
    
    xcodebuild -create-xcframework \
        -framework "./build/ios.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
        -framework "./build/ios_sim.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
        -framework "./build/macos.xcarchive/Products/Library/Frameworks/XCFrameworkExample.framework" \
        -output "./build/XCFrameworkExample.xcframework"

    Содержимое .xcframework


    В результате работы скрипта cборки из предыдущего пункта статьи, мы получаем заветный bundle .xcframework, который можно добавлять в проект.


    Если мы заглянем внутрь этого bundle, который, как и .framework, является по сути простой папкой, то увидим следующую структуру:



    Здесь мы видим, что внутри .xcframework лежат сборки в формате .framework, разбитые по платформам и архитектурам. Также для описания содержимого bundle .xcframework, внутри имеется файл Info.plist.


    Файл Info.plist имеет следующее содержание
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>AvailableLibraries</key>
        <array>
            <dict>
                <key>LibraryIdentifier</key>
                <string>ios-arm64</string>
                <key>LibraryPath</key>
                <string>XCFrameworkExample.framework</string>
                <key>SupportedArchitectures</key>
                <array>
                    <string>arm64</string>
                </array>
                <key>SupportedPlatform</key>
                <string>ios</string>
            </dict>
            <dict>
                <key>LibraryIdentifier</key>
                <string>ios-x86_64-simulator</string>
                <key>LibraryPath</key>
                <string>XCFrameworkExample.framework</string>
                <key>SupportedArchitectures</key>
                <array>
                    <string>x86_64</string>
                </array>
                <key>SupportedPlatform</key>
                <string>ios</string>
                <key>SupportedPlatformVariant</key>
                <string>simulator</string>
            </dict>
            <dict>
                <key>LibraryIdentifier</key>
                <string>macos-x86_64</string>
                <key>LibraryPath</key>
                <string>XCFrameworkExample.framework</string>
                <key>SupportedArchitectures</key>
                <array>
                    <string>x86_64</string>
                </array>
                <key>SupportedPlatform</key>
                <string>macos</string>
            </dict>
        </array>
        <key>CFBundlePackageType</key>
        <string>XFWK</string>
        <key>XCFrameworkFormatVersion</key>
        <string>1.0</string>
    </dict>
    </plist>

    Можно заметить, что для ключа «CFBundlePackageType», в отличии от формата .framework используется новое значение «XFWK», а не «FMWK».


    Итоги


    Итак, формат упаковки библиотек в XCFramework есть ни что иное, как обычный контейнер для библиотек собираемых в формате .framework.


    Однако, такой формат позволяет отдельно хранить и независимо использовать каждую из архитектур и платформ, представленных внутри. Это избавляет от ряда проблем, присущих широко распространённому подходу со сборкой fat/universal фреймворков.


    Как бы то ни было, на данный момент, есть немаловажный нюанс, касающийся вопроса использования XCFramework в реальных проектах — управление зависимостями, которое в формате XCFramework компанией Apple не реализовано.


    Для этих целей привычно используются Swift PM, Carthage, CocoaPods и прочие системы управления зависимостями и их сборки. Поэтому неудивительно, что поддержка нового формата уже находится на стадии реализации именно в проектах CocoaPods и Carthage.

    Only registered users can participate in poll. Log in, please.

    Что вы думаете о новом формате?

    • 4%фигня1
    • 52%интересно13
    • 40%когда завезут поддерку в CocoaPods и Carthage!?10
    • 0%попробовал, не понравилось0
    • 4%напишу в комментарии1
    True Engineering
    95.36
    Лаборатория технологических инноваций
    Share post

    Comments 2

      0

      Fastlane тоже занимаются поддержкой, https://github.com/CocoaPods/CocoaPods/issues/9148
      Это хорошо для CI сборок своих фреймворков.
      А как обновлять их в проектах? Руками подключать новую версию? И хранить подключённые фреймворки в репозитории проекта или как-то удобнее?

        0

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


        Если остановить свой выбор на Carthage, то можно воспользоваться поддержкой уже собранных зависимостей (Binary only frameworks), которые подтягиваются на основе файлов-спецификаций в формате JSON. Версионирование таких зависимостей немного отличается от работы с зависимостями, собираемыми из исходного кода, где конкретная версия библиотеки обозначается через релизный тэг в git. Разница в том, что версии зависимостей для Binary only frameworks определяются в файле-спецификации, а не на основе списка тэгов git.


        Таким образом, для поддержки этой фичи Carthage при сборке библиотек на CI/CD системе, потребуется хранить и обновлять файл-спецификацию со списком собранных артефактов и ссылками на них.


        Что касается хранения зависимостей в репозитории, тут возможны различные варианты.
        Если делать этого не хочется, то можно оставить подтягивание зависимостей на совести Carthage на этапе сборки проекта. При этом, если хочется — не обязательно включать папку с подтянутыми зависимости в репозирий, читай включить её в .gitignore. Просто при каждой сборке проекта будет выполняться дополнительная операция по стягиванию/синхронизации зависимостей.

      Only users with full accounts can post comments. Log in, please.