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:
Kas 2019-06-19 13:25:46 +01:00 committed by GitHub
parent 6a9594ed4a
commit 76b50a2f9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 568 additions and 38 deletions

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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/")

View File

@ -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]
}

View File

@ -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",
])
}
}

View File

@ -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.

View File

@ -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.

63
fixtures/ios_app_with_sdk/.gitignore vendored Normal file
View File

@ -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

View File

@ -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>

View File

@ -0,0 +1,3 @@
//: Playground - noun: a place where people can play
import Foundation

View File

@ -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>

View File

@ -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"])),
])

View File

@ -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
}
}

View File

@ -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>

View File

@ -0,0 +1,6 @@
import Foundation
import XCTest
@testable import App
final class AppTests: XCTestCase {}