Skip to content

Commit

Permalink
Add new InfoPlist.extendingDefault case (#448)
Browse files Browse the repository at this point in the history
* Add .extendingDefault option to the InfoPlist model

* Use the InfoPlistContentProvider to generate the Info.plist content when it's a .extendingDefault Info.plist.

* Implement the InfoPlistContentProvider

* Test InfoPlistContentProvider

* Add acceptance tests

* Add documentation

* Update CHANGELOG

* Address comments on the PR

* Fix linting issues
  • Loading branch information
Pedro Piñera Buendía committed Sep 3, 2019
1 parent c778fed commit 083defa
Show file tree
Hide file tree
Showing 14 changed files with 471 additions and 82 deletions.
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])

/// 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 }
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": [
"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

0 comments on commit 083defa

Please sign in to comment.