Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new InfoPlist.extendingDefault case #448

Merged
merged 9 commits into from Sep 3, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,7 +4,12 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/

## Next

### Added

- New InfoPlist type, `.extendingDefault([:])` https://github.com/tuist/tuist/pull/448 by @pepibumur

### Fixed

- Transitively link static dependency's dynamic dependencies correctly https://github.com/tuist/tuist/pull/484 by @adamkhazi
- Prevent embedding static frameworks https://github.com/tuist/tuist/pull/490 by @kwridan

Expand Down
18 changes: 18 additions & 0 deletions Sources/ProjectDescription/InfoPlist.swift
Expand Up @@ -70,9 +70,20 @@ public enum InfoPlist: Codable, Equatable {
}
}

/// Use an existing Info.plist file.
case file(path: String)

/// Generate an Info.plist file with the content in the given dictionary.
case dictionary([String: Value])

/// Generate an Info.plist file with the default content for the target product extended with the values in the given dictionary.
case extendingDefault(with: [String: Value])
pepicrft marked this conversation as resolved.
Show resolved Hide resolved

/// Default value.
public static var `default`: InfoPlist {
return .extendingDefault(with: [:])
}

// MARK: - Error

public enum CodingError: Error {
Expand Down Expand Up @@ -105,6 +116,8 @@ public enum InfoPlist: Codable, Equatable {
return lhsPath == rhsPath
case let (.dictionary(lhsDictionary), .dictionary(rhsDictionary)):
return lhsDictionary == rhsDictionary
case let (.extendingDefault(lhsDictionary), .extendingDefault(rhsDictionary)):
return lhsDictionary == rhsDictionary
default:
return false
}
Expand All @@ -121,6 +134,9 @@ public enum InfoPlist: Codable, Equatable {
case let .dictionary(dictionary):
try container.encode("dictionary", forKey: .type)
try container.encode(dictionary, forKey: .value)
case let .extendingDefault(dictionary):
try container.encode("extended", forKey: .type)
try container.encode(dictionary, forKey: .value)
}
}

Expand All @@ -132,6 +148,8 @@ public enum InfoPlist: Codable, Equatable {
self = .file(path: try container.decode(String.self, forKey: .value))
case "dictionary":
self = .dictionary(try container.decode([String: Value].self, forKey: .value))
case "extended":
self = .extendingDefault(with: try container.decode([String: Value].self, forKey: .value))
default:
preconditionFailure("unsupported type")
}
Expand Down
31 changes: 26 additions & 5 deletions Sources/TuistGenerator/Generator/DerivedFileGenerator.swift
Expand Up @@ -18,6 +18,17 @@ final class DerivedFileGenerator: DerivedFileGenerating {
fileprivate static let derivedFolderName = "Derived"
fileprivate static let infoPlistsFolderName = "InfoPlists"

/// Info.plist content provider.
let infoPlistContentProvider: InfoPlistContentProviding

/// Initializes the generator with its attributes.
///
/// - Parameters:
/// - infoPlistContentProvider: Info.plist content provider.
init(infoPlistContentProvider: InfoPlistContentProviding = InfoPlistContentProvider()) {
self.infoPlistContentProvider = infoPlistContentProvider
}

/// Generates the derived files that are associated to the given project.
///
/// - Parameters:
Expand Down Expand Up @@ -47,8 +58,9 @@ final class DerivedFileGenerator: DerivedFileGenerating {
func generateInfoPlists(project: Project, sourceRootPath: AbsolutePath) throws -> Set<AbsolutePath> {
let infoPlistsPath = DerivedFileGenerator.infoPlistsPath(sourceRootPath: sourceRootPath)
let targetsWithGeneratableInfoPlists = project.targets.filter {
guard let infoPlist = $0.infoPlist else { return false }
guard case InfoPlist.dictionary = infoPlist else { return false }
pepicrft marked this conversation as resolved.
Show resolved Hide resolved
if let infoPlist = $0.infoPlist, case InfoPlist.file = infoPlist {
return false
}
return true
}

Expand All @@ -67,13 +79,22 @@ final class DerivedFileGenerator: DerivedFileGenerating {
// Generate the Info.plist
try targetsWithGeneratableInfoPlists.forEach { target in
guard let infoPlist = target.infoPlist else { return }
guard case let InfoPlist.dictionary(dictionary) = infoPlist else { return }

let dictionary: [String: Any]

if case let InfoPlist.dictionary(content) = infoPlist {
dictionary = content.mapValues { $0.value }
} else if case let InfoPlist.extendingDefault(extended) = infoPlist,
let content = self.infoPlistContentProvider.content(target: target, extendedWith: extended) {
dictionary = content
} else {
return
}

let path = DerivedFileGenerator.infoPlistPath(target: target, sourceRootPath: sourceRootPath)
if FileHandler.shared.exists(path) { try FileHandler.shared.delete(path) }

let outputDictionary = dictionary.mapValues { $0.value }
let data = try PropertyListSerialization.data(fromPropertyList: outputDictionary,
let data = try PropertyListSerialization.data(fromPropertyList: dictionary,
format: .xml,
options: 0)

Expand Down
151 changes: 151 additions & 0 deletions Sources/TuistGenerator/Generator/InfoPlistContentProvider.swift
@@ -0,0 +1,151 @@
import Foundation

/// Defines the interface to obtain the content to generate derived Info.plist files for the targets.
protocol InfoPlistContentProviding {
/// It returns the content that should be used to generate an Info.plist file
/// for the given target. It uses default values that specific to the target's platform
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]?
}

final class InfoPlistContentProvider: InfoPlistContentProviding {
/// It returns the content that should be used to generate an Info.plist file
/// for the given target. It uses default values that specific to the target's platform
/// and product, and extends them with the values provided by the user.
///
/// - Parameters:
/// - target: Target whose Info.plist content will be returned.
/// - extendedWith: Values provided by the user to extend the default ones.
/// - Returns: Content to generate the Info.plist file.
func content(target: Target, extendedWith: [String: InfoPlist.Value]) -> [String: Any]? {
if target.product == .staticLibrary || target.product == .dynamicLibrary {
return nil
}

var content = base()

// Bundle package type
extend(&content, with: bundlePackageType(target))

// iOS app
if target.product == .app, target.platform == .iOS {
extend(&content, with: iosApp())
}

// macOS app
if target.product == .app, target.platform == .iOS {
extend(&content, with: macosApp())
}

// macOS
if target.platform == .macOS {
extend(&content, with: macos())
}

extend(&content, with: extendedWith.unwrappingValues())

return content
}

/// Returns a dictionary that contains the base content that all Info.plist
/// files should have regardless of the platform or product.
///
/// - Returns: Base content.
func base() -> [String: Any] {
return [
"CFBundleDevelopmentRegion": "$(DEVELOPMENT_LANGUAGE)",
"CFBundleExecutable": "$(EXECUTABLE_NAME)",
"CFBundleIdentifier": "$(PRODUCT_BUNDLE_IDENTIFIER)",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleName": "$(PRODUCT_NAME)",
"CFBundleShortVersionString": "1.0",
"CFBundleVersion": "1",
]
}

/// Returns the Info.plist content that includes the CFBundlePackageType
/// attribute depending on the target product type.
///
/// - Parameter target: Target whose Info.plist's CFBundlePackageType will be returned.
/// - Returns: Dictionary with the CFBundlePackageType attribute.
func bundlePackageType(_ target: Target) -> [String: Any] {
var packageType: String?

switch target.product {
case .app:
packageType = "APPL"
case .staticLibrary, .dynamicLibrary:
packageType = nil
case .uiTests, .unitTests, .bundle:
packageType = "BNDL"
case .staticFramework, .framework:
packageType = "FMWK"
}

if let packageType = packageType {
return ["CFBundlePackageType": packageType]
} else {
return [:]
}
}

/// Returns the default Info.plist content that iOS apps should have.
///
/// - Returns: Info.plist content.
func iosApp() -> [String: Any] {
return [
"LSRequiresIPhoneOS": true,
"UILaunchStoryboardName": "LaunchScreen",
"UIMainStoryboardFile": "Main",
"UIRequiredDeviceCapabilities": [
"armv7",
],
"UISupportedInterfaceOrientations": [
pepicrft marked this conversation as resolved.
Show resolved Hide resolved
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationLandscapeLeft",
"UIInterfaceOrientationLandscapeRight",
],
"UISupportedInterfaceOrientations~ipad": [
"UIInterfaceOrientationPortrait",
"UIInterfaceOrientationPortraitUpsideDown",
"UIInterfaceOrientationLandscapeLeft",
"UIInterfaceOrientationLandscapeRight",
],
]
}

/// Returns the default Info.plist content that macOS apps should have.
///
/// - Returns: Info.plist content.
func macosApp() -> [String: Any] {
return [
"CFBundleIconFile": "",
"LSMinimumSystemVersion": "$(MACOSX_DEPLOYMENT_TARGET)",
"NSMainStoryboardFile": "Main",
"NSPrincipalClass": "NSApplication",
]
}

/// Returns the default Info.plist content that macOS targets should have.
///
/// - Returns: Info.plist content.
func macos() -> [String: Any] {
return [
"NSHumanReadableCopyright": "Copyright ©. All rights reserved.",
]
}

/// Given a dictionary, it extends it with another dictionary.
///
/// - Parameters:
/// - base: Dictionary to be extended.
/// - with: The content to extend the dictionary with.
fileprivate func extend(_ base: inout [String: Any], with: [String: Any]) {
with.forEach { base[$0.key] = $0.value }
}
}
11 changes: 11 additions & 0 deletions Sources/TuistGenerator/Models/InfoPlist.swift
Expand Up @@ -45,6 +45,7 @@ public enum InfoPlist: Equatable {

case file(path: AbsolutePath)
case dictionary([String: Value])
case extendingDefault(with: [String: Value])

// MARK: - Equatable

Expand All @@ -54,6 +55,8 @@ public enum InfoPlist: Equatable {
return lhsPath == rhsPath
case let (.dictionary(lhsDictionary), .dictionary(rhsDictionary)):
return lhsDictionary == rhsDictionary
case let (.extendingDefault(lhsDictionary), .extendingDefault(rhsDictionary)):
return lhsDictionary == rhsDictionary
default:
return false
}
Expand Down Expand Up @@ -118,3 +121,11 @@ extension InfoPlist.Value: ExpressibleByArrayLiteral {
self = .array(elements)
}
}

// MARK: - Dictionary (InfoPlist.Value)

extension Dictionary where Value == InfoPlist.Value {
func unwrappingValues() -> [Key: Any] {
return mapValues { $0.value }
}
}
3 changes: 3 additions & 0 deletions Sources/TuistKit/Generator/GeneratorModelLoader.swift
Expand Up @@ -359,6 +359,9 @@ extension TuistGenerator.InfoPlist {
return .dictionary(
dictionary.mapValues { TuistGenerator.InfoPlist.Value.from(manifest: $0) }
)
case let .extendingDefault(dictionary):
return .extendingDefault(with:
dictionary.mapValues { TuistGenerator.InfoPlist.Value.from(manifest: $0) })
}
}
}
Expand Down
Expand Up @@ -7,14 +7,17 @@ import XCTest

final class DerivedFileGeneratorTests: XCTestCase {
var fileHandler: MockFileHandler!
var infoPlistContentProvider: MockInfoPlistContentProvider!
var subject: DerivedFileGenerator!

override func setUp() {
super.setUp()
mockEnvironment()

fileHandler = sharedMockFileHandler()
infoPlistContentProvider = MockInfoPlistContentProvider()

subject = DerivedFileGenerator()
subject = DerivedFileGenerator(infoPlistContentProvider: infoPlistContentProvider)
}

func test_generate_generatesTheInfoPlistFiles_whenDictionaryInfoPlist() throws {
Expand All @@ -37,6 +40,30 @@ final class DerivedFileGeneratorTests: XCTestCase {
.isEqual(to: ["a": "b"]))
}

func test_generate_generatesTheInfoPlistFiles_whenExtendingDefault() throws {
// Given
let sourceRootPath = fileHandler.currentPath
let target = Target.test(name: "Target", infoPlist: InfoPlist.extendingDefault(with: ["a": "b"]))
let project = Project.test(name: "App", targets: [target])
let infoPlistsPath = DerivedFileGenerator.infoPlistsPath(sourceRootPath: sourceRootPath)
let path = infoPlistsPath.appending(component: "Target.plist")
infoPlistContentProvider.contentStub = ["test": "value"]

// When
_ = try subject.generate(project: project,
sourceRootPath: sourceRootPath)

// Then
XCTAssertTrue(fileHandler.exists(path))
XCTAssertTrue(infoPlistContentProvider.contentArgs.first?.target == target)
XCTAssertTrue(infoPlistContentProvider.contentArgs.first?.extendedWith["a"] == "b")

let writtenData = try Data(contentsOf: path.url)
let content = try PropertyListSerialization.propertyList(from: writtenData, options: [], format: nil)
XCTAssertTrue(NSDictionary(dictionary: (content as? [String: Any]) ?? [:])
.isEqual(to: ["test": "value"]))
}

func test_generate_returnsABlockToDeleteUnnecessaryInfoPlistFiles() throws {
// Given
let sourceRootPath = fileHandler.currentPath
Expand Down