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

#17: Basic XCode Workspace support #18

Open
wants to merge 1 commit into
base: master
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// XcodeProjectAsDependencyManager.swift
//
//
// Created by Max Chuquimia on 1/2/2023.
//

import Foundation
import XCGrapherPluginSupport

struct XcodeProjectAsDependencyManager {

let allCustomFrameworks: [String: [FileManager.Path]]

init(projects: [FileManager.Path]) throws {
var allCustomFrameworks: [String: [FileManager.Path]] = [:]
for project in projects {
let targets = try XcodeprojTargets(projectFile: project).targetList()
for target in targets {
allCustomFrameworks[target] = try Xcodeproj(projectFile: project, target: target).compileSourcesList()
}
}
self.allCustomFrameworks = allCustomFrameworks
}

}

extension XcodeProjectAsDependencyManager: DependencyManager {

var pluginModuleType: XCGrapherImport.ModuleType {
.target
}

func isManaging(module: String) -> Bool {
allCustomFrameworks.keys.contains(module)
}

func dependencies(of module: String) -> [String] {
guard let files = allCustomFrameworks[module] else { return [] }
return ImportFinder(fileList: files).allImportedModules()
}

}
32 changes: 32 additions & 0 deletions Sources/XCGrapherLib/PluginSupport/PluginSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class PluginSupport {
var swiftPackageManager: SwiftPackageManager?
var cocoapodsManager: CocoapodsManager?
var nativeManager: NativeDependencyManager?
var customProjectManager: XcodeProjectAsDependencyManager?
var unknownManager: UnmanagedDependencyManager?

init(pluginPath: FileManager.Path) throws {
Expand Down Expand Up @@ -62,6 +63,12 @@ class PluginSupport {
nodes.append(contentsOf: _nodes)
}

// MARK: - Custom
else if customProjectManager?.isManaging(module: module) == true {
var previouslyEncounteredModules: Set<String> = []
try recurseCustomXcodeProjects(from: module, importedBy: target, importerType: .target, building: &nodes, skipping: &previouslyEncounteredModules)
}

// Weird unknown cases
else if unknownManager?.isManaging(module: module) == true {
let _nodes = try plugin_process(library: XCGrapherImport(moduleName: module, importerName: target, moduleType: .other, importerType: .target))
Expand Down Expand Up @@ -143,6 +150,31 @@ private extension PluginSupport {
}
}

func recurseCustomXcodeProjects(from module: String, importedBy importer: String, importerType: XCGrapherImport.ModuleType, building nodeList: inout [Any], skipping modulesToSkip: inout Set<String>) throws {
let _nodes = try plugin_process(library: XCGrapherImport(moduleName: module, importerName: importer, moduleType: .other, importerType: importerType))
nodeList.append(contentsOf: _nodes)

guard !modulesToSkip.contains(module) else { return }
modulesToSkip.insert(module)

guard customProjectManager?.isManaging(module: module) == true else { return }

for importedName in customProjectManager?.dependencies(of: module) ?? [] {
if swiftPackageManager?.isManaging(module: importedName) == true {
try recurseSwiftPackages(from: importedName, importedBy: module, importerType: .other, building: &nodeList, skipping: &modulesToSkip)
} else if customProjectManager?.isManaging(module: importedName) == true {
try recurseCustomXcodeProjects(from: importedName, importedBy: module, importerType: .other, building: &nodeList, skipping: &modulesToSkip)
} else if cocoapodsManager?.isManaging(module: importedName) == true {
try recurseCocoapods(from: importedName, importedBy: module, importerType: .other, building: &nodeList, skipping: &modulesToSkip)
} else if nativeManager?.isManaging(module: importedName) == true {
modulesToSkip.insert(importedName)

let _nodes = try plugin_process(library: XCGrapherImport(moduleName: importedName, importerName: module, moduleType: .apple, importerType: .other))
nodeList.append(contentsOf: _nodes)
}
}
}

}

// MARK: - Plugin Caller Proxies
Expand Down
79 changes: 79 additions & 0 deletions Sources/XCGrapherLib/ShellTasks/XCWorkspace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// XCWorkspace.swift
//
//
// Created by Max Chuquimia on 1/2/2023.
//

import Foundation

struct XCWorkspace {

let file: FileManager.Path
let options: XCGrapherOptions

func referencesList() throws -> [FileManager.Path] {
try execute()
.breakIntoLines()
.filter { !$0.isEmpty }
}

func xcodeProjectList() throws -> [FileManager.Path] {
try referencesList()
.filter { path in
path.hasSuffix(".xcodeproj")
}
}

func fakeHeaderFile() throws -> FileManager.Path {
let xcodeTargets = try xcodeProjectList()
.flatMap { path in
try XcodeprojTargets(projectFile: path).targetList()
}

let spmTargets = try swiftPackagesList()
.flatMap { path in
try SwiftPackage(clone: path).targets()
}
.map(\.name)

let fakeFilePath = "/tmp/WorkspaceHeader.swift"
var fakeFile =
"""
// Generated by XCGrapher.
// This file is a kind of "header" for \(file).
// It imports all targets discovered in the workspace to provide a starting point for XCGrapher.


"""
for target in xcodeTargets + spmTargets {
fakeFile.append("import " + target + "\n")
}

try fakeFile
.data(using: .utf8)!
.write(to: URL(fileURLWithPath: fakeFilePath))

return fakeFilePath
}

func swiftPackagesList() throws -> [FileManager.Path] {
try referencesList()
.filter { path in
FileManager.default.fileExists(atPath: path.appendingPathComponent("Package.swift"))
}
}

}

extension XCWorkspace: ShellTask {

var stringRepresentation: String {
"ruby -r xcodeproj -e 'Xcodeproj::Workspace.new_from_xcworkspace(\"\(file)\").schemes.each do |s| puts s[1] end'"
}

var commandNotFoundInstructions: String {
"Missing command 'xcodeproj' - install it with `gem install xcodeproj` or see https://github.com/CocoaPods/Xcodeproj"
}

}
14 changes: 11 additions & 3 deletions Sources/XCGrapherLib/ShellTasks/Xcodebuild.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import Foundation

struct Xcodebuild: SwiftPackageDependencySource {
let projectFile: FileManager.Path
let target: String

let commandArgs: String

init(projectFile: FileManager.Path, target: String) {
commandArgs = "-project \"\(projectFile)\" -target \"\(target)\""
}

init(workspaceFile: FileManager.Path, scheme: String) {
commandArgs = "-workspace \"\(workspaceFile)\" -scheme \"\(scheme)\""
}

func computeCheckoutsDirectory() throws -> String {
// Clone all the packages into $DERIVED_DATA/SourcePackages/checkouts
Expand All @@ -26,7 +34,7 @@ struct Xcodebuild: SwiftPackageDependencySource {
extension Xcodebuild: ShellTask {

var stringRepresentation: String {
"xcodebuild -project \"\(projectFile)\" -target \"\(target)\" -showBuildSettings"
"xcodebuild \(commandArgs) -showBuildSettings"
}

var commandNotFoundInstructions: String {
Expand Down
32 changes: 32 additions & 0 deletions Sources/XCGrapherLib/ShellTasks/XcodeprojTargets.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// XcodeprojTargets.swift
//
//
// Created by Max Chuquimia on 1/2/2023.
//

import Foundation

struct XcodeprojTargets {

let projectFile: FileManager.Path

func targetList() throws -> [String] {
try execute()
.breakIntoLines()
.filter { !$0.isEmpty }
}

}

extension XcodeprojTargets: ShellTask {

var stringRepresentation: String {
"ruby -r xcodeproj -e 'Xcodeproj::Project.open(\"\(projectFile)\").targets.each do |t| puts t.name end'"
}

var commandNotFoundInstructions: String {
"Missing command 'xcodeproj' - install it with `gem install xcodeproj` or see https://github.com/CocoaPods/Xcodeproj"
}

}
38 changes: 29 additions & 9 deletions Sources/XCGrapherLib/XCGrapher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,31 @@ public enum XCGrapher {
Log("Generating list of source files in \(options.startingPoint.localisedName)")
var sources: [FileManager.Path] = []
switch options.startingPoint {
case let .xcodeProject(project):
let xcodeproj = Xcodeproj(projectFile: project, target: options.target)
case let .xcodeProject(project, target):
let xcodeproj = Xcodeproj(projectFile: project, target: target)
sources = try xcodeproj.compileSourcesList()
case let .swiftPackage(packagePath):
case let .swiftPackage(packagePath, target):
let package = SwiftPackage(clone: packagePath)
guard let target = try package.targets().first(where: { $0.name == options.target }) else { die("Could not locate target '\(options.target)'") }
guard let target = try package.targets().first(where: { $0.name == target }) else { die("Could not locate target '\(target)'") }
sources = target.sources
case let .xcodeWorkspace(workspace, _):
sources = [try XCWorkspace(file: workspace, options: options).fakeHeaderFile()]
}

// MARK: - Create dependency manager lookups

if options.spm || options.startingPoint.isSPM {
Log("Building Swift Package list")
var additionalPackages: [FileManager.Path] = []
let swiftPackageDependencySource: SwiftPackageDependencySource
switch options.startingPoint {
case .xcodeProject: swiftPackageDependencySource = Xcodebuild(projectFile: options.startingPoint.path, target: options.target)
case .swiftPackage: swiftPackageDependencySource = SwiftBuild(packagePath: options.startingPoint.path, product: options.target)
case let .xcodeProject(path, target): swiftPackageDependencySource = Xcodebuild(projectFile: path, target: target)
case let .swiftPackage(path, target): swiftPackageDependencySource = SwiftBuild(packagePath: path, product: target)
case let .xcodeWorkspace(path, scheme):
swiftPackageDependencySource = Xcodebuild(workspaceFile: path, scheme: scheme)
additionalPackages = try XCWorkspace(file: path, options: options).swiftPackagesList()
}
let swiftPackageClones = try swiftPackageDependencySource.swiftPackageDependencies()
let swiftPackageClones = try swiftPackageDependencySource.swiftPackageDependencies() + additionalPackages
let swiftPackageManager = try SwiftPackageManager(packageClones: swiftPackageClones)
pluginHandler.swiftPackageManager = swiftPackageManager
}
Expand All @@ -48,9 +54,16 @@ public enum XCGrapher {
pluginHandler.nativeManager = nativeManager
}

if case let .xcodeWorkspace(path, _) = options.startingPoint {
Log("Building custom framework list")
let projects = try XCWorkspace(file: path, options: options).xcodeProjectList()
let customManager = try XcodeProjectAsDependencyManager(projects: projects)
pluginHandler.customProjectManager = customManager
}

if options.force {
Log("Ensuring all additional modules are graphed")
// Don't ignore unknown dependencies - add a manager that claims it is reponsible for them being there.
// Don't ignore unknown dependencies - add a manager that claims it is responsible for them being there.
// MUST be last in `allDependencyManagers`.
let unknownManager = UnmanagedDependencyManager()
pluginHandler.unknownManager = unknownManager
Expand All @@ -60,8 +73,15 @@ public enum XCGrapher {

Log("Graphing...")

let digraphTargetDisplayName: String
switch options.startingPoint {
case let .xcodeProject(_, target): digraphTargetDisplayName = target
case let .swiftPackage(_, target): digraphTargetDisplayName = target
case let .xcodeWorkspace(workspace, _): digraphTargetDisplayName = workspace.lastPathComponent()
}

let digraph = try pluginHandler.generateDigraph(
target: options.target,
target: digraphTargetDisplayName,
projectSourceFiles: sources
)

Expand Down
19 changes: 11 additions & 8 deletions Sources/XCGrapherLib/XCGrapherOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation

public protocol XCGrapherOptions {
var startingPoint: StartingPoint { get }
var target: String { get }
var podlock: String { get }
var output: String { get }
var apple: Bool { get }
Expand All @@ -14,27 +13,31 @@ public protocol XCGrapherOptions {

public enum StartingPoint {

case xcodeProject(String)
case swiftPackage(String)
case xcodeProject(String, String)
case swiftPackage(String, String)
case xcodeWorkspace(String, String)

var localisedName: String {
switch self {
case let .xcodeProject(project): return "Xcode project at path '\(project)'"
case let .swiftPackage(packagePath): return "Swift Package at path '\(packagePath)'"
case let .xcodeProject(project, _): return "Xcode project at path '\(project)'"
case let .swiftPackage(packagePath, _): return "Swift Package at path '\(packagePath)'"
case let .xcodeWorkspace(workspace, _): return "Xcode workspace at path '\(workspace)'"
}
}

var isSPM: Bool {
switch self {
case .xcodeProject: return false
case .xcodeProject, .xcodeWorkspace: return false
case .swiftPackage: return true
}
}

var path: String {
switch self {
case let .xcodeProject(projectPath): return projectPath
case let .swiftPackage(packagePath): return packagePath
case
let .xcodeProject(path, _),
let .swiftPackage(path, _),
let .xcodeWorkspace(path, _): return path
}
}

Expand Down
Loading