Adding support for linking system libraries and frameworks (#406)
Resolves https://github.com/tuist/tuist/issues/174 Continuation of https://github.com/tuist/tuist/pull/272 by @steprescott ### Short description In some cases specifying the status of the frameworks or libraries to link against is needed (e.g. weakly linking frameworks). ### Solution - A new `.sdk` dependency type is being introduced usage: ```swift Target(name: "App", platform: .iOS, product: .app, bundleId: "io.tuist.App", infoPlist: "Info.plist", sources: "Sources/**", dependencies: [ .sdk(name: "CloudKit.framework", status: .required), .sdk(name: "StoreKit.framework", status: .optional), .sdk(name: "libc++.tbd"), ]) ``` ### Test Plan - Verify unit tests pass via `swift test` - Verify acceptance tests pass via `bundle rake exec features` - Manually generate `fixtures/ios_app_with_sdk` via `tuist generate` - Verify the appropriate libraries are included in the generated project ### Notes Credit goes to @steprescott and [`XcodeGen`](https://github.com/yonaskolb/XcodeGen) - this PR is based on their work!
This commit is contained in:
parent
6a9594ed4a
commit
76b50a2f9d
|
@ -8,6 +8,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
- `DefaultSettings.none` to disable the generation of default build settings https://github.com/tuist/tuist/pull/395 by @pepibumur.
|
||||
- Version information for tuistenv https://github.com/tuist/tuist/pull/399 by @ollieatkinson
|
||||
- Add input & output paths for target action https://github.com/tuist/tuist/pull/353 by Rag0n
|
||||
- Adding support for linking system libraries and frameworks https://github.com/tuist/tuist/pull/353 by @steprescott
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -2,12 +2,63 @@ import Foundation
|
|||
|
||||
// MARK: - TargetDependency
|
||||
|
||||
public enum TargetDependency: Codable {
|
||||
/// Dependency status used by `.sdk` target dependencies
|
||||
public enum SDKStatus: String {
|
||||
/// Required dependency
|
||||
case required
|
||||
|
||||
/// Optional dependency (weakly linked)
|
||||
case optional
|
||||
}
|
||||
|
||||
/// Defines the target dependencies supported by Tuist
|
||||
public enum TargetDependency: Codable, Equatable {
|
||||
/// Dependency on another target within the same project
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the target to depend on
|
||||
case target(name: String)
|
||||
|
||||
/// Dependency on a target within another project
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - target: Name of the target to depend on
|
||||
/// - path: Relative path to the other project directory
|
||||
case project(target: String, path: String)
|
||||
|
||||
/// Dependency on a prebuilt framework
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: Relative path to the prebuilt framework
|
||||
case framework(path: String)
|
||||
|
||||
/// Dependency on prebuilt library
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: Relative path to the prebuilt library
|
||||
/// - publicHeaders: Relative path to the library's public headers directory
|
||||
/// - swiftModuleMap: Relative path to the library's swift module map file
|
||||
case library(path: String, publicHeaders: String, swiftModuleMap: String?)
|
||||
|
||||
/// Dependency on system library or framework
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the system library or framework (including extension)
|
||||
/// e.g. `ARKit.framework`, `libc++.tbd`
|
||||
/// - status: The dependency status (optional dependencies are weakly linked)
|
||||
case sdk(name: String, status: SDKStatus)
|
||||
|
||||
/// Dependency on system library or framework
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: Name of the system library or framework (including extension)
|
||||
/// e.g. `ARKit.framework`, `libc++.tbd`
|
||||
///
|
||||
/// Note: Defaults to using a `required` dependency status
|
||||
public static func sdk(name: String) -> TargetDependency {
|
||||
return .sdk(name: name, status: .required)
|
||||
}
|
||||
|
||||
public var typeName: String {
|
||||
switch self {
|
||||
case .target:
|
||||
|
@ -18,10 +69,16 @@ public enum TargetDependency: Codable {
|
|||
return "framework"
|
||||
case .library:
|
||||
return "library"
|
||||
case .sdk:
|
||||
return "sdk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SDKStatus (Coding)
|
||||
|
||||
extension SDKStatus: Codable {}
|
||||
|
||||
// MARK: - TargetDependency (Coding)
|
||||
|
||||
extension TargetDependency {
|
||||
|
@ -36,6 +93,7 @@ extension TargetDependency {
|
|||
case path
|
||||
case publicHeaders = "public_headers"
|
||||
case swiftModuleMap = "swift_module_map"
|
||||
case status
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
@ -63,6 +121,10 @@ extension TargetDependency {
|
|||
swiftModuleMap: try container.decodeIfPresent(String.self, forKey: .swiftModuleMap)
|
||||
)
|
||||
|
||||
case "sdk":
|
||||
self = .sdk(name: try container.decode(String.self, forKey: .name),
|
||||
status: try container.decode(SDKStatus.self, forKey: .status))
|
||||
|
||||
default:
|
||||
throw CodingError.unknownType(type)
|
||||
}
|
||||
|
@ -85,6 +147,9 @@ extension TargetDependency {
|
|||
try container.encode(path, forKey: .path)
|
||||
try container.encode(publicHeaders, forKey: .publicHeaders)
|
||||
try container.encodeIfPresent(swiftModuleMap, forKey: .swiftModuleMap)
|
||||
case let .sdk(name, status):
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(status, forKey: .status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,21 +226,29 @@ final class LinkGenerator: LinkGenerating {
|
|||
try dependencies
|
||||
.sorted()
|
||||
.forEach { dependency in
|
||||
if case let DependencyReference.absolute(path) = dependency {
|
||||
switch dependency {
|
||||
case let .absolute(path):
|
||||
guard let fileRef = fileElements.file(path: path) else {
|
||||
throw LinkGeneratorError.missingReference(path: path)
|
||||
}
|
||||
let buildFile = PBXBuildFile(file: fileRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
buildPhase.files?.append(buildFile)
|
||||
|
||||
} else if case let DependencyReference.product(name) = dependency {
|
||||
case let .product(name):
|
||||
guard let fileRef = fileElements.product(name: name) else {
|
||||
throw LinkGeneratorError.missingProduct(name: name)
|
||||
}
|
||||
let buildFile = PBXBuildFile(file: fileRef)
|
||||
pbxproj.add(object: buildFile)
|
||||
buildPhase.files?.append(buildFile)
|
||||
case let .sdk(sdkPath, sdkStatus):
|
||||
guard let fileRef = fileElements.sdk(path: sdkPath) else {
|
||||
throw LinkGeneratorError.missingReference(path: sdkPath)
|
||||
}
|
||||
|
||||
let buildFile = createSDKBuildFile(for: fileRef, status: sdkStatus)
|
||||
pbxproj.add(object: buildFile)
|
||||
buildPhase.files?.append(buildFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -317,6 +325,15 @@ final class LinkGenerator: LinkGenerating {
|
|||
pbxproj.add(object: buildPhase)
|
||||
pbxTarget.buildPhases.append(buildPhase)
|
||||
}
|
||||
|
||||
func createSDKBuildFile(for fileReference: PBXFileReference, status: SDKStatus) -> PBXBuildFile {
|
||||
var settings: [String: Any]?
|
||||
if status == .optional {
|
||||
settings = ["ATTRIBUTES": ["Weak"]]
|
||||
}
|
||||
return PBXBuildFile(file: fileReference,
|
||||
settings: settings)
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCBuildConfiguration {
|
||||
|
|
|
@ -30,6 +30,7 @@ class ProjectFileElements {
|
|||
|
||||
var elements: [AbsolutePath: PBXFileElement] = [:]
|
||||
var products: [String: PBXFileReference] = [:]
|
||||
var sdks: [AbsolutePath: PBXFileReference] = [:]
|
||||
let playgrounds: Playgrounding
|
||||
let filesSortener: ProjectFilesSortening
|
||||
|
||||
|
@ -230,13 +231,21 @@ class ProjectFileElements {
|
|||
filesGroup: ProjectGroup) throws {
|
||||
let sortedDependencies = dependencies.sorted(by: { $0.path < $1.path })
|
||||
try sortedDependencies.forEach { node in
|
||||
if let precompiledNode = node as? PrecompiledNode {
|
||||
switch node {
|
||||
case let precompiledNode as PrecompiledNode:
|
||||
let fileElement = GroupFileElement(path: precompiledNode.path,
|
||||
group: filesGroup)
|
||||
try generate(fileElement: fileElement,
|
||||
groups: groups,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
return
|
||||
case let sdkNode as SDKNode:
|
||||
generateSDKFileElement(node: sdkNode,
|
||||
toGroup: groups.frameworks,
|
||||
pbxproj: pbxproj)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -446,6 +455,31 @@ class ProjectFileElements {
|
|||
elements[fileAbsolutePath] = file
|
||||
}
|
||||
|
||||
private func generateSDKFileElement(node: SDKNode,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) {
|
||||
guard sdks[node.path] == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
addSDKElement(node: node, toGroup: toGroup, pbxproj: pbxproj)
|
||||
}
|
||||
|
||||
private func addSDKElement(node: SDKNode,
|
||||
toGroup: PBXGroup,
|
||||
pbxproj: PBXProj) {
|
||||
let sdkPath = node.path.relative(to: AbsolutePath("/")) // SDK paths are relative
|
||||
|
||||
let lastKnownFileType = sdkPath.extension.flatMap { Xcode.filetype(extension: $0) }
|
||||
let file = PBXFileReference(sourceTree: .sdkRoot,
|
||||
name: sdkPath.basename,
|
||||
lastKnownFileType: lastKnownFileType,
|
||||
path: sdkPath.pathString)
|
||||
pbxproj.add(object: file)
|
||||
toGroup.children.append(file)
|
||||
sdks[node.path] = file
|
||||
}
|
||||
|
||||
func group(path: AbsolutePath) -> PBXGroup? {
|
||||
return elements[path] as? PBXGroup
|
||||
}
|
||||
|
@ -454,6 +488,10 @@ class ProjectFileElements {
|
|||
return products[name]
|
||||
}
|
||||
|
||||
func sdk(path: AbsolutePath) -> PBXFileReference? {
|
||||
return sdks[path]
|
||||
}
|
||||
|
||||
func file(path: AbsolutePath) -> PBXFileReference? {
|
||||
return elements[path] as? PBXFileReference
|
||||
}
|
||||
|
|
|
@ -23,26 +23,7 @@ enum GraphError: FatalError {
|
|||
enum DependencyReference: Equatable, Comparable, Hashable {
|
||||
case absolute(AbsolutePath)
|
||||
case product(String)
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case let .absolute(path):
|
||||
hasher.combine(path)
|
||||
case let .product(product):
|
||||
hasher.combine(product)
|
||||
}
|
||||
}
|
||||
|
||||
static func == (lhs: DependencyReference, rhs: DependencyReference) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.absolute(lhsPath), .absolute(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
case let (.product(lhsName), .product(rhsName)):
|
||||
return lhsName == rhsName
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
case sdk(AbsolutePath, SDKStatus)
|
||||
|
||||
static func < (lhs: DependencyReference, rhs: DependencyReference) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
|
@ -50,6 +31,12 @@ enum DependencyReference: Equatable, Comparable, Hashable {
|
|||
return lhsPath < rhsPath
|
||||
case let (.product(lhsName), .product(rhsName)):
|
||||
return lhsName < rhsName
|
||||
case let (.sdk(lhsPath, _), .sdk(rhsPath, _)):
|
||||
return lhsPath < rhsPath
|
||||
case (.sdk, .absolute):
|
||||
return true
|
||||
case (.sdk, .product):
|
||||
return true
|
||||
case (.product, .absolute):
|
||||
return true
|
||||
default:
|
||||
|
@ -156,6 +143,13 @@ class Graph: Graphing {
|
|||
|
||||
var references: [DependencyReference] = []
|
||||
|
||||
// System libraries and frameworks
|
||||
let systemLibrariesAndFrameworks = targetNode.sdkDependencies.map {
|
||||
DependencyReference.sdk($0.path, $0.status)
|
||||
}
|
||||
|
||||
references.append(contentsOf: systemLibrariesAndFrameworks)
|
||||
|
||||
// Precompiled libraries and frameworks
|
||||
|
||||
let precompiledLibrariesAndFrameworks = targetNode.precompiledDependencies
|
||||
|
@ -322,6 +316,10 @@ extension TargetNode {
|
|||
fileprivate var frameworkDependencies: [FrameworkNode] {
|
||||
return dependencies.lazy.compactMap { $0 as? FrameworkNode }
|
||||
}
|
||||
|
||||
fileprivate var sdkDependencies: [SDKNode] {
|
||||
return dependencies.lazy.compactMap { $0 as? SDKNode }
|
||||
}
|
||||
}
|
||||
|
||||
extension Graph {
|
||||
|
|
|
@ -114,6 +114,8 @@ class TargetNode: GraphNode {
|
|||
projectPath: path,
|
||||
path: libraryPath,
|
||||
fileHandler: fileHandler, cache: cache)
|
||||
case let .sdk(name, status):
|
||||
return try SDKNode(name: name, status: status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +149,66 @@ enum PrecompiledNodeError: FatalError, Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
class SDKNode: GraphNode {
|
||||
enum `Type`: String, CaseIterable {
|
||||
case framework
|
||||
case library = "tbd"
|
||||
|
||||
static var supportedTypesDescription: String {
|
||||
let supportedTypes = allCases
|
||||
.map { ".\($0.rawValue)" }
|
||||
.joined(separator: ", ")
|
||||
return "[\(supportedTypes)]"
|
||||
}
|
||||
}
|
||||
|
||||
enum Error: FatalError, Equatable {
|
||||
case unsupported(sdk: String)
|
||||
var description: String {
|
||||
switch self {
|
||||
case let .unsupported(sdk):
|
||||
let supportedTypes = Type.supportedTypesDescription
|
||||
return "The SDK type of \(sdk) is not currently supported - only \(supportedTypes) are supported."
|
||||
}
|
||||
}
|
||||
|
||||
var type: ErrorType {
|
||||
switch self {
|
||||
case .unsupported:
|
||||
return .abort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let name: String
|
||||
let status: SDKStatus
|
||||
let type: Type
|
||||
|
||||
init(name: String, status: SDKStatus) throws {
|
||||
let sdk = AbsolutePath("/\(name)")
|
||||
|
||||
guard let sdkExtension = sdk.extension,
|
||||
let type = Type(rawValue: sdkExtension) else {
|
||||
throw Error.unsupported(sdk: name)
|
||||
}
|
||||
|
||||
self.name = name
|
||||
self.status = status
|
||||
self.type = type
|
||||
|
||||
let path: AbsolutePath
|
||||
|
||||
switch type {
|
||||
case .framework:
|
||||
path = AbsolutePath("/System/Library/Frameworks").appending(component: name)
|
||||
case .library:
|
||||
path = AbsolutePath("/usr/lib").appending(component: name)
|
||||
}
|
||||
|
||||
super.init(path: path)
|
||||
}
|
||||
}
|
||||
|
||||
class PrecompiledNode: GraphNode {
|
||||
enum Linking {
|
||||
case `static`, dynamic
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import Basic
|
||||
import Foundation
|
||||
|
||||
public enum SDKStatus {
|
||||
case required
|
||||
case optional
|
||||
}
|
||||
|
||||
public enum Dependency: Equatable {
|
||||
case target(name: String)
|
||||
case project(target: String, path: RelativePath)
|
||||
case framework(path: RelativePath)
|
||||
case library(path: RelativePath, publicHeaders: RelativePath, swiftModuleMap: RelativePath?)
|
||||
case sdk(name: String, status: SDKStatus)
|
||||
}
|
||||
|
|
|
@ -9,8 +9,6 @@ public enum InfoPlist: Equatable, ExpressibleByStringLiteral, ExpressibleByUnico
|
|||
switch (lhs, rhs) {
|
||||
case let (.file(lhsPath), .file(rhsPath)):
|
||||
return lhsPath == rhsPath
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -367,6 +367,9 @@ extension TuistGenerator.Dependency {
|
|||
return .library(path: RelativePath(libraryPath),
|
||||
publicHeaders: RelativePath(publicHeaders),
|
||||
swiftModuleMap: swiftModuleMap.map { RelativePath($0) })
|
||||
case let .sdk(name, status):
|
||||
return .sdk(name: name,
|
||||
status: .from(manifest: status))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -485,3 +488,14 @@ extension TuistGenerator.Platform {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TuistGenerator.SDKStatus {
|
||||
static func from(manifest: ProjectDescription.SDKStatus) -> TuistGenerator.SDKStatus {
|
||||
switch manifest {
|
||||
case .required:
|
||||
return .required
|
||||
case .optional:
|
||||
return .optional
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,4 +32,20 @@ final class TargetDependencyTests: XCTestCase {
|
|||
"""
|
||||
assertCodableEqualToJson(subject, expected)
|
||||
}
|
||||
|
||||
func test_sdk_codable() throws {
|
||||
// Given
|
||||
let sdks: [TargetDependency] = [
|
||||
.sdk(name: "A.framework"),
|
||||
.sdk(name: "B.framework", status: .required),
|
||||
.sdk(name: "c.framework", status: .optional),
|
||||
]
|
||||
|
||||
// When
|
||||
let encoded = try JSONEncoder().encode(sdks)
|
||||
let decoded = try JSONDecoder().decode([TargetDependency].self, from: encoded)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(decoded, sdks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,7 +233,7 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
pbxproj: pbxproj,
|
||||
fileElements: fileElements)
|
||||
|
||||
let buildPhase: PBXFrameworksBuildPhase? = pbxTarget.buildPhases.last as? PBXFrameworksBuildPhase
|
||||
let buildPhase = try pbxTarget.frameworksBuildPhase()
|
||||
|
||||
let testBuildFile: PBXBuildFile? = buildPhase?.files?.first
|
||||
let wakaBuildFile: PBXBuildFile? = buildPhase?.files?.last
|
||||
|
@ -272,6 +272,40 @@ final class LinkGeneratorErrorTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
|
||||
func test_generateLinkingPhase_sdkNodes() throws {
|
||||
// Given
|
||||
let dependencies: [DependencyReference] = [
|
||||
.sdk("/Strong/Foo.framework", .required),
|
||||
.sdk("/Weak/Bar.framework", .optional),
|
||||
]
|
||||
let pbxproj = PBXProj()
|
||||
let pbxTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = ProjectFileElements()
|
||||
let requiredFile = PBXFileReference(name: "required")
|
||||
let optionalFile = PBXFileReference(name: "optional")
|
||||
fileElements.sdks["/Strong/Foo.framework"] = requiredFile
|
||||
fileElements.sdks["/Weak/Bar.framework"] = optionalFile
|
||||
|
||||
// When
|
||||
try subject.generateLinkingPhase(dependencies: dependencies,
|
||||
pbxTarget: pbxTarget,
|
||||
pbxproj: pbxproj,
|
||||
fileElements: fileElements)
|
||||
|
||||
// Then
|
||||
let buildPhase = try pbxTarget.frameworksBuildPhase()
|
||||
|
||||
XCTAssertNotNil(buildPhase)
|
||||
XCTAssertEqual(buildPhase?.files?.map { $0.file }, [
|
||||
requiredFile,
|
||||
optionalFile,
|
||||
])
|
||||
XCTAssertEqual(buildPhase?.files?.map { $0.settings?.description }, [
|
||||
nil,
|
||||
"[\"ATTRIBUTES\": [\"Weak\"]]",
|
||||
])
|
||||
}
|
||||
|
||||
func test_generateCopyProductsdBuildPhase_staticTargetDependsOnStaticProducts() throws {
|
||||
// Given
|
||||
let path = AbsolutePath("/path/")
|
||||
|
|
|
@ -69,7 +69,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"myfolder/resources/a.png",
|
||||
])
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"my.folder/resources/a.png",
|
||||
])
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"myfolder/resources/generated_images",
|
||||
])
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"another/path/resources/a.png",
|
||||
])
|
||||
}
|
||||
|
@ -142,7 +142,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"myfolder/resources/assets.xcassets",
|
||||
])
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"myfolder/resources/assets.xcassets",
|
||||
])
|
||||
}
|
||||
|
@ -200,7 +200,7 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
|
||||
// Then
|
||||
let projectGroup = groups.main.group(named: "Project")
|
||||
XCTAssertEqual(projectGroup?.debugChildPaths, [
|
||||
XCTAssertEqual(projectGroup?.flattenedChildren, [
|
||||
"resources/App.strings/en",
|
||||
"resources/App.strings/fr",
|
||||
"resources/Extension.strings/en",
|
||||
|
@ -585,6 +585,36 @@ final class ProjectFileElementsTests: XCTestCase {
|
|||
sourceRootPath: AbsolutePath("/a/b/c/project"))
|
||||
XCTAssertEqual(got, RelativePath("../../../framework"))
|
||||
}
|
||||
|
||||
func test_generateDependencies_sdks() throws {
|
||||
// Given
|
||||
let pbxproj = PBXProj()
|
||||
let project = Project.test()
|
||||
let sourceRootPath = AbsolutePath("/a/project/")
|
||||
let groups = ProjectGroups.generate(project: project,
|
||||
pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath)
|
||||
|
||||
let sdk = try SDKNode(name: "ARKit.framework", status: .required)
|
||||
|
||||
// When
|
||||
try subject.generate(dependencies: [sdk],
|
||||
path: sourceRootPath,
|
||||
groups: groups, pbxproj: pbxproj,
|
||||
sourceRootPath: sourceRootPath,
|
||||
filesGroup: .group(name: "Project"))
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(groups.frameworks.flattenedChildren, [
|
||||
"ARKit.framework",
|
||||
])
|
||||
|
||||
let sdkElement = subject.sdks[sdk.path]
|
||||
XCTAssertNotNil(sdkElement)
|
||||
XCTAssertEqual(sdkElement?.sourceTree, .sdkRoot)
|
||||
XCTAssertEqual(sdkElement?.path, sdk.path.relative(to: "/").pathString)
|
||||
XCTAssertEqual(sdkElement?.name, sdk.path.basename)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PBXGroup {
|
||||
|
@ -597,11 +627,11 @@ private extension PBXGroup {
|
|||
/// -- D
|
||||
/// Would return:
|
||||
/// ["A/B", "A/C/D"]
|
||||
var debugChildPaths: [String] {
|
||||
var flattenedChildren: [String] {
|
||||
return children.flatMap { (element: PBXFileElement) -> [String] in
|
||||
switch element {
|
||||
case let group as PBXGroup:
|
||||
return group.debugChildPaths.map { group.nameOrPath + "/" + $0 }
|
||||
return group.flattenedChildren.map { group.nameOrPath + "/" + $0 }
|
||||
default:
|
||||
return [element.nameOrPath]
|
||||
}
|
||||
|
|
|
@ -198,3 +198,43 @@ final class LibraryNodeTests: XCTestCase {
|
|||
XCTAssertNotEqual(a1, b)
|
||||
}
|
||||
}
|
||||
|
||||
final class SDKNodeTests: XCTestCase {
|
||||
func test_sdk_supportedTypes() throws {
|
||||
// Given
|
||||
let libraries = [
|
||||
"Foo.framework",
|
||||
"libBar.tbd",
|
||||
]
|
||||
|
||||
// When / Then
|
||||
XCTAssertNoThrow(try libraries.map { try SDKNode(name: $0, status: .required) })
|
||||
}
|
||||
|
||||
func test_sdk_usupportedTypes() throws {
|
||||
XCTAssertThrowsError(try SDKNode(name: "FooBar", status: .required)) { error in
|
||||
XCTAssertEqual(error as? SDKNode.Error, .unsupported(sdk: "FooBar"))
|
||||
}
|
||||
}
|
||||
|
||||
func test_sdk_errors() {
|
||||
XCTAssertEqual(SDKNode.Error.unsupported(sdk: "Foo").type, .abort)
|
||||
}
|
||||
|
||||
func test_sdk_paths() throws {
|
||||
// Given
|
||||
let libraries = [
|
||||
"Foo.framework",
|
||||
"libBar.tbd",
|
||||
]
|
||||
|
||||
// When
|
||||
let nodes = try libraries.map { try SDKNode(name: $0, status: .required) }
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(nodes.map(\.path), [
|
||||
"/System/Library/Frameworks/Foo.framework",
|
||||
"/usr/lib/libBar.tbd",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,4 +60,22 @@ It defines a dependency with a pre-compiled framework, for example, a framework
|
|||
|
||||
It defines a dependency with a pre-compiled library. It allows specifying the path where the public headers or Swift module map is.
|
||||
|
||||
## System libraries and frameworks dependencies
|
||||
|
||||
```swift
|
||||
.sdk(name: "StoreKit.framework", status: .required)
|
||||
```
|
||||
|
||||
```swift
|
||||
.sdk(name: "ARKit.framework", status: .optional)
|
||||
```
|
||||
|
||||
```swift
|
||||
.sdk(name: "libc++.tbd")
|
||||
```
|
||||
|
||||
It defines a dependency on a system library (`.tbd`) or framework (`.framework`) and optionally if it is `required` or `optional` (i.e. gets weakly linked).
|
||||
|
||||
-----
|
||||
|
||||
As we mentioned, the beauty of defining your dependencies with Tuist is that when you generate the project, things are set up and ready for you to successfully compile your targets.
|
||||
|
|
|
@ -53,7 +53,7 @@ Dependencies:
|
|||
- App -> Framework2
|
||||
- Framework1 -> Framework2
|
||||
|
||||
# ios_app_with_framework_and_resources
|
||||
## ios_app_with_framework_and_resources
|
||||
|
||||
A workspace with an application that includes resources.
|
||||
|
||||
|
@ -100,6 +100,12 @@ Dependencies:
|
|||
- Framework1 -> Framework3
|
||||
- Framework3 -> Framework4
|
||||
|
||||
## ios_app_with_sdk
|
||||
|
||||
An application that contains an application target that depends on system libraries and frameworks (`.framework` and `.tbd`).
|
||||
|
||||
One of the dependencies is declared as `.optional` i.e. will be linked weakly.
|
||||
|
||||
## ios_app_with_static_libraries
|
||||
|
||||
This application provides a top level application with two static library dependencies. The first static library dependency has another static library dependency so that we are able to test how tuist handles the transitiveness of the static libraries in the linked frameworks of the main app.
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## User settings
|
||||
xcuserdata/
|
||||
|
||||
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
|
||||
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
|
||||
build/
|
||||
DerivedData/
|
||||
*.moved-aside
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
|
||||
### Projects ###
|
||||
*.xcodeproj
|
||||
*.xcworkspace
|
|
@ -0,0 +1,43 @@
|
|||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright ©. All rights reserved.</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,3 @@
|
|||
//: Playground - noun: a place where people can play
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='ios'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
|
@ -0,0 +1,29 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(name: "App",
|
||||
targets: [
|
||||
Target(name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: "Info.plist",
|
||||
sources: "Sources/**",
|
||||
dependencies: [
|
||||
.sdk(name: "CloudKit.framework", status: .required),
|
||||
.sdk(name: "StoreKit.framework", status: .optional),
|
||||
.sdk(name: "libc++.tbd"),
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])),
|
||||
Target(name: "AppTests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppTests",
|
||||
infoPlist: "Tests.plist",
|
||||
sources: "Tests/**",
|
||||
dependencies: [
|
||||
.target(name: "App"),
|
||||
],
|
||||
settings: Settings(base: ["CODE_SIGN_IDENTITY": "",
|
||||
"CODE_SIGNING_REQUIRED": "NO"])),
|
||||
])
|
|
@ -0,0 +1,15 @@
|
|||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
window = UIWindow(frame: UIScreen.main.bounds)
|
||||
let viewController = UIViewController()
|
||||
viewController.view.backgroundColor = .white
|
||||
window?.rootViewController = viewController
|
||||
window?.makeKeyAndVisible()
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright ©. All rights reserved.</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,6 @@
|
|||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable import App
|
||||
|
||||
final class AppTests: XCTestCase {}
|
Loading…
Reference in New Issue