Zależności
Gdy projekt się rozrasta, często dzieli się go na wiele celów, aby współdzielić kod, definiować granice i skrócić czas kompilacji. Wiele celów oznacza definiowanie zależności między nimi, tworząc graf zależności **** , który może obejmować również zależności zewnętrzne.
Skodyfikowane wykresy XcodeProj
Ze względu na konstrukcję Xcode i XcodeProj, utrzymanie wykresu zależności może być żmudnym i podatnym na błędy zadaniem. Oto kilka przykładów problemów, które można napotkać:
- Ponieważ system kompilacji Xcode wyprowadza wszystkie produkty projektu do tego samego katalogu w danych pochodnych, cele mogą być w stanie importować produkty, których nie powinny. Kompilacje mogą zakończyć się niepowodzeniem w CI, gdzie czyste kompilacje są bardziej powszechne, lub później, gdy używana jest inna konfiguracja.
- Przechodnie zależności dynamiczne celu muszą zostać skopiowane do dowolnego z katalogów, które są częścią ustawienia kompilacji
LD_RUNPATH_SEARCH_PATHS. Jeśli tak nie jest, cel nie będzie w stanie ich znaleźć w czasie wykonywania. Jest to łatwe do pomyślenia i skonfigurowania, gdy wykres jest mały, ale staje się problemem, gdy wykres rośnie. - Gdy cel łączy statyczny XCFramework, cel wymaga dodatkowej fazy kompilacji, aby Xcode mógł przetworzyć pakiet i wyodrębnić odpowiednie pliki binarne dla bieżącej platformy i architektury. Ta faza kompilacji nie jest dodawana automatycznie i łatwo jest zapomnieć o jej dodaniu.
Powyższe to tylko kilka przykładów, ale jest ich znacznie więcej, z którymi mieliśmy do czynienia na przestrzeni lat. Wyobraź sobie, że potrzebujesz zespołu inżynierów do utrzymania wykresu zależności i zapewnienia jego poprawności. Albo jeszcze gorzej, że zawiłości zostały rozwiązane w czasie kompilacji przez zamknięty system kompilacji, którego nie można kontrolować ani dostosowywać. Brzmi znajomo? Jest to podejście przyjęte przez Apple w Xcode i XcodeProj, które odziedziczył Swift Package Manager.
Głęboko wierzymy, że graf zależności powinien być jawny i statyczny ponieważ tylko wtedy może być zweryfikowany i zoptymalizowany. Dzięki Tuist skupiasz się na opisaniu, co zależy od czego, a my zajmujemy się resztą. Zawiłości i szczegóły implementacji są abstrahowane od Ciebie.
W poniższych sekcjach dowiesz się, jak zadeklarować zależności w swoim projekcie.
::: końcówka WALIDACJA GRAFU
Tuist waliduje graf podczas generowania projektu, aby upewnić się, że nie ma cykli i że wszystkie zależności są prawidłowe. Dzięki temu każdy zespół może wziąć udział w ewolucji grafu zależności bez obawy o jego uszkodzenie.
:::
Lokalne zależności
Obiekty docelowe mogą zależeć od innych obiektów docelowych w tym samym lub różnych projektach, a także od plików binarnych. Podczas tworzenia instancji celu `` można przekazać argument dependencies z dowolną z poniższych opcji:
Cel: Deklaruje zależność z celem w ramach tego samego projektu.Projekt: Deklaruje zależność z celem w innym projekcie.Framework: Deklaruje zależność z binarnym frameworkiem.Biblioteka: Deklaruje zależność z biblioteką binarną.XCFramework: Deklaruje zależność z binarnym XCFramework.SDK: Deklaruje zależność z systemowym SDK.XCTest: Deklaruje zależność z XCTest.
WARUNKI ZALEŻNOŚCI
Każdy typ zależności akceptuje warunek opcję warunkowego łączenia zależności w oparciu o platformę. Domyślnie łączy ona zależność dla wszystkich platform obsługiwanych przez cel.
Zależności zewnętrzne
Tuist pozwala również na deklarowanie zewnętrznych zależności w projekcie.
Pakiety Swift
Pakiety Swift to zalecany przez nas sposób deklarowania zależności w projekcie. Można je zintegrować za pomocą domyślnego mechanizmu integracji Xcode lub integracji opartej na XcodeProj Tuist.
Integracja Tuist oparta na XcodeProj
Domyślna integracja Xcode jest najwygodniejsza, ale brakuje jej elastyczności i kontroli, które są wymagane w przypadku średnich i dużych projektów. Aby temu zaradzić, Tuist oferuje integrację opartą na XcodeProj, która umożliwia integrację pakietów Swift w projekcie przy użyciu celów XcodeProj. Dzięki temu możemy nie tylko zapewnić większą kontrolę nad integracją, ale także uczynić ją kompatybilną z przepływami pracy, takimi jak
caching iselektywne uruchamianie testów.Integracja XcodeProj może zająć więcej czasu, aby obsługiwać nowe funkcje pakietów Swift lub obsługiwać więcej konfiguracji pakietów. Jednak logika mapowania między pakietami Swift i celami XcodeProj jest open-source i może być współtworzona przez społeczność. Jest to przeciwieństwo domyślnej integracji Xcode, która jest zamknięta i utrzymywana przez Apple.
Aby dodać zewnętrzne zależności, należy utworzyć plik Package.swift w sekcji Tuist/ lub w katalogu głównym projektu.
swift
// swift-tools-version: 5.9
import PackageDescription
#if TUIST
import ProjectDescription
import ProjectDescriptionHelpers
let packageSettings = PackageSettings(
productTypes: [
"Alamofire": .framework, // default is .staticFramework
]
)
#endif
let package = Package(
name: "PackageName",
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire", from: "5.0.0"),
],
targets: [
.binaryTarget(
name: "Sentry",
url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.40.1/Sentry.xcframework.zip",
checksum: "db928e6fdc30de1aa97200576d86d467880df710cf5eeb76af23997968d7b2c7"
),
]
):: tip USTAWIENIA PAKIETU
Instancja PackageSettings opakowana w dyrektywę kompilatora pozwala skonfigurować sposób integracji pakietów. Na przykład w powyższym przykładzie służy do zastąpienia domyślnego typu produktu używanego dla pakietów. Domyślnie nie powinno to być potrzebne.
:::
[Jeśli projekt używa niestandardowych konfiguracji kompilacji (konfiguracji innych niż standardowe
DebugiRelease), należy je określić wPackageSettingsużywającbaseSettings. Zewnętrzne zależności muszą wiedzieć o konfiguracjach projektu, aby kompilować się poprawnie. Na przykład:swift#if TUIST import ProjectDescription let packageSettings = PackageSettings( productTypes: [:], baseSettings: .settings(configurations: [ .debug(name: "Base"), .release(name: "Production") ]) ) #endifWięcej szczegółów można znaleźć w #8345.
Plik Package.swift to tylko interfejs do deklarowania zewnętrznych zależności, nic więcej. Dlatego w pakiecie nie definiuje się żadnych obiektów docelowych ani produktów. Po zdefiniowaniu zależności można uruchomić następujące polecenie, aby rozwiązać i pobrać zależności do katalogu Tuist/Dependencies:
bash
tuist install
# Resolving and fetching dependencies. {#resolving-and-fetching-dependencies}
# Installing Swift Package Manager dependencies. {#installing-swift-package-manager-dependencies}Jak być może zauważyłeś, stosujemy podejście podobne do CocoaPods', gdzie rozwiązywanie zależności jest osobnym poleceniem. Daje to użytkownikom kontrolę nad tym, kiedy chcą, aby zależności zostały rozwiązane i zaktualizowane, a także umożliwia otwarcie projektu Xcode i przygotowanie go do kompilacji. Jest to obszar, w którym uważamy, że doświadczenie programisty zapewniane przez integrację Apple z Menedżerem pakietów Swift pogarsza się z czasem wraz z rozwojem projektu.
Z poziomu celów projektu można następnie odwoływać się do tych zależności za pomocą typu zależności TargetDependency.external:
swift
import ProjectDescription
let project = Project(
name: "App",
organizationName: "tuist.io",
targets: [
.target(
name: "App",
destinations: [.iPhone],
product: .app,
bundleId: "dev.tuist.app",
deploymentTargets: .iOS("13.0"),
infoPlist: .default,
sources: ["Targets/App/Sources/**"],
dependencies: [
.external(name: "Alamofire"),
]
),
]
)BRAK SCHEMATÓW GENEROWANYCH DLA PAKIETÓW ZEWNĘTRZNYCH
Schematy **** nie są automatycznie tworzone dla projektów Swift Package, aby zachować czystość listy schematów. Można je utworzyć za pomocą interfejsu użytkownika Xcode.
Domyślna integracja Xcode
Jeśli chcesz korzystać z domyślnego mechanizmu integracji Xcode, możesz przekazać listę pakietów podczas tworzenia instancji projektu:
swift
let project = Project(name: "MyProject", packages: [
.remote(url: "https://github.com/krzyzanowskim/CryptoSwift", requirement: .exact("1.8.0"))
])A następnie odwołaj się do nich ze swoich celów:
swift
let target = .target(name: "MyTarget", dependencies: [
.package(product: "CryptoSwift", type: .runtime)
])W przypadku makr Swift i wtyczek Build Tool należy użyć odpowiednio typów .macro i .plugin.
SPM Build Tool Plugins
Wtyczki narzędzi kompilacji SPM muszą być zadeklarowane przy użyciu mechanizmu Domyślna integracja Xcode, nawet jeśli używana jest integracja Tuist Oparta na XcodeProj dla zależności projektu.
Praktycznym zastosowaniem wtyczki narzędzia kompilacji SPM jest wykonywanie lintingu kodu podczas fazy kompilacji Xcode "Run Build Tool Plug-ins". W manifeście pakietu jest to zdefiniowane w następujący sposób:
swift
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "Framework",
products: [
.library(name: "Framework", targets: ["Framework"]),
],
dependencies: [
.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", .upToNextMajor(from: "0.56.1")),
],
targets: [
.target(
name: "Framework",
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin"),
]
),
]
)Aby wygenerować projekt Xcode z nienaruszoną wtyczką narzędzia kompilacji, należy zadeklarować pakiet w manifeście projektu w tablicy packages, a następnie dołączyć pakiet o typie .plugin do zależności celu.
swift
import ProjectDescription
let project = Project(
name: "Framework",
packages: [
.remote(url: "https://github.com/SimplyDanny/SwiftLintPlugins", requirement: .upToNextMajor(from: "0.56.1")),
],
targets: [
.target(
name: "Framework",
dependencies: [
.package(product: "SwiftLintBuildToolPlugin", type: .plugin),
]
),
]
)Kartagina
Ponieważ Carthage wyświetla frameworks lub xcframeworks, można uruchomić carthage update, aby wyświetlić zależności w katalogu Carthage/Build, a następnie użyć typu zależności .framework lub .xcframework target, aby zadeklarować zależność w celu. Można to zawinąć w skrypt, który można uruchomić przed wygenerowaniem projektu.
bash
#!/usr/bin/env bash
carthage update
tuist generateBUDUJ I TESTUJ
Jeśli budujesz i testujesz swój projekt za pomocą tuist build i tuist test, musisz również upewnić się, że zależności rozwiązane przez Carthage są obecne, uruchamiając polecenie carthage update przed uruchomieniem tuist build lub tuist test.
CocoaPods
CocoaPods oczekuje projektu Xcode w celu zintegrowania zależności. Możesz użyć Tuist do wygenerowania projektu, a następnie uruchomić pod install, aby zintegrować zależności, tworząc obszar roboczy zawierający projekt i zależności Pods. Można to zawrzeć w skrypcie, który można uruchomić przed wygenerowaniem projektu.
bash
#!/usr/bin/env bash
tuist generate
pod installWARNING
Zależności CocoaPods nie są kompatybilne z przepływami pracy, takimi jak build lub test, które uruchamiają xcodebuild zaraz po wygenerowaniu projektu. Są one również niekompatybilne z buforowaniem binarnym i testowaniem selektywnym, ponieważ logika fingerprintingu nie uwzględnia zależności Pods.
Statyczny lub dynamiczny
Frameworki i biblioteki mogą być łączone statycznie lub dynamicznie, wybór, który ma znaczący wpływ na takie aspekty jak rozmiar aplikacji i czas uruchamiania. Pomimo swojego znaczenia, decyzja ta jest często podejmowana bez większego zastanowienia.
Ogólna zasada **** jest taka, że chcesz, aby jak najwięcej rzeczy było połączonych statycznie w kompilacjach wydania, aby osiągnąć szybki czas uruchamiania, a jak najwięcej rzeczy było połączonych dynamicznie w kompilacjach debugowania, aby osiągnąć szybki czas iteracji.
Wyzwanie związane ze zmianą pomiędzy statycznym i dynamicznym linkowaniem w grafie projektu nie jest trywialne w Xcode, ponieważ zmiana ma kaskadowy wpływ na cały graf (np. biblioteki nie mogą zawierać zasobów, statyczne frameworki nie muszą być osadzone). Apple próbowało rozwiązać ten problem za pomocą rozwiązań w czasie kompilacji, takich jak automatyczna decyzja Swift Package Manager pomiędzy statycznym i dynamicznym linkowaniem lub Mergeable Libraries. Rozwiązanie to dodaje jednak nowe dynamiczne zmienne do grafu kompilacji, dodając nowe źródła niedeterminizmu i potencjalnie powodując, że niektóre funkcje, takie jak Swift Previews, które opierają się na grafie kompilacji, stają się zawodne.
Na szczęście Tuist koncepcyjnie kompresuje złożoność związaną ze zmianą między statycznym i dynamicznym i syntetyzuje
bundle accessors, które są standardowe dla wszystkich typów linkowania.W połączeniu z
dynamicznymi konfiguracjami poprzez zmienne środowiskowe, możeszprzekazać typ łączenia w czasie wywołania i użyć wartości w swoich manifestach, aby ustawić typ produktu swoich celów.
swift
// Use the value returned by this function to set the product type of your targets.
func productType() -> Product {
if case let .string(linking) = Environment.linking {
return linking == "static" ? .staticFramework : .framework
} else {
return .framework
}
}Należy pamiętać, że Tuist
nie jest domyślnie wygodny poprzez niejawną konfigurację ze względu na jego koszty . Oznacza to, że polegamy na ustawieniu typu linkowania iwszelkich dodatkowych ustawień kompilacji, które są czasami wymagane, takich jak flaga linkera -ObjC, aby zapewnić poprawność wynikowych plików binarnych. W związku z tym nasze stanowisko polega na dostarczaniu zasobów, zwykle w postaci dokumentacji, w celu podejmowania właściwych decyzji.
PRZYKŁAD: ARCHITEKTURA KOMPOZYTOWA
Pakiet Swift, który integruje wiele projektów, to The Composable Architecture. Więcej szczegółów można znaleźć w tej sekcji.
Scenariusze
Istnieją pewne scenariusze, w których ustawienie linkowania całkowicie na statyczne lub dynamiczne nie jest wykonalne lub nie jest dobrym pomysłem. Poniżej znajduje się niewyczerpująca lista scenariuszy, w których może być konieczne połączenie statycznego i dynamicznego linkowania:
- Aplikacje z rozszerzeniami: Ponieważ aplikacje i ich rozszerzenia muszą współdzielić kod, może być konieczne uczynienie tych celów dynamicznymi. W przeciwnym razie ten sam kod zostanie zduplikowany zarówno w aplikacji, jak i rozszerzeniu, co spowoduje zwiększenie rozmiaru pliku binarnego.
- Wstępnie skompilowane zależności zewnętrzne: Czasami dostarczane są prekompilowane pliki binarne, które są statyczne lub dynamiczne. Statyczne pliki binarne mogą być opakowane w dynamiczne frameworki lub biblioteki do dynamicznego łączenia.
Podczas wprowadzania zmian w wykresie, Tuist przeanalizuje go i wyświetli ostrzeżenie, jeśli wykryje "statyczny efekt uboczny". Ostrzeżenie to ma na celu pomóc w zidentyfikowaniu problemów, które mogą pojawić się w wyniku statycznego łączenia celu, który zależy tranzytowo od celu statycznego za pośrednictwem celów dynamicznych. Te efekty uboczne często objawiają się zwiększonym rozmiarem pliku binarnego lub, w najgorszych przypadkach, awariami w czasie wykonywania.
Rozwiązywanie problemów
Zależności Objective-C
Podczas integrowania zależności Objective-C, włączenie pewnych flag w celu konsumpcji może być konieczne, aby uniknąć awarii w czasie wykonywania, jak opisano szczegółowo w Apple Technical Q&A QA1490.
Ponieważ system kompilacji i Tuist nie mają możliwości wywnioskowania, czy flaga jest konieczna, czy nie, i ponieważ flaga ta ma potencjalnie niepożądane skutki uboczne, Tuist nie zastosuje automatycznie żadnej z tych flag, a ponieważ Menedżer pakietów Swift uważa -ObjC za dołączoną za pośrednictwem .unsafeFlag, większość pakietów nie może dołączyć jej jako części swoich domyślnych ustawień łączenia, gdy jest to wymagane.
Konsumenci zależności Objective-C (lub wewnętrznych celów Objective-C) powinni stosować flagi -ObjC lub -force_load, gdy jest to wymagane, ustawiając OTHER_LDFLAGS na konsumowanych celach.
Firebase i inne biblioteki Google
Biblioteki open source Google - choć potężne - mogą być trudne do zintegrowania z Tuist, ponieważ często wykorzystują niestandardową architekturę i techniki w sposobie ich tworzenia.
Oto kilka wskazówek, które mogą być niezbędne do zintegrowania Firebase i innych bibliotek Google dla platformy Apple:
Upewnij się, że -ObjC jest dodany do OTHER_LDFLAGS
Wiele bibliotek Google jest napisanych w Objective-C. Z tego powodu każdy zużywający się cel będzie musiał zawierać znacznik -ObjC w swoim OTHER_LDFLAGS ustawieniu kompilacji. Można to ustawić w pliku .xcconfig lub ręcznie określić w ustawieniach celu w manifestach Tuist. Przykład:
swift
Target.target(
...
settings: .settings(
base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"]
)
...
)Więcej szczegółów znajduje się w sekcji Zależności Objective-C powyżej.
Ustaw typ produktu dla FBLPromises na dynamiczny framework {#set-the-product-type-for-fblpromises-to-dynamic-framework}.
Niektóre biblioteki Google zależą od FBLPromises, innej biblioteki Google. Możesz napotkać awarię, która wspomina o FBLPromises, wyglądającą mniej więcej tak:
NSInvalidArgumentException. Reason: -[FBLPromise HTTPBody]: unrecognized selector sent to instance 0x600000cb2640.Jawne ustawienie typu produktu FBLPromises na .framework w pliku Package.swift powinno rozwiązać problem:
swift
// swift-tools-version: 5.10
import PackageDescription
#if TUIST
import ProjectDescription
import ProjectDescriptionHelpers
let packageSettings = PackageSettings(
productTypes: [
"FBLPromises": .framework,
]
)
#endif
let package = Package(
...Architektura komponowalna
Jak opisano tutaj i sekcja rozwiązywania problemów, musisz ustawić ustawienie kompilacji OTHER_LDFLAGS na $(inherited) -ObjC podczas statycznego łączenia pakietów, co jest domyślnym typem łączenia Tuist. Alternatywnie można zastąpić typ produktu, aby pakiet był dynamiczny. Podczas łączenia statycznego, cele testowe i aplikacji zazwyczaj działają bez żadnych problemów, ale podglądy SwiftUI są uszkodzone. Można to rozwiązać, łącząc wszystko dynamicznie. W poniższym przykładzie Sharing jest również dodane jako zależność, ponieważ jest często używane razem z The Composable Architecture i ma swoje własne pułapki konfiguracyjne.
Poniższa konfiguracja połączy wszystko dynamicznie - więc aplikacja + cele testowe i podglądy SwiftUI działają.
::: końcówka STATYCZNA LUB DYNAMICZNA
Dynamiczne linkowanie nie zawsze jest zalecane. Więcej szczegółów można znaleźć w sekcji Statyczne lub dynamiczne. W tym przykładzie wszystkie zależności są łączone dynamicznie bez warunków dla uproszczenia.
:::
swift
// swift-tools-version: 6.0
import PackageDescription
#if TUIST
import enum ProjectDescription.Environment
import struct ProjectDescription.PackageSettings
let packageSettings = PackageSettings(
productTypes: [
"CasePaths": .framework,
"CasePathsCore": .framework,
"Clocks": .framework,
"CombineSchedulers": .framework,
"ComposableArchitecture": .framework,
"ConcurrencyExtras": .framework,
"CustomDump": .framework,
"Dependencies": .framework,
"DependenciesTestSupport": .framework,
"IdentifiedCollections": .framework,
"InternalCollectionsUtilities": .framework,
"IssueReporting": .framework,
"IssueReportingPackageSupport": .framework,
"IssueReportingTestSupport": .framework,
"OrderedCollections": .framework,
"Perception": .framework,
"PerceptionCore": .framework,
"Sharing": .framework,
"SnapshotTesting": .framework,
"SwiftNavigation": .framework,
"SwiftUINavigation": .framework,
"UIKitNavigation": .framework,
"XCTestDynamicOverlay": .framework
],
targetSettings: [
"ComposableArchitecture": .settings(base: [
"OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
]),
"Sharing": .settings(base: [
"PRODUCT_NAME": "SwiftSharing",
"OTHER_SWIFT_FLAGS": ["-module-alias", "Sharing=SwiftSharing"]
])
]
)
#endifWARNING
Zamiast import Sharing należy import SwiftSharing.
Przejściowe zależności statyczne przeciekające przez .swiftmodule
Gdy dynamiczny framework lub biblioteka zależy od statycznych poprzez import StaticSwiftModule, symbole są zawarte w .swiftmodule dynamicznego frameworka lub biblioteki, potencjalnie
zaimportować zależność statyczną za pomocą
`internal import`:swift
internal import StaticModule:: info
Poziom dostępu do importów został włączony w Swift 6. Jeśli używasz starszych wersji Swift, musisz użyć
`@_implementationOnly`zamiast tego:
:::
swift
@_implementationOnly import StaticModule