Appclip support (#1854)
* Introduce product type appClips * Add embed AppClips build phase * Add appclips to be executable * Apply swift formatting * Add linter rules for appClips target bundle ID * Add default settings for AppClips * Apply swift formatting * Add fixture for appclips * Add acceptance test for appclips * Add appClips product type to be supported for iOS platform * Add appClips product type tests * Add lint for required parent application identifiers entitlement in app clip target * Add missing appclips product in unit test and ui tests lintable target * Update documentation with appClips addition * Update changelog * Rename appClips product type to appClip * Add appClip lint tests for required entitlements * Update docs with appClip product type renaming * Update appClip acceptance test with appClip renaming * Add target of appClip product type can host tests * Skip adding embed app clip build phase for targets of non-app product type * Add unit test and ui test targets in app clip test fixture * Include missing appClip to be returned as apps * Avoid traversing graph to extract app and appClip pair * Add steps to build app clip unit and ui tests * Apply formatting * Add a lint rule to detect an app dependency on more one app clip * Improve missing parent application identifiers entitlement linting message * Fix ambiguous rake task name * Run generate-6 acceptance tests containing appclip test with Xcode 12 * Restore changelog * Replace references of info.plist app clip fixture with default one * Remove redundant references to assets in app clip fixture * Add tests for appClip product type * Update doc to include app clip reference * Add test to confirm app clip bundle package type * Add a step to appClip acceptance test to verify valid architecture * Add a test for appClip build phase attributes * Add App Clips example docs * Infer AppClip framework dependency for target of AppClip product type * Rename appclip target to avoid name collision with system AppClip import
This commit is contained in:
parent
c3de526717
commit
4a8bfd826e
|
@ -78,7 +78,6 @@ jobs:
|
|||
'generate-3',
|
||||
'generate-4',
|
||||
'generate-5',
|
||||
'generate-6',
|
||||
'init',
|
||||
'lint-project',
|
||||
'lint-code',
|
||||
|
@ -121,7 +120,8 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
xcode: ['12.1']
|
||||
feature: ['cache-xcframeworks', 'cache-frameworks', 'precompiled']
|
||||
feature:
|
||||
['generate-6', 'cache-xcframeworks', 'cache-frameworks', 'precompiled']
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Select Xcode
|
||||
|
@ -150,7 +150,6 @@ jobs:
|
|||
bundle install --jobs 4 --retry 3
|
||||
- name: Run tests
|
||||
run: FEATURE=features/${{ matrix.feature }}.feature bundle exec rake features
|
||||
|
||||
upload:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
name: Upload
|
||||
|
|
|
@ -12,6 +12,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
|
|||
- Extended `BuildPhaseGenerator` to generate script build phases [#1932](https://github.com/tuist/tuist/pull/1932) by [@pepibumur](https://github.com/pepibumur).
|
||||
- Extend the `TargetContentHasher` to account for the `Target.scripts` attribute [#1933](https://github.com/tuist/tuist/pull/1933) by [@pepibumur](https://github.com/pepibumur).
|
||||
- Extend the `CacheController` to generate projects with the build phase to locate the targets' built products directory [#1933](https://github.com/tuist/tuist/pull/1933) by [@pepibumur](https://github.com/pepibumur).
|
||||
- Add support for appClip [#1854](https://github.com/tuist/tuist/pull/1854) by [@lakpa](https://github.com/lakpa).
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ public enum Product: String, Codable, Equatable {
|
|||
case unitTests = "unit_tests"
|
||||
case uiTests = "ui_tests"
|
||||
case bundle
|
||||
case appClip
|
||||
|
||||
// Not supported yet
|
||||
case appExtension = "app_extension"
|
||||
|
|
|
@ -206,7 +206,7 @@ public class Graph: Encodable, Equatable {
|
|||
/// - Parameters:
|
||||
/// - path: Path to the directory where the project that defines the target is located.
|
||||
/// - name: Name of the target.
|
||||
public func linkableDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
|
||||
public func linkableDependencies(path: AbsolutePath, name: String) throws -> [GraphDependencyReference] {
|
||||
guard let targetNode = findTargetNode(path: path, name: name) else {
|
||||
return []
|
||||
}
|
||||
|
@ -225,6 +225,13 @@ public class Graph: Encodable, Equatable {
|
|||
references = references.union(transitiveSystemLibraries)
|
||||
}
|
||||
|
||||
if targetNode.target.isAppClip {
|
||||
let path = try SDKNode.appClip(status: .required).path
|
||||
references.insert(GraphDependencyReference.sdk(path: path,
|
||||
status: .required,
|
||||
source: .system))
|
||||
}
|
||||
|
||||
let directSystemLibrariesAndFrameworks = targetNode.sdkDependencies.map {
|
||||
GraphDependencyReference.sdk(path: $0.path, status: $0.status, source: $0.source)
|
||||
}
|
||||
|
@ -406,8 +413,8 @@ public class Graph: Encodable, Equatable {
|
|||
/// This method is useful to know which references should be added to the products directory in the generated project.
|
||||
/// - Parameter project: Project whose dependency references will be returned.
|
||||
public func allDependencyReferences(for project: Project) throws -> [GraphDependencyReference] {
|
||||
let linkableDependencies = project.targets.flatMap {
|
||||
self.linkableDependencies(path: project.path, name: $0.name)
|
||||
let linkableDependencies = try project.targets.flatMap {
|
||||
try self.linkableDependencies(path: project.path, name: $0.name)
|
||||
}
|
||||
|
||||
let embeddableDependencies = try project.targets.flatMap {
|
||||
|
@ -439,6 +446,14 @@ public class Graph: Encodable, Equatable {
|
|||
.filter { validProducts.contains($0.target.product) }
|
||||
}
|
||||
|
||||
public func appClipsDependency(path: AbsolutePath, name: String) -> TargetNode? {
|
||||
guard let targetNode = findTargetNode(path: path, name: name) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return targetNode.targetDependencies.first { $0.target.product == .appClip }
|
||||
}
|
||||
|
||||
/// Depth-first search (DFS) is an algorithm for traversing graph data structures. It starts at a source node
|
||||
/// and explores as far as possible along each branch before backtracking.
|
||||
///
|
||||
|
|
|
@ -34,4 +34,8 @@ public final class GraphTraverser: GraphTraversing {
|
|||
public func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
|
||||
graph.staticDependencies(path: path, name: name)
|
||||
}
|
||||
|
||||
public func appClipsDependency(path: AbsolutePath, name: String) -> Target? {
|
||||
graph.appClipsDependency(path: path, name: name).map { $0.target }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,14 @@ public class SDKNode: GraphNode {
|
|||
try! SDKNode(name: "XCTest.framework", platform: platform, status: status, source: .system)
|
||||
}
|
||||
|
||||
/// Creates an instace of SDKNode that represents the AppClip framework.
|
||||
/// - Parameters:
|
||||
/// - status: SDK status
|
||||
/// - Returns: Initialized SDK node.
|
||||
public static func appClip(status: SDKStatus) throws -> SDKNode {
|
||||
try SDKNode(name: "AppClip.framework", platform: .iOS, status: status, source: .system)
|
||||
}
|
||||
|
||||
static func path(name: String, platform: Platform, source _: SDKSource, type: Type) throws -> AbsolutePath {
|
||||
let sdkRootPath: AbsolutePath
|
||||
if name == SDKNode.xctestFrameworkName {
|
||||
|
|
|
@ -41,4 +41,10 @@ public protocol GraphTraversing {
|
|||
/// - path: Path to the directory where the project that defines the target is located.
|
||||
/// - name: Name of the target.
|
||||
func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference]
|
||||
|
||||
/// Given a project directory and a target name, it returns an appClips dependency.
|
||||
/// - Parameters:
|
||||
/// - path: Path to the directory that contains the project.
|
||||
/// - name: Target name.
|
||||
func appClipsDependency(path: AbsolutePath, name: String) -> Target?
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
|
|||
// case messagesApplication = "messages_application"
|
||||
case messagesExtension = "messages_extension"
|
||||
case stickerPackExtension = "sticker_pack_extension"
|
||||
case appClip
|
||||
|
||||
public var caseValue: String {
|
||||
switch self {
|
||||
|
@ -56,6 +57,8 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
|
|||
return "messagesExtension"
|
||||
case .stickerPackExtension:
|
||||
return "stickerPackExtension"
|
||||
case .appClip:
|
||||
return "appClip"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,13 +98,15 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
|
|||
return "iMessage extension"
|
||||
case .stickerPackExtension:
|
||||
return "sticker pack extension"
|
||||
case .appClip:
|
||||
return "appClip"
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the target can be ran.
|
||||
public var runnable: Bool {
|
||||
switch self {
|
||||
case .app:
|
||||
case .app, .appClip:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
@ -126,6 +131,7 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
|
|||
base.append(.stickerPackExtension)
|
||||
// base.append(.messagesApplication)
|
||||
base.append(.messagesExtension)
|
||||
base.append(.appClip)
|
||||
}
|
||||
|
||||
if platform == .tvOS {
|
||||
|
@ -195,6 +201,12 @@ public enum Product: String, CustomStringConvertible, CaseIterable, Encodable {
|
|||
return .messagesExtension
|
||||
case .stickerPackExtension:
|
||||
return .stickerPack
|
||||
case .appClip:
|
||||
return .onDemandInstallCapableApplication
|
||||
}
|
||||
}
|
||||
|
||||
public func canHostTests() -> Bool {
|
||||
[.app, .appClip].contains(self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -170,6 +170,14 @@ public struct Target: Equatable, Hashable, Comparable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if the target is an AppClip
|
||||
public var isAppClip: Bool {
|
||||
if case .appClip = product {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns true if the file at the given path is a resource.
|
||||
/// - Parameter path: Path to the file to be checked.
|
||||
public static func isResource(path: AbsolutePath) -> Bool {
|
||||
|
@ -287,8 +295,8 @@ extension Sequence where Element == Target {
|
|||
filter { $0.product.testsBundle }
|
||||
}
|
||||
|
||||
/// Filters and returns only the targets that are apps.
|
||||
/// Filters and returns only the targets that are apps and app clips.
|
||||
var apps: [Target] {
|
||||
filter { $0.product == .app }
|
||||
filter { $0.product == .app || $0.product == .appClip }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,6 +71,11 @@ public class ValueGraphTraverser: GraphTraversing {
|
|||
.sorted()
|
||||
}
|
||||
|
||||
public func appClipsDependency(path: AbsolutePath, name: String) -> Target? {
|
||||
directTargetDependencies(path: path, name: name)
|
||||
.first { $0.product == .appClip }
|
||||
}
|
||||
|
||||
public func directStaticDependencies(path: AbsolutePath, name: String) -> [GraphDependencyReference] {
|
||||
graph.dependencies[.target(name: name, path: path)]?
|
||||
.compactMap { (dependency: ValueGraphDependency) -> (path: AbsolutePath, name: String)? in
|
||||
|
|
|
@ -100,4 +100,8 @@ final class MockGraphTraverser: GraphTraversing {
|
|||
invokedDirectStaticDependenciesParametersList.append((path, name))
|
||||
return stubbedDirectStaticDependenciesResult
|
||||
}
|
||||
|
||||
func appClipsDependency(path _: AbsolutePath, name _: String) -> Target? {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,13 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
|
|||
generateScripts(target.scripts,
|
||||
pbxTarget: pbxTarget,
|
||||
pbxproj: pbxproj)
|
||||
|
||||
try generateEmbedAppClipsBuildPhase(path: path,
|
||||
target: target,
|
||||
graphTraverser: graphTraverser,
|
||||
pbxTarget: pbxTarget,
|
||||
fileElements: fileElements,
|
||||
pbxproj: pbxproj)
|
||||
}
|
||||
|
||||
func generateActions(actions: [TargetAction],
|
||||
|
@ -376,4 +383,32 @@ final class BuildPhaseGenerator: BuildPhaseGenerating {
|
|||
embedWatchAppBuildPhase.files?.append(pbxBuildFile)
|
||||
}
|
||||
}
|
||||
|
||||
func generateEmbedAppClipsBuildPhase(path: AbsolutePath,
|
||||
target: Target,
|
||||
graphTraverser: GraphTraversing,
|
||||
pbxTarget: PBXTarget,
|
||||
fileElements: ProjectFileElements,
|
||||
pbxproj: PBXProj) throws
|
||||
{
|
||||
guard target.product == .app else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let appClips = graphTraverser.appClipsDependency(path: path, name: target.name) else {
|
||||
return
|
||||
}
|
||||
|
||||
let embedAppClipsBuildPhase = PBXCopyFilesBuildPhase(dstPath: "$(CONTENTS_FOLDER_PATH)/AppClips",
|
||||
dstSubfolderSpec: .productsDirectory,
|
||||
name: "Embed App Clips")
|
||||
pbxproj.add(object: embedAppClipsBuildPhase)
|
||||
pbxTarget.buildPhases.append(embedAppClipsBuildPhase)
|
||||
|
||||
let refs = fileElements.product(target: appClips.name)
|
||||
|
||||
let pbxBuildFile = PBXBuildFile(file: refs, settings: ["ATTRIBUTES": ["RemoveHeadersOnCopy"]])
|
||||
pbxproj.add(object: pbxBuildFile)
|
||||
embedAppClipsBuildPhase.files?.append(pbxBuildFile)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ final class ConfigGenerator: ConfigGenerating {
|
|||
}
|
||||
|
||||
let targetDependencies = graphTraverser.directTargetDependencies(path: projectPath, name: target.name)
|
||||
let appDependency = targetDependencies.first { $0.product == .app }
|
||||
let appDependency = targetDependencies.first { $0.product.canHostTests() }
|
||||
|
||||
guard let app = appDependency else {
|
||||
return [:]
|
||||
|
|
|
@ -96,7 +96,7 @@ final class InfoPlistContentProvider: InfoPlistContentProviding {
|
|||
var packageType: String?
|
||||
|
||||
switch target.product {
|
||||
case .app:
|
||||
case .app, .appClip:
|
||||
packageType = "APPL"
|
||||
case .staticLibrary, .dynamicLibrary:
|
||||
packageType = nil
|
||||
|
|
|
@ -91,7 +91,7 @@ final class LinkGenerator: LinkGenerating {
|
|||
graph: Graph) throws
|
||||
{
|
||||
let embeddableFrameworks = try graph.embeddableFrameworks(path: path, name: target.name)
|
||||
let linkableModules = graph.linkableDependencies(path: path, name: target.name)
|
||||
let linkableModules = try graph.linkableDependencies(path: path, name: target.name)
|
||||
|
||||
try setupSearchAndIncludePaths(target: target,
|
||||
pbxTarget: pbxTarget,
|
||||
|
|
|
@ -60,7 +60,7 @@ extension GraphNode {
|
|||
|
||||
if let targetNode = self as? TargetNode {
|
||||
switch targetNode.target.product {
|
||||
case .app, .watch2App:
|
||||
case .app, .watch2App, .appClip:
|
||||
return .init(fillColorName: .deepskyblue, strokeWidth: 1.5, shape: .box3d)
|
||||
case .appExtension, .watch2Extension:
|
||||
return .init(fillColorName: .deepskyblue2, shape: .component)
|
||||
|
|
|
@ -40,6 +40,7 @@ public class GraphLinter: GraphLinting {
|
|||
issues.append(contentsOf: lintDependencies(graph: graph))
|
||||
issues.append(contentsOf: lintMismatchingConfigurations(graph: graph))
|
||||
issues.append(contentsOf: lintWatchBundleIndentifiers(graph: graph))
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
|
@ -57,6 +58,7 @@ public class GraphLinter: GraphLinting {
|
|||
issues.append(contentsOf: lintCarthageDependencies(graph: graph))
|
||||
issues.append(contentsOf: lintCocoaPodsDependencies(graph: graph))
|
||||
issues.append(contentsOf: lintPackageDependencies(graph: graph))
|
||||
issues.append(contentsOf: lintAppClip(graph: graph))
|
||||
|
||||
return issues
|
||||
}
|
||||
|
@ -170,6 +172,34 @@ public class GraphLinter: GraphLinting {
|
|||
}
|
||||
}
|
||||
|
||||
private func lintAppClip(graph: Graph) -> [LintingIssue] {
|
||||
let apps = graph
|
||||
.targets.values
|
||||
.flatMap { targets -> [TargetNode] in
|
||||
targets.compactMap { target in
|
||||
if target.target.product == .app { return target }
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let issues = apps.flatMap { app -> [LintingIssue] in
|
||||
let appClips = products(ofType: .appClip, for: app, graph: graph)
|
||||
|
||||
if appClips.count > 1 {
|
||||
return [
|
||||
LintingIssue(reason: "App '\(app)' cannot depend on more than one app clip -> \(appClips.map(\.name).joined(separator: ", "))",
|
||||
severity: .error),
|
||||
]
|
||||
}
|
||||
|
||||
return appClips.flatMap { appClip -> [LintingIssue] in
|
||||
lint(appClip: appClip, parentApp: app)
|
||||
}
|
||||
}
|
||||
|
||||
return issues
|
||||
}
|
||||
|
||||
private func lintCarthageDependencies(graph: Graph) -> [LintingIssue] {
|
||||
let frameworks = graph.frameworks
|
||||
let carthageFrameworks = frameworks.filter { $0.isCarthage }
|
||||
|
@ -200,10 +230,10 @@ public class GraphLinter: GraphLinting {
|
|||
}
|
||||
|
||||
let issues = apps.flatMap { app -> [LintingIssue] in
|
||||
let watchApps = watchAppsFor(targetNode: app, graph: graph)
|
||||
let watchApps = products(ofType: .watch2App, for: app, graph: graph)
|
||||
return watchApps.flatMap { watchApp -> [LintingIssue] in
|
||||
let watchAppIssues = lint(watchApp: watchApp, parentApp: app)
|
||||
let watchExtensions = watchExtensionsFor(targetNode: watchApp, graph: graph)
|
||||
let watchExtensions = products(ofType: .watch2Extension, for: watchApp, graph: graph)
|
||||
let watchExtensionIssues = watchExtensions.flatMap { watchExtension in
|
||||
lint(watchExtension: watchExtension, parentWatchApp: watchApp)
|
||||
}
|
||||
|
@ -236,16 +266,31 @@ public class GraphLinter: GraphLinting {
|
|||
return []
|
||||
}
|
||||
|
||||
private func watchAppsFor(targetNode: TargetNode, graph: Graph) -> [TargetNode] {
|
||||
private func products(ofType type: Product, for targetNode: TargetNode, graph: Graph) -> [TargetNode] {
|
||||
graph.targetDependencies(path: targetNode.path,
|
||||
name: targetNode.name)
|
||||
.filter { $0.target.product == .watch2App }
|
||||
.filter { $0.target.product == type }
|
||||
}
|
||||
|
||||
private func watchExtensionsFor(targetNode: TargetNode, graph: Graph) -> [TargetNode] {
|
||||
graph.targetDependencies(path: targetNode.path,
|
||||
name: targetNode.name)
|
||||
.filter { $0.target.product == .watch2Extension }
|
||||
private func lint(appClip: TargetNode, parentApp: TargetNode) -> [LintingIssue] {
|
||||
var foundIssues = [LintingIssue]()
|
||||
|
||||
if !appClip.target.bundleId.hasPrefix(parentApp.target.bundleId) {
|
||||
foundIssues.append(
|
||||
LintingIssue(reason: """
|
||||
AppClip '\(appClip.name)' bundleId: \(appClip.target.bundleId) isn't prefixed with its parent's app '\(parentApp.name)' bundleId '\(parentApp.target.bundleId)'
|
||||
""", severity: .error))
|
||||
}
|
||||
|
||||
if let entitlements = appClip.target.entitlements {
|
||||
if !FileHandler.shared.exists(entitlements) {
|
||||
foundIssues.append(LintingIssue(reason: "The entitlements at path '\(entitlements)' referenced by target does not exist", severity: .error))
|
||||
}
|
||||
} else {
|
||||
foundIssues.append(LintingIssue(reason: "An AppClip '\(appClip.target.name)' requires its Parent Application Identifiers Entitlement to be set", severity: .error))
|
||||
}
|
||||
|
||||
return foundIssues
|
||||
}
|
||||
|
||||
struct LintableTarget: Equatable, Hashable {
|
||||
|
@ -265,6 +310,7 @@ public class GraphLinter: GraphLinting {
|
|||
LintableTarget(platform: .iOS, product: .messagesExtension),
|
||||
LintableTarget(platform: .iOS, product: .stickerPackExtension),
|
||||
LintableTarget(platform: .watchOS, product: .watch2App),
|
||||
LintableTarget(platform: .iOS, product: .appClip),
|
||||
// LintableTarget(platform: .watchOS, product: .watchApp),
|
||||
],
|
||||
LintableTarget(platform: .iOS, product: .staticLibrary): [
|
||||
|
@ -296,6 +342,7 @@ public class GraphLinter: GraphLinting {
|
|||
LintableTarget(platform: .iOS, product: .framework),
|
||||
LintableTarget(platform: .iOS, product: .staticFramework),
|
||||
LintableTarget(platform: .iOS, product: .bundle),
|
||||
LintableTarget(platform: .iOS, product: .appClip),
|
||||
],
|
||||
LintableTarget(platform: .iOS, product: .uiTests): [
|
||||
LintableTarget(platform: .iOS, product: .app),
|
||||
|
@ -304,6 +351,7 @@ public class GraphLinter: GraphLinting {
|
|||
LintableTarget(platform: .iOS, product: .framework),
|
||||
LintableTarget(platform: .iOS, product: .staticFramework),
|
||||
LintableTarget(platform: .iOS, product: .bundle),
|
||||
LintableTarget(platform: .iOS, product: .appClip),
|
||||
],
|
||||
LintableTarget(platform: .iOS, product: .appExtension): [
|
||||
LintableTarget(platform: .iOS, product: .staticLibrary),
|
||||
|
|
|
@ -98,7 +98,7 @@ public final class AutogeneratedSchemesProjectMapper: ProjectMapping {
|
|||
project: Project) -> [TargetReference]
|
||||
{
|
||||
project.targets
|
||||
.filter { $0.product == .app && $0.dependencies.contains(.target(name: target.name)) }
|
||||
.filter { $0.product.canHostTests() && $0.dependencies.contains(.target(name: target.name)) }
|
||||
.sorted(by: { $0.name < $1.name })
|
||||
.map { TargetReference(projectPath: project.path, name: $0.name) }
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ final class SettingsHelper {
|
|||
|
||||
func settingsProviderProduct(_ target: Target) -> BuildSettingsProvider.Product? {
|
||||
switch target.product {
|
||||
case .app, .watch2App:
|
||||
case .app, .watch2App, .appClip:
|
||||
return .application
|
||||
case .dynamicLibrary:
|
||||
return .dynamicLibrary
|
||||
|
|
|
@ -35,6 +35,8 @@ extension TuistCore.Product {
|
|||
return .watch2Extension
|
||||
case .messagesExtension:
|
||||
return .messagesExtension
|
||||
case .appClip:
|
||||
return .appClip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,5 +12,6 @@ final class ProductTests: XCTestCase {
|
|||
XCTAssertCodableEqualToJson([Product.framework], "[\"framework\"]")
|
||||
XCTAssertCodableEqualToJson([Product.unitTests], "[\"unit_tests\"]")
|
||||
XCTAssertCodableEqualToJson([Product.uiTests], "[\"ui_tests\"]")
|
||||
XCTAssertCodableEqualToJson([Product.appClip], "[\"appClip\"]")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
dependencies: [precompiledNode])
|
||||
let graph = Graph.test(targets: [targetNode.path: [targetNode]])
|
||||
|
||||
let got = graph.linkableDependencies(path: project.path, name: target.name)
|
||||
let got = try graph.linkableDependencies(path: project.path, name: target.name)
|
||||
XCTAssertEqual(got.first, GraphDependencyReference(precompiledNode: precompiledNode))
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
project.path: [dependencyNode, targetNode],
|
||||
])
|
||||
|
||||
let got = graph.linkableDependencies(path: project.path, name: target.name)
|
||||
let got = try graph.linkableDependencies(path: project.path, name: target.name)
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency", productName: "libDependency.a"))
|
||||
}
|
||||
|
||||
|
@ -120,12 +120,12 @@ final class GraphTests: TuistUnitTestCase {
|
|||
dependencies: [dependencyNode])
|
||||
|
||||
let graph = Graph.test(projects: [project], targets: [project.path: [targetNode, dependencyNode, staticDependencyNode]])
|
||||
let got = graph.linkableDependencies(path: project.path,
|
||||
name: target.name)
|
||||
let got = try graph.linkableDependencies(path: project.path,
|
||||
name: target.name)
|
||||
XCTAssertEqual(got.count, 1)
|
||||
XCTAssertEqual(got.first, .product(target: "Dependency", productName: "Dependency.framework"))
|
||||
|
||||
let frameworkGot = graph.linkableDependencies(path: project.path, name: dependency.name)
|
||||
let frameworkGot = try graph.linkableDependencies(path: project.path, name: dependency.name)
|
||||
|
||||
XCTAssertEqual(frameworkGot.count, 1)
|
||||
XCTAssertTrue(frameworkGot.contains(.product(target: "StaticDependency", productName: "libStaticDependency.a")))
|
||||
|
@ -152,7 +152,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, [GraphDependencyReference.product(target: "DynamicFramework", productName: "DynamicFramework.framework"),
|
||||
|
@ -188,8 +188,8 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let appResult = graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let dynamicFramework1Result = graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name)
|
||||
let appResult = try graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let dynamicFramework1Result = try graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(appResult, [
|
||||
|
@ -235,7 +235,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let dynamicFramework1Result = graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name)
|
||||
let dynamicFramework1Result = try graph.linkableDependencies(path: projectA.path, name: dynamicFramework1.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(dynamicFramework1Result, [GraphDependencyReference.product(target: "DynamicFramework2", productName: "DynamicFramework2.framework")])
|
||||
|
@ -262,7 +262,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result.compactMap(sdkDependency), [
|
||||
|
@ -291,8 +291,8 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let appResult = graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let dynamicResult = graph.linkableDependencies(path: projectA.path, name: dynamicFramework.name)
|
||||
let appResult = try graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let dynamicResult = try graph.linkableDependencies(path: projectA.path, name: dynamicFramework.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(appResult.compactMap(sdkDependency), [])
|
||||
|
@ -318,7 +318,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: app.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result.compactMap(sdkDependency), [SDKPathAndStatus(name: "some.framework", status: .optional)])
|
||||
|
@ -339,7 +339,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: staticFramework.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: staticFramework.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result.compactMap(sdkDependency),
|
||||
|
@ -365,7 +365,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: staticFrameworkA.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: staticFrameworkA.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result.compactMap(sdkDependency),
|
||||
|
@ -387,7 +387,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: project.path, name: watchExtension.name)
|
||||
let result = try graph.linkableDependencies(path: project.path, name: watchExtension.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, [
|
||||
|
@ -410,7 +410,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: project.path, name: watchExtension.name)
|
||||
let result = try graph.linkableDependencies(path: project.path, name: watchExtension.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, [
|
||||
|
@ -436,7 +436,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(result.isEmpty)
|
||||
|
@ -459,7 +459,7 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(result, [
|
||||
|
@ -484,12 +484,27 @@ final class GraphTests: TuistUnitTestCase {
|
|||
])
|
||||
|
||||
// When
|
||||
let result = graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
let result = try graph.linkableDependencies(path: projectA.path, name: tests.name)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(result.isEmpty)
|
||||
}
|
||||
|
||||
func test_linkableDependencies_when_appClipSDKNode() throws {
|
||||
// Given
|
||||
let target = Target.test(name: "AppClip", product: .appClip)
|
||||
let projectA = Project.test(path: "/path/a")
|
||||
|
||||
let graph = Graph.create(project: projectA,
|
||||
dependencies: [(target: target, dependencies: [])])
|
||||
|
||||
// When
|
||||
let linkableModules = try graph.linkableDependencies(path: projectA.path, name: target.name)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(linkableModules, [.sdk(path: try SDKNode.appClip(status: .required).path, status: .required, source: .system)])
|
||||
}
|
||||
|
||||
func test_librariesPublicHeaders() throws {
|
||||
let target = Target.test(name: "Main")
|
||||
let publicHeadersPath = AbsolutePath("/test/public/")
|
||||
|
|
|
@ -106,4 +106,15 @@ final class SDKNodeTests: XCTestCase {
|
|||
// Then
|
||||
XCTAssertEqual(got, "CoreData")
|
||||
}
|
||||
|
||||
func test_appClip_sdk_framework_path() throws {
|
||||
// Given
|
||||
let subject = try SDKNode.appClip(status: .required)
|
||||
|
||||
// When
|
||||
let got = subject.path
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, "/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/AppClip.framework")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ final class ProductTests: XCTestCase {
|
|||
XCTAssertEqual(Product.uiTests.xcodeValue, PBXProductType.uiTestBundle)
|
||||
XCTAssertEqual(Product.appExtension.xcodeValue, PBXProductType.appExtension)
|
||||
XCTAssertEqual(Product.stickerPackExtension.xcodeValue, PBXProductType.stickerPack)
|
||||
XCTAssertEqual(Product.appClip.xcodeValue, PBXProductType.onDemandInstallCapableApplication)
|
||||
}
|
||||
|
||||
func test_description() {
|
||||
|
@ -25,6 +26,7 @@ final class ProductTests: XCTestCase {
|
|||
XCTAssertEqual(Product.uiTests.description, "ui tests")
|
||||
XCTAssertEqual(Product.appExtension.description, "app extension")
|
||||
XCTAssertEqual(Product.stickerPackExtension.description, "sticker pack extension")
|
||||
XCTAssertEqual(Product.appClip.description, "appClip")
|
||||
}
|
||||
|
||||
func test_forPlatform_when_ios() {
|
||||
|
@ -40,6 +42,7 @@ final class ProductTests: XCTestCase {
|
|||
.messagesExtension,
|
||||
.unitTests,
|
||||
.uiTests,
|
||||
.appClip,
|
||||
]
|
||||
XCTAssertEqual(Set(got), Set(expected))
|
||||
}
|
||||
|
@ -73,7 +76,7 @@ final class ProductTests: XCTestCase {
|
|||
|
||||
func test_runnable() {
|
||||
Product.allCases.forEach { product in
|
||||
if product == .app {
|
||||
if [.app, .appClip].contains(product) {
|
||||
XCTAssertTrue(product.runnable)
|
||||
} else {
|
||||
XCTAssertFalse(product.runnable)
|
||||
|
|
|
@ -43,6 +43,11 @@ final class TargetTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(target.productNameWithExtension, "Test.app")
|
||||
}
|
||||
|
||||
func test_productName_when_appClip() {
|
||||
let target = Target.test(name: "Test", product: .appClip)
|
||||
XCTAssertEqual(target.productNameWithExtension, "Test.app")
|
||||
}
|
||||
|
||||
func test_sequence_testBundles() {
|
||||
let app = Target.test(product: .app)
|
||||
let tests = Target.test(product: .unitTests)
|
||||
|
@ -59,6 +64,14 @@ final class TargetTests: TuistUnitTestCase {
|
|||
XCTAssertEqual(targets.apps, [app])
|
||||
}
|
||||
|
||||
func test_sequence_appClips() {
|
||||
let appClip = Target.test(product: .appClip)
|
||||
let tests = Target.test(product: .unitTests)
|
||||
let targets = [appClip, tests]
|
||||
|
||||
XCTAssertEqual(targets.apps, [appClip])
|
||||
}
|
||||
|
||||
func test_targetLocatorBuildPhaseVariable() {
|
||||
XCTAssertEqual(Target.test(productName: "My Framework").targetLocatorBuildPhaseVariable, "MY_FRAMEWORK_LOCATE_HASH")
|
||||
XCTAssertEqual(Target.test(productName: "Feature").targetLocatorBuildPhaseVariable, "FEATURE_LOCATE_HASH")
|
||||
|
@ -198,6 +211,7 @@ final class TargetTests: TuistUnitTestCase {
|
|||
XCTAssertTrue(Target.test(product: .watch2Extension).supportsResources)
|
||||
XCTAssertTrue(Target.test(product: .messagesExtension).supportsResources)
|
||||
XCTAssertTrue(Target.test(product: .stickerPackExtension).supportsResources)
|
||||
XCTAssertTrue(Target.test(product: .appClip).supportsResources)
|
||||
}
|
||||
|
||||
func test_resources() throws {
|
||||
|
|
|
@ -643,6 +643,44 @@ final class BuildPhaseGeneratorTests: TuistUnitTestCase {
|
|||
XCTAssertTrue(postBuildPhase.alwaysOutOfDate)
|
||||
}
|
||||
|
||||
func test_generateEmbedAppClipsBuildPhase() throws {
|
||||
// Given
|
||||
let app = Target.test(name: "App", product: .app)
|
||||
let appClip = Target.test(name: "AppClip", product: .appClip)
|
||||
let project = Project.test()
|
||||
let pbxproj = PBXProj()
|
||||
let nativeTarget = PBXNativeTarget(name: "Test")
|
||||
let fileElements = createProductFileElements(for: [app, appClip])
|
||||
|
||||
let targets: [AbsolutePath: [String: Target]] = [
|
||||
project.path: [app.name: app, appClip.name: appClip],
|
||||
]
|
||||
let dependencies: [ValueGraphDependency: Set<ValueGraphDependency>] = [
|
||||
.target(name: appClip.name, path: project.path): Set(),
|
||||
.target(name: app.name, path: project.path): Set([.target(name: appClip.name, path: project.path)]),
|
||||
]
|
||||
let graph = ValueGraph.test(path: project.path,
|
||||
projects: [project.path: project],
|
||||
targets: targets,
|
||||
dependencies: dependencies)
|
||||
let graphTraverser = ValueGraphTraverser(graph: graph)
|
||||
// When
|
||||
try subject.generateEmbedAppClipsBuildPhase(path: project.path,
|
||||
target: app,
|
||||
graphTraverser: graphTraverser,
|
||||
pbxTarget: nativeTarget,
|
||||
fileElements: fileElements,
|
||||
pbxproj: pbxproj)
|
||||
|
||||
// Then
|
||||
let pbxBuildPhase: PBXBuildPhase? = nativeTarget.buildPhases.first
|
||||
XCTAssertNotNil(pbxBuildPhase)
|
||||
XCTAssertTrue(pbxBuildPhase is PBXCopyFilesBuildPhase)
|
||||
XCTAssertEqual(pbxBuildPhase?.files?.compactMap { $0.file?.nameOrPath }, ["AppClip"])
|
||||
XCTAssertEqual(pbxBuildPhase?.files?.compactMap { $0.settings as? [String: [String]] },
|
||||
[["ATTRIBUTES": ["RemoveHeadersOnCopy"]]])
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func createProductFileElements(for targets: [Target]) -> ProjectFileElements {
|
||||
|
|
|
@ -168,6 +168,7 @@ final class InfoPlistContentProviderTests: XCTestCase {
|
|||
assertPackageType(content(for: .test(product: .framework)), "FMWK")
|
||||
assertPackageType(content(for: .test(product: .staticFramework)), "FMWK")
|
||||
assertPackageType(content(for: .test(product: .watch2App)), "$(PRODUCT_BUNDLE_PACKAGE_TYPE)")
|
||||
assertPackageType(content(for: .test(product: .appClip)), "APPL")
|
||||
}
|
||||
|
||||
func test_content_whenWatchOSApp() {
|
||||
|
|
|
@ -475,4 +475,167 @@ final class GraphLinterTests: TuistUnitTestCase {
|
|||
severity: .error),
|
||||
])
|
||||
}
|
||||
|
||||
func test_lint_valid_appClipTargetBundleIdentifiers() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
||||
try createFiles([
|
||||
"entitlements/AppClip.entitlements",
|
||||
])
|
||||
|
||||
let entitlementsPath = temporaryPath.appending(RelativePath("entitlements/AppClip.entitlements"))
|
||||
|
||||
let app = Target.test(name: "App",
|
||||
product: .app,
|
||||
bundleId: "com.example.app")
|
||||
let appClip = Target.test(name: "AppClip",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example.app.clip",
|
||||
entitlements: entitlementsPath)
|
||||
let project = Project.test(targets: [app, appClip])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [
|
||||
(target: app, dependencies: [appClip]),
|
||||
(target: appClip, dependencies: []),
|
||||
])
|
||||
|
||||
// When
|
||||
let got = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(got.isEmpty)
|
||||
}
|
||||
|
||||
func test_lint_invalid_appClipTargetBundleIdentifiers() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
||||
try createFiles([
|
||||
"entitlements/AppClip.entitlements",
|
||||
])
|
||||
|
||||
let entitlementsPath = temporaryPath.appending(RelativePath("entitlements/AppClip.entitlements"))
|
||||
|
||||
let app = Target.test(name: "TestApp",
|
||||
product: .app,
|
||||
bundleId: "com.example.app")
|
||||
let appClip = Target.test(name: "TestAppClip",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example1.app.clip",
|
||||
entitlements: entitlementsPath)
|
||||
let project = Project.test(targets: [app, appClip])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [
|
||||
(target: app, dependencies: [appClip]),
|
||||
(target: appClip, dependencies: []),
|
||||
])
|
||||
|
||||
// When
|
||||
let got = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, [
|
||||
LintingIssue(reason: "AppClip 'TestAppClip' bundleId: com.example1.app.clip isn't prefixed with its parent's app 'TestApp' bundleId 'com.example.app'",
|
||||
severity: .error),
|
||||
])
|
||||
}
|
||||
|
||||
func test_lint_when_appclip_is_missing_required_entitlements() throws {
|
||||
// Given
|
||||
let app = Target.test(name: "App",
|
||||
product: .app,
|
||||
bundleId: "com.example.app")
|
||||
let appClip = Target.test(name: "AppClip",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example.app.clip")
|
||||
let project = Project.test(targets: [app, appClip])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [
|
||||
(target: app, dependencies: [appClip]),
|
||||
(target: appClip, dependencies: []),
|
||||
])
|
||||
|
||||
// When
|
||||
let got = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, [
|
||||
LintingIssue(reason: "An AppClip 'AppClip' requires its Parent Application Identifiers Entitlement to be set",
|
||||
severity: .error),
|
||||
])
|
||||
}
|
||||
|
||||
func test_lint_when_appclip_entitlements_does_not_exist() throws {
|
||||
// Given
|
||||
let app = Target.test(name: "App",
|
||||
product: .app,
|
||||
bundleId: "com.example.app")
|
||||
let appClip = Target.test(name: "AppClip",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example.app.clip",
|
||||
entitlements: "/entitlements/AppClip.entitlements")
|
||||
let project = Project.test(targets: [app, appClip])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [
|
||||
(target: app, dependencies: [appClip]),
|
||||
(target: appClip, dependencies: []),
|
||||
])
|
||||
|
||||
// When
|
||||
let got = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, [
|
||||
LintingIssue(reason: "The entitlements at path '/entitlements/AppClip.entitlements' referenced by target does not exist",
|
||||
severity: .error),
|
||||
])
|
||||
}
|
||||
|
||||
func test_lint_when_app_contains_more_than_one_appClip() throws {
|
||||
// Given
|
||||
let temporaryPath = try self.temporaryPath()
|
||||
|
||||
try createFiles([
|
||||
"entitlements/AppClip.entitlements",
|
||||
])
|
||||
|
||||
let entitlementsPath = temporaryPath.appending(RelativePath("entitlements/AppClip.entitlements"))
|
||||
|
||||
let app = Target.test(name: "App",
|
||||
product: .app,
|
||||
bundleId: "com.example.app")
|
||||
let appClip1 = Target.test(name: "AppClip1",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example.app.clip1",
|
||||
entitlements: entitlementsPath)
|
||||
|
||||
let appClip2 = Target.test(name: "AppClip2",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "com.example.app.clip2",
|
||||
entitlements: entitlementsPath)
|
||||
|
||||
let project = Project.test(targets: [app, appClip1, appClip2])
|
||||
let graph = Graph.create(project: project,
|
||||
dependencies: [
|
||||
(target: app, dependencies: [appClip1, appClip2]),
|
||||
(target: appClip1, dependencies: []),
|
||||
(target: appClip2, dependencies: []),
|
||||
])
|
||||
|
||||
// When
|
||||
let got = subject.lint(graph: graph)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(got, [
|
||||
LintingIssue(reason: "App 'App' cannot depend on more than one app clip -> AppClip1, AppClip2",
|
||||
severity: .error),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -179,6 +179,7 @@ final class SettingsHelpersTests: XCTestCase {
|
|||
XCTAssertEqual(subject.settingsProviderProduct(.test(product: .framework)), .framework)
|
||||
XCTAssertEqual(subject.settingsProviderProduct(.test(product: .unitTests)), .unitTests)
|
||||
XCTAssertEqual(subject.settingsProviderProduct(.test(product: .uiTests)), .uiTests)
|
||||
XCTAssertEqual(subject.settingsProviderProduct(.test(product: .appClip)), .application)
|
||||
XCTAssertNil(subject.settingsProviderProduct(.test(product: .bundle)))
|
||||
}
|
||||
|
||||
|
|
|
@ -16,4 +16,16 @@ Scenario: The project is an iOS application with core data models (ios_app_with_
|
|||
Then tuist generates the project
|
||||
Then I should be able to build for iOS the scheme App
|
||||
Then the product 'App.app' with destination 'Debug-iphonesimulator' contains resource 'Users.momd'
|
||||
Then the product 'App.app' with destination 'Debug-iphonesimulator' contains resource '1_2.cdm'
|
||||
Then the product 'App.app' with destination 'Debug-iphonesimulator' contains resource '1_2.cdm'
|
||||
|
||||
Scenario: The project is an iOS application with appclip (ios_app_with_appclip)
|
||||
Given that tuist is available
|
||||
And I have a working directory
|
||||
Then I copy the fixture ios_app_with_appclip into the working directory
|
||||
Then tuist generates the project
|
||||
Then I should be able to build for iOS the scheme App
|
||||
Then the product 'App.app' with destination 'Debug-iphonesimulator' contains the appClip 'AppClip1' with architecture 'x86_64'
|
||||
Then the product 'App.app' with destination 'Debug-iphonesimulator' contains the appClip 'AppClip1' without architecture 'armv7'
|
||||
Then I should be able to build for iOS the scheme AppClip1
|
||||
Then I should be able to test for iOS the scheme AppClip1Tests
|
||||
Then I should be able to test for iOS the scheme AppClip1UITests
|
||||
|
|
|
@ -121,3 +121,33 @@ Then(/^a file (.+) exists$/) do |file|
|
|||
file_path = File.join(@dir, file)
|
||||
assert(File.exist?(file_path), "#{file_path} does not exist")
|
||||
end
|
||||
|
||||
Then("the product {string} with destination {string} contains the appClip {string} with architecture {string}") do |product, destination, app_clip, architecture|
|
||||
app_clip_path = Xcode.find_app_clip(
|
||||
product: product,
|
||||
destination: destination,
|
||||
app_clip: app_clip,
|
||||
derived_data_path: @derived_data_path
|
||||
)
|
||||
flunk("AppClip #{app_clip} not found") if app_clip_path.nil?
|
||||
|
||||
binary_path = File.join(app_clip_path, app_clip)
|
||||
out, err, status = Open3.capture3("file", binary_path)
|
||||
assert(status.success?, err)
|
||||
assert(out.include?(architecture))
|
||||
end
|
||||
|
||||
Then("the product {string} with destination {string} contains the appClip {string} without architecture {string}") do |product, destination, app_clip, architecture|
|
||||
app_clip_path = Xcode.find_app_clip(
|
||||
product: product,
|
||||
destination: destination,
|
||||
app_clip: app_clip,
|
||||
derived_data_path: @derived_data_path
|
||||
)
|
||||
flunk("AppClip #{app_clip} not found") if app_clip_path.nil?
|
||||
|
||||
binary_path = File.join(app_clip_path, app_clip)
|
||||
out, err, status = Open3.capture3("file", binary_path)
|
||||
assert(status.success?, err)
|
||||
refute(out.include?(architecture))
|
||||
end
|
||||
|
|
|
@ -106,4 +106,3 @@ Then(/^in project (.+) the target (.+) should have the build phase (.+) in the l
|
|||
flunk("The target #{target_name} doesn't have build phases") if build_phase.nil?
|
||||
assert_equal phase_name, build_phase.name
|
||||
end
|
||||
|
||||
|
|
|
@ -39,6 +39,22 @@ module Xcode
|
|||
framework_path
|
||||
end
|
||||
|
||||
def self.find_app_clip(product:, destination:, app_clip:, derived_data_path:)
|
||||
product_path = product_with_name(
|
||||
product,
|
||||
destination: destination,
|
||||
derived_data_path: derived_data_path
|
||||
)
|
||||
|
||||
return if product_path.nil?
|
||||
|
||||
app_clip_glob = File.join(product_path, "/AppClips/#{app_clip}.app")
|
||||
# /path/to/product/AppClips/AppClip.app
|
||||
app_clip_path = Dir.glob(app_clip_glob).first
|
||||
|
||||
app_clip_path
|
||||
end
|
||||
|
||||
def self.find_resource(product:, destination:, resource:, derived_data_path:)
|
||||
product_path = product_with_name(
|
||||
product,
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
### 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
|
||||
|
||||
### Tuist derived files ###
|
||||
graph.dot
|
||||
Derived/
|
|
@ -0,0 +1,14 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello, world!")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct MainApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?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>com.apple.developer.parent-application-identifiers</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)io.tuist.App</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,14 @@
|
|||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
Text("Hello Appclips!")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import SwiftUI
|
||||
import AppClip
|
||||
|
||||
@main
|
||||
struct MainAppClip: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import XCTest
|
||||
@testable import AppClip
|
||||
|
||||
final class AppClipTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
XCTAssertTrue(2==2)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import XCTest
|
||||
|
||||
final class AppClipUITests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use recording to get started writing UI tests.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import ProjectDescription
|
||||
|
||||
let project = Project(name: "App",
|
||||
organizationName: "Tuist",
|
||||
targets: [
|
||||
Target(name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: .default,
|
||||
sources: ["App/Sources/**",],
|
||||
dependencies: [
|
||||
.target(name: "AppClip1"),
|
||||
]),
|
||||
Target(name: "AppClip1",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "io.tuist.App.Clip",
|
||||
infoPlist: .default,
|
||||
sources: ["AppClip1/Sources/**",],
|
||||
entitlements: "AppClip1/Entitlements/AppClip.entitlements",
|
||||
dependencies: []),
|
||||
Target(name: "AppClip1Tests",
|
||||
platform: .iOS,
|
||||
product: .unitTests,
|
||||
bundleId: "io.tuist.AppClip1Tests",
|
||||
infoPlist: .default,
|
||||
sources: ["AppClip1Tests/Tests/**"],
|
||||
dependencies: [
|
||||
.target(name: "AppClip1")
|
||||
]),
|
||||
Target(name: "AppClip1UITests",
|
||||
platform: .iOS,
|
||||
product: .uiTests,
|
||||
bundleId: "io.tuist.AppClip1UITests",
|
||||
infoPlist: .default,
|
||||
sources: ["AppClip1UITests/Tests/**"],
|
||||
dependencies: [
|
||||
.target(name: "AppClip1")
|
||||
])
|
||||
])
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
name: App Clips
|
||||
excerpt: 'This page documents how to declare and use App Clip targets.'
|
||||
---
|
||||
|
||||
import Message from '../components/message'
|
||||
|
||||
# App Clips
|
||||
|
||||
## Product Types
|
||||
|
||||
An App Clip is a small part of your app that’s discoverable at the moment it’s needed. App Clips are fast and lightweight so a user can open them quickly.
|
||||
|
||||
For example, this is how an AppClip can be declared:
|
||||
|
||||
```swift
|
||||
let project = Project(name: "App",
|
||||
targets: [
|
||||
Target(name: "App",
|
||||
platform: .iOS,
|
||||
product: .app,
|
||||
bundleId: "io.tuist.App",
|
||||
infoPlist: "App/Configs/Info.plist",
|
||||
sources: ["App/Sources/**"],
|
||||
dependencies: [
|
||||
.target(name: "AppClip"),
|
||||
]),
|
||||
Target(name: "AppClip",
|
||||
platform: .iOS,
|
||||
product: .appClip,
|
||||
bundleId: "io.tuist.App.Clip",
|
||||
infoPlist: "AppClip/Configs/Info.plist",
|
||||
sources: ["AppClip/Sources/**",],
|
||||
entitlements: "AppClip/Entitlements/AppClip.entitlements",
|
||||
dependencies: [
|
||||
.sdk(name: "AppClip.framework", status: .required),
|
||||
]),
|
||||
])
|
||||
```
|
||||
|
||||
## Parent Application Identifiers Entitlement
|
||||
|
||||
The Parent Application Identifiers entitlement establishes a secure association between an App Clip and its corresponding app. Add it only to an App Clip target.
|
||||
|
||||
An App Clip is always associated with exactly one app, ensure the parent application entitlement has exactly one entry, the corresponding app’s application identifier as shown below.
|
||||
|
||||
![Diagram that shows appclip's parent application entitlement configuration](assets/appclip-entitlement-example.png)
|
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -28,6 +28,7 @@ import {
|
|||
#### Examples
|
||||
|
||||
- [App Extensions](/docs/examples/app-extensions/)
|
||||
- [App Clips](/docs/examples/app-clips/)
|
||||
|
||||
#### Building at scale
|
||||
|
||||
|
|
|
@ -524,6 +524,10 @@ The type of build product this target will output. It can be any of the followin
|
|||
case: '.messagesExtension',
|
||||
description: 'An iMessage extension. (iOS platform only)',
|
||||
},
|
||||
{
|
||||
case: '.appClip',
|
||||
description: 'An appClip. (iOS platform only)',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
|
|
Loading…
Reference in New Issue