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

Ensuring Library search paths and Swift import paths are set #308

Merged
merged 3 commits into from
Mar 27, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Please, check out guidelines: https://keepachangelog.com/en/1.0.0/
- Fix `instance method nearly matches optional requirements` warning in generated `AppDelegate.swift` in iOS projects https://github.com/tuist/tuist/pull/291 by @BalestraPatrick
- Fix Header & Framework search paths override project or xcconfig settings https://github.com/tuist/tuist/pull/301 by @ollieatkinson
- Unit tests bundle for an app target compile & run https://github.com/tuist/tuist/pull/300 by @ollieatkinson
- `LIBRARY_SEARCH_PATHS` and `SWIFT_INCLUDE_PATHS` are now set https://github.com/tuist/tuist/pull/308 by @kwridan

## 0.12.0

Expand Down
76 changes: 56 additions & 20 deletions Sources/TuistKit/Generator/LinkGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ final class LinkGenerator: LinkGenerating {
system: Systeming = System()) throws {
let embeddableFrameworks = try graph.embeddableFrameworks(path: path, name: target.name, system: system)
let headersSearchPaths = graph.librariesPublicHeadersFolders(path: path, name: target.name)
let librarySearchPaths = graph.librariesSearchPaths(path: path, name: target.name)
let swiftIncludePaths = graph.librariesSwiftIncludePaths(path: path, name: target.name)
let linkableModules = try graph.linkableDependencies(path: path, name: target.name)

try generateEmbedPhase(dependencies: embeddableFrameworks,
Expand All @@ -92,6 +94,14 @@ final class LinkGenerator: LinkGenerating {
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)

try setupLibrarySearchPaths(librarySearchPaths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)

try setupSwiftIncludePaths(swiftIncludePaths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)

try generateLinkingPhase(dependencies: linkableModules,
pbxTarget: pbxTarget,
pbxproj: pbxproj,
Expand Down Expand Up @@ -168,39 +178,55 @@ final class LinkGenerator: LinkGenerating {
return nil
}
.map { $0.removingLastComponent() }
.map { $0.relative(to: sourceRootPath).asString }
.map { "$(SRCROOT)/\($0)" }

if paths.isEmpty { return }
let uniquePaths = Array(Set(paths)).sorted()
try setup(setting: "FRAMEWORK_SEARCH_PATHS",
paths: uniquePaths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)
}

let configurationList = pbxTarget.buildConfigurationList
let buildConfigurations = configurationList?.buildConfigurations
func setupHeadersSearchPath(_ paths: [AbsolutePath],
pbxTarget: PBXTarget,
sourceRootPath: AbsolutePath) throws {
try setup(setting: "HEADER_SEARCH_PATHS",
paths: paths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)
}

let pathsValue = Set(paths).sorted().joined(separator: " ")
buildConfigurations?.forEach { buildConfiguration in
var frameworkSearchPaths = (buildConfiguration.buildSettings["FRAMEWORK_SEARCH_PATHS"] as? String) ?? "$(inherited)"
if frameworkSearchPaths.isEmpty {
frameworkSearchPaths = pathsValue
} else {
frameworkSearchPaths.append(" \(pathsValue)")
}
buildConfiguration.buildSettings["FRAMEWORK_SEARCH_PATHS"] = frameworkSearchPaths
}
func setupLibrarySearchPaths(_ paths: [AbsolutePath],
pbxTarget: PBXTarget,
sourceRootPath: AbsolutePath) throws {
try setup(setting: "LIBRARY_SEARCH_PATHS",
paths: paths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)
}

func setupHeadersSearchPath(_ headersFolders: [AbsolutePath],
func setupSwiftIncludePaths(_ paths: [AbsolutePath],
pbxTarget: PBXTarget,
sourceRootPath: AbsolutePath) throws {
let relativePaths = headersFolders
try setup(setting: "SWIFT_INCLUDE_PATHS",
paths: paths,
pbxTarget: pbxTarget,
sourceRootPath: sourceRootPath)
}

private func setup(setting: String,
paths: [AbsolutePath],
pbxTarget: PBXTarget,
sourceRootPath: AbsolutePath) throws {
let relativePaths = paths
.map { $0.relative(to: sourceRootPath).asString }
.map { "$(SRCROOT)/\($0)" }
guard let configurationList = pbxTarget.buildConfigurationList else {
throw LinkGeneratorError.missingConfigurationList(targetName: pbxTarget.name)
}

let pathsValue = relativePaths.joined(separator: " ")
configurationList.buildConfigurations.forEach {
var headers = ($0.buildSettings["HEADER_SEARCH_PATHS"] as? String) ?? "$(inherited)"
headers.append(" \(relativePaths.joined(separator: " "))")
$0.buildSettings["HEADER_SEARCH_PATHS"] = headers
$0.append(setting: setting, value: pathsValue)
}
}

Expand Down Expand Up @@ -266,3 +292,13 @@ final class LinkGenerator: LinkGenerating {
pbxTarget.buildPhases.append(buildPhase)
}
}

private extension XCBuildConfiguration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move to xcodeproj (the library) and make it public. I think it's a method more developers could benefit from.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ‘ will submit a separate PR to xcodeproj for this, it would need to be beefed up a bit further as here we only deal with space separated string settings, however other use cases may contain array of strings.

func append(setting name: String, value: String) {
guard !value.isEmpty else {
return
}
let existing = (buildSettings[name] as? String) ?? "$(inherited)"
buildSettings[name] = [existing, value].joined(separator: " ")
}
}
20 changes: 20 additions & 0 deletions Sources/TuistKit/Graph/Graph.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ protocol Graphing: AnyObject {

func linkableDependencies(path: AbsolutePath, name: String) throws -> [DependencyReference]
func librariesPublicHeadersFolders(path: AbsolutePath, name: String) -> [AbsolutePath]
func librariesSearchPaths(path: AbsolutePath, name: String) -> [AbsolutePath]
func librariesSwiftIncludePaths(path: AbsolutePath, name: String) -> [AbsolutePath]
func embeddableFrameworks(path: AbsolutePath, name: String, system: Systeming) throws -> Set<DependencyReference>
func targetDependencies(path: AbsolutePath, name: String) -> [TargetNode]
func staticDependencies(path: AbsolutePath, name: String) -> [DependencyReference]
Expand Down Expand Up @@ -174,6 +176,24 @@ class Graph: Graphing {
.map(\.publicHeaders)
}

func librariesSearchPaths(path: AbsolutePath, name: String) -> [AbsolutePath] {
guard let targetNode = findTargetNode(path: path, name: name) else {
return []
}

return targetNode.libraryDependencies
.map { $0.path.removingLastComponent() }
}

func librariesSwiftIncludePaths(path: AbsolutePath, name: String) -> [AbsolutePath] {
guard let targetNode = findTargetNode(path: path, name: name) else {
return []
}

return targetNode.libraryDependencies
.compactMap { $0.swiftModuleMap?.removingLastComponent() }
}

func embeddableFrameworks(path: AbsolutePath,
name: String,
system: Systeming) throws -> Set<DependencyReference> {
Expand Down
98 changes: 98 additions & 0 deletions Tests/TuistKitTests/Generator/LinkGeneratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,78 @@ final class LinkGeneratorErrorTests: XCTestCase {
}
}

func test_setupLibrarySearchPaths_multiplePaths() throws {
// Given
let searchPaths = [
AbsolutePath("/path/to/libraries"),
AbsolutePath("/path/to/other/libraries"),
]
let sourceRootPath = AbsolutePath("/path")
let xcodeprojElements = createXcodeprojElements()

// When
try subject.setupLibrarySearchPaths(searchPaths,
pbxTarget: xcodeprojElements.pbxTarget,
sourceRootPath: sourceRootPath)

// Then
let config = xcodeprojElements.config
XCTAssertEqual(config.buildSettings["LIBRARY_SEARCH_PATHS"] as? String,
"$(inherited) $(SRCROOT)/to/libraries $(SRCROOT)/to/other/libraries")
}

func test_setupLibrarySearchPaths_noPaths() throws {
// Given
let searchPaths: [AbsolutePath] = []
let sourceRootPath = AbsolutePath("/path")
let xcodeprojElements = createXcodeprojElements()

// When
try subject.setupLibrarySearchPaths(searchPaths,
pbxTarget: xcodeprojElements.pbxTarget,
sourceRootPath: sourceRootPath)

// Then
let config = xcodeprojElements.config
XCTAssertNil(config.buildSettings["LIBRARY_SEARCH_PATHS"])
}

func test_setupSwiftIncludePaths_multiplePaths() throws {
// Given
let searchPaths = [
AbsolutePath("/path/to/libraries"),
AbsolutePath("/path/to/other/libraries"),
]
let sourceRootPath = AbsolutePath("/path")
let xcodeprojElements = createXcodeprojElements()

// When
try subject.setupSwiftIncludePaths(searchPaths,
pbxTarget: xcodeprojElements.pbxTarget,
sourceRootPath: sourceRootPath)

// Then
let config = xcodeprojElements.config
XCTAssertEqual(config.buildSettings["SWIFT_INCLUDE_PATHS"] as? String,
"$(inherited) $(SRCROOT)/to/libraries $(SRCROOT)/to/other/libraries")
}

func test_setupSwiftIncludePaths_noPaths() throws {
// Given
let searchPaths: [AbsolutePath] = []
let sourceRootPath = AbsolutePath("/path")
let xcodeprojElements = createXcodeprojElements()

// When
try subject.setupSwiftIncludePaths(searchPaths,
pbxTarget: xcodeprojElements.pbxTarget,
sourceRootPath: sourceRootPath)

// Then
let config = xcodeprojElements.config
XCTAssertNil(config.buildSettings["SWIFT_INCLUDE_PATHS"])
}

func test_generateLinkingPhase() throws {
var dependencies: [DependencyReference] = []
dependencies.append(DependencyReference.absolute(AbsolutePath("/test.framework")))
Expand Down Expand Up @@ -197,4 +269,30 @@ final class LinkGeneratorErrorTests: XCTestCase {
XCTAssertEqual($0 as? LinkGeneratorError, LinkGeneratorError.missingProduct(name: "waka.framework"))
}
}

// MARK: - Helpers

struct XcodeprojElements {
var pbxproj: PBXProj
var pbxTarget: PBXNativeTarget
var config: XCBuildConfiguration
}

func createXcodeprojElements() -> XcodeprojElements {
let pbxproj = PBXProj()
let pbxTarget = PBXNativeTarget(name: "Test")
pbxproj.add(object: pbxTarget)

let configurationList = XCConfigurationList(buildConfigurations: [])
pbxproj.add(object: configurationList)
pbxTarget.buildConfigurationList = configurationList

let config = XCBuildConfiguration(name: "Debug")
pbxproj.add(object: config)
configurationList.buildConfigurations.append(config)

return XcodeprojElements(pbxproj: pbxproj,
pbxTarget: pbxTarget,
config: config)
}
}
46 changes: 46 additions & 0 deletions Tests/TuistKitTests/Graph/GraphTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,4 +235,50 @@ final class GraphTests: XCTestCase {
DependencyReference.absolute(frameworkPath),
])
}

func test_librariesSearchPaths() throws {
// Given
let target = Target.test(name: "Main")
let precompiledNode = LibraryNode(path: AbsolutePath("/test/test.a"),
publicHeaders: AbsolutePath("/test/public/"))
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
dependencies: [precompiledNode])
let cache = GraphLoaderCache()
cache.add(targetNode: targetNode)
let graph = Graph.test(cache: cache)

// When
let got = graph.librariesSearchPaths(path: project.path,
name: target.name)

// Then
XCTAssertEqual(got, [AbsolutePath("/test")])
}

func test_librariesSwiftIncludePaths() throws {
// Given
let target = Target.test(name: "Main")
let precompiledNodeA = LibraryNode(path: AbsolutePath("/test/test.a"),
publicHeaders: AbsolutePath("/test/public/"),
swiftModuleMap: AbsolutePath("/test/modules/test.swiftmodulemap"))
let precompiledNodeB = LibraryNode(path: AbsolutePath("/test/another.a"),
publicHeaders: AbsolutePath("/test/public/"),
swiftModuleMap: nil)
let project = Project.test(targets: [target])
let targetNode = TargetNode(project: project,
target: target,
dependencies: [precompiledNodeA, precompiledNodeB])
let cache = GraphLoaderCache()
cache.add(targetNode: targetNode)
let graph = Graph.test(cache: cache)

// When
let got = graph.librariesSwiftIncludePaths(path: project.path,
name: target.name)

// Then
XCTAssertEqual(got, [AbsolutePath("/test/modules")])
}
}
8 changes: 8 additions & 0 deletions fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,17 @@ Workspace:
- BTests (iOS unit tests)
```

A standalone C project is used to generate a prebuilt static library:
```
- C:
- C (static library iOS)
- CTests (iOS unit tests)
```

Dependencies:
- App -> A
- A -> B
- A -> prebuild C (libC.a)

## ios_app_with_static_frameworks

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ let project = Project(name: "A",
sources: "Sources/**",
dependencies: [
.project(target: "B", path: "../B"),
.library(path: "../prebuilt/C/libC.a",
publicHeaders: "../prebuilt/C",
swiftModuleMap: "../prebuilt/C/C.swiftmodule")
]),
Target(name: "ATests",
platform: .iOS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import B
import C

public class A {
public static let value: String = "aValue"

public static func printFromA() {
print("print from A")
B.printFromB()
C.printFromC()
}
}
26 changes: 26 additions & 0 deletions fixtures/ios_app_with_static_libraries/Modules/C/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright Β©. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>