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 xcodePackage target dependency with better local package support #6060

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
33 changes: 32 additions & 1 deletion Sources/ProjectDescription/TargetDependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public enum TargetDependency: Codable, Hashable {
case macro
}

public enum PackageSource: Hashable, Codable {
/// Dependency on a local SPM package.
/// **Note**: If your local package has external, non-local package dependencies, they still need to be defined in your
/// `Tuist/Package.swift` file.
///
/// To link to external package dependencies from you local packages, refer to them via the `byName` static initializer.
case local(Path)
/// Dependency on an external SPM package dependency imported through `Tuist/Package.swift`.
case external
}

/// Dependency on another target within the same project
///
/// - Parameters:
Expand Down Expand Up @@ -76,7 +87,6 @@ public enum TargetDependency: Codable, Hashable {
case library(path: Path, publicHeaders: Path, swiftModuleMap: Path?, condition: PlatformCondition? = nil)

/// Dependency on a swift package manager product using Xcode native integration. It's recommended to use `external` instead.
/// For more info, check the [external dependencies documentation](https://docs.tuist.io/guides/third-party-dependencies/).
///
/// - Parameters:
/// - product: The name of the output product. ${PRODUCT_NAME} inside Xcode.
Expand All @@ -85,6 +95,25 @@ public enum TargetDependency: Codable, Hashable {
/// - condition: condition under which to use this dependency, `nil` if this should always be used
case package(product: String, type: PackageType = .runtime, condition: PlatformCondition? = nil)

/// Dependency on a Swift Package Manager product using Tuist's recommended XcodeProj-based integration.
///
/// You can either depend on an external SPM dependency that needs to be also defined in the `Tuist/Package.swift`:
/// ```
/// .xcodePackage(product: "Alamofire", source: .external)
/// ```
///
/// Or you can depend on a local package by:
/// ```
/// .xcodePackage(product: "MyLibrary", source: .local("MyLocalPackage"))
/// ```
///
/// - Parameters:
/// - product: The name of the output product. ${PRODUCT_NAME} inside Xcode.
/// e.g. RxSwift
/// - source: The source of package to be integreated.
/// - condition: condition under which to use this dependency, `nil` if this should always be used
case xcodePackage(product: String, source: PackageSource = .external, condition: PlatformCondition? = nil)

/// Dependency on system library or framework
///
/// - Parameters:
Expand Down Expand Up @@ -153,6 +182,8 @@ public enum TargetDependency: Codable, Hashable {
return "xctest"
case .external:
return "external"
case .xcodePackage:
return "xcode-package"
}
}
}
7 changes: 4 additions & 3 deletions Sources/TuistKit/Utils/ManifestGraphLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public final class ManifestGraphLoader: ManifestGraphLoading {
at: path,
packageSettings: packageSettings
)
let (workspaceModels, manifestProjects) = (
let (workspace, manifestProjects) = (
try converter.convert(manifest: allManifests.workspace, path: allManifests.path),
allManifests.projects
)
Expand All @@ -139,15 +139,16 @@ public final class ManifestGraphLoader: ManifestGraphLoading {
projects: manifestProjects,
plugins: plugins,
externalDependencies: dependenciesGraph.externalDependencies
.merging(allManifests.packageProducts, uniquingKeysWith: { $1 })
) +
dependenciesGraph.externalProjects.values

// Check circular dependencies
try graphLoaderLinter.lintWorkspace(workspace: workspaceModels, projects: projectsModels)
try graphLoaderLinter.lintWorkspace(workspace: workspace, projects: projectsModels)

// Apply any registered model mappers
let (updatedModels, modelMapperSideEffects) = try workspaceMapper.map(
workspace: .init(workspace: workspaceModels, projects: projectsModels)
workspace: .init(workspace: workspace, projects: projectsModels)
)

// Load graph
Expand Down
146 changes: 88 additions & 58 deletions Sources/TuistLoader/Loaders/RecursiveManifestLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,26 @@ public protocol RecursiveManifestLoading {

public struct LoadedProjects {
public var projects: [AbsolutePath: ProjectDescription.Project]
public var packageProducts: [String: [TuistGraph.TargetDependency]]
}

public struct LoadedWorkspace {
public var path: AbsolutePath
public var workspace: ProjectDescription.Workspace
public var projects: [AbsolutePath: ProjectDescription.Project]
public let packageProducts: [String: [TuistGraph.TargetDependency]]
}

enum ManifestPath: Hashable {
case package(AbsolutePath)
case project(AbsolutePath)

var path: AbsolutePath {
switch self {
case let .project(path), let .package(path):
return path
}
}
}

public class RecursiveManifestLoader: RecursiveManifestLoading {
Expand Down Expand Up @@ -50,33 +64,27 @@ public class RecursiveManifestLoader: RecursiveManifestLoading {

let generatorPaths = GeneratorPaths(manifestDirectory: path)
let projectSearchPaths = (loadedWorkspace?.projects ?? ["."])
let projectPaths = try projectSearchPaths.map {
try generatorPaths.resolve(path: $0)
}.flatMap {
fileHandler.glob($0, glob: "")
}.filter {
fileHandler.isFolder($0)
}.filter {
manifestLoader.manifests(at: $0).contains(.project)
}

let packagePaths = try projectSearchPaths.map {
let manifestPaths: [ManifestPath] = try projectSearchPaths.map {
try generatorPaths.resolve(path: $0)
}.flatMap {
fileHandler.glob($0, glob: "")
}.filter {
fileHandler.isFolder($0) && $0.basename != Constants.tuistDirectoryName && !$0.pathString.contains(".build/checkouts")
}.filter {
}.compactMap {
let manifests = manifestLoader.manifests(at: $0)
return manifests.contains(.package) && !manifests.contains(.project) && !manifests.contains(.workspace)
if manifests.contains(.project) {
return .project($0)
} else if manifests.contains(.package), !manifests.contains(.workspace) {
return .package($0)
} else {
return nil
}
}

let packageProjects = try loadPackageProjects(paths: packagePaths, packageSettings: packageSettings)

let projects = LoadedProjects(projects: try loadProjects(paths: projectPaths).projects.merging(
packageProjects.projects,
uniquingKeysWith: { _, newValue in newValue }
))
let projects = try loadProjects(
paths: manifestPaths,
packageSettings: packageSettings
)
let workspace: ProjectDescription.Workspace
if let loadedWorkspace {
workspace = loadedWorkspace
Expand All @@ -88,68 +96,90 @@ public class RecursiveManifestLoader: RecursiveManifestLoading {
return LoadedWorkspace(
path: path,
workspace: workspace,
projects: projects.projects
projects: projects.projects,
packageProducts: projects.packageProducts
)
}

// MARK: - Private

private func loadPackageProjects(
paths: [AbsolutePath],
private func loadProjects(
paths: [ManifestPath],
packageSettings: TuistGraph.PackageSettings?
) throws -> LoadedProjects {
guard let packageSettings else { return LoadedProjects(projects: [:]) }
var cache = [AbsolutePath: ProjectDescription.Project]()
var packageProducts: [String: [TuistGraph.TargetDependency]] = [:]

var paths = Set(paths)
while !paths.isEmpty {
paths.subtract(cache.keys)
let projects = try Array(paths).compactMap(context: ExecutionContext.concurrent) {
let packageInfo = try manifestLoader.loadPackage(at: $0)
return try packageInfoMapper.map(
packageInfo: packageInfo,
path: $0,
packageType: .local,
packageSettings: packageSettings,
packageToProject: [:]
)
paths = paths.filter {
!cache.keys.contains($0.path)
}
var newDependenciesPaths = Set<AbsolutePath>()
for (path, project) in zip(paths, projects) {
cache[path] = project
newDependenciesPaths.formUnion(try dependencyPaths(for: project, path: path))
var newDependenciesPaths = Set<ManifestPath>()
let projects = try Array(paths).compactMap(context: ExecutionContext.concurrent) { manifestPath in
switch manifestPath {
case let .project(path):
return try manifestLoader.loadProject(at: path)
case let .package(path):
guard let packageSettings else { return nil }
let packageInfo = try manifestLoader.loadPackage(at: path)
newDependenciesPaths.formUnion(
try packageInfo.dependencies.map {
switch $0 {
case let .local(path: localPackagePath):
return try .package(AbsolutePath(validating: localPackagePath))
}
}
)

let packageProject = try packageInfoMapper.map(
packageInfo: packageInfo,
path: path,
packageType: .local,
packageSettings: packageSettings,
packageToProject: [:]
)

for product in packageInfo.products {
packageProducts[product.name] = product.targets.map { target in
TuistGraph.TargetDependency.project(
target: target,
path: path,
condition: nil
)
}
}

return packageProject
}
}
paths = newDependenciesPaths
}
return LoadedProjects(projects: cache)
}

private func loadProjects(paths: [AbsolutePath]) throws -> LoadedProjects {
var cache = [AbsolutePath: ProjectDescription.Project]()

var paths = Set(paths)
while !paths.isEmpty {
paths.subtract(cache.keys)
let projects = try Array(paths).map(context: ExecutionContext.concurrent) {
try manifestLoader.loadProject(at: $0)
}
var newDependenciesPaths = Set<AbsolutePath>()
for (path, project) in zip(paths, projects) {
cache[path] = project
newDependenciesPaths.formUnion(try dependencyPaths(for: project, path: path))
for (manifestPath, project) in zip(paths, projects) {
cache[manifestPath.path] = project
newDependenciesPaths.formUnion(try dependencyPaths(for: project, path: manifestPath.path))
}
paths = newDependenciesPaths
}
return LoadedProjects(projects: cache)
return LoadedProjects(
projects: cache,
packageProducts: packageProducts
)
}

private func dependencyPaths(for project: ProjectDescription.Project, path: AbsolutePath) throws -> [AbsolutePath] {
private func dependencyPaths(for project: ProjectDescription.Project, path: AbsolutePath) throws -> [ManifestPath] {
let generatorPaths = GeneratorPaths(manifestDirectory: path)
let paths: [AbsolutePath] = try project.targets.flatMap {
let paths: [ManifestPath] = try project.targets.flatMap {
try $0.dependencies.compactMap {
switch $0 {
case let .project(target: _, path: projectPath, _):
return try generatorPaths.resolve(path: projectPath)
return .project(try generatorPaths.resolve(path: projectPath))
case let .xcodePackage(product: _, source: source, condition: _):
switch source {
case .external:
return nil
case let .local(packagePath):
return .package(try generatorPaths.resolve(path: packagePath))
}
default:
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,29 @@ extension TuistGraph.TargetDependency {
case .xctest:
return [.xctest]
case let .external(name, condition):
guard let dependencies = externalDependencies[name] else {
if let dependencies = externalDependencies[name] {
return dependencies.map { $0.withCondition(condition?.asGraphCondition) }
} else {
throw TargetDependencyMapperError.invalidExternalDependency(name: name)
}

return dependencies.map { $0.withCondition(condition?.asGraphCondition) }
case let .xcodePackage(product: product, source: source, condition: condition):
switch source {
case .external:
guard let dependencies = externalDependencies[product] else {
throw TargetDependencyMapperError.invalidExternalDependency(name: product)
}

return dependencies.map { $0.withCondition(condition?.asGraphCondition) }
case let .local(packagePath):
return [
.project(
target: product,
path: try generatorPaths.resolve(path: packagePath),
condition: condition?.asGraphCondition
),
]
}
}
}
}
Expand Down