-
Notifications
You must be signed in to change notification settings - Fork 39
feat: introduce XcodeProjMapper to map XcodeProj to XcodeGraph #87
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
Conversation
fortmarek
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of all, thanks a lot @ajkolean 👏
This work is huge and will enable powerful workflows that the Swift community has not seen, yet.
I did a first pass, trying to mostly focus on the higher level structure of the codebase.
Some of the comments are applicable across the PR, so it'd be amazing if you could have that in mind as I didn't want to repeat myself too much. I tried to point out when I saw some pattern we should imho redo in other places as well, but I might not have done this perfectly.
Overall, I am very onboard with the structure of breaking down the mapping of XcodeProj into multiple layers of individual mappers that have a simple responsibility.
Some of the patterns I'd consider changing (and these are also captured in the individual review comments):
- Prefer structs over classes
- Minimize the public interface of the
XcodeProjToGraphmodule to the bare minimum. This will help us keep this repository stable. - Don't overdo it with running things in parallel. Benchmark first and make asynchronous only parts of the code that are actually resource intensive.
- Use our utility for running shell commands instead of having a new implementation: https://github.com/tuist/command
- Utilities should usually be configured directly via methods instead of passing the configuration at the
inittime. Unless there's a clear win by configuration utilities throughinit, let's keepinitreserved for dependency injection. - Individual mappers should not be accessing layers above. For example, a
PBXBuildPhasemapper should not have a notion ofPBXTarget.
Let me know if we can help you in any way to proceed. This is top on my priority list, so I'll try to provide feedback swiftly here as you go through the review.
Once higher-level concerns are resolved, I'll do another pass going deeper into the individual implementations as I skimmed through some of them during the initial pass.
This must have been a ton of work and we really appreciate it – this is open source at its best 💜
Sources/XcodeProjToGraph/Mappers/Settings/ConfigurationMatcher.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjToGraph/Mappers/TargetAndSchemes/TargetMapper.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjToGraph/ProcessRunner/Lipo/LipoArchsResult.swift
Outdated
Show resolved
Hide resolved
Tests/XcodeProjToGraphTests/MapperTests/Xcode/Build/BuildRuleMapperTests.swift
Outdated
Show resolved
Hide resolved
|
@fortmarek I’ve gone through and addressed almost everything: cleaned up the mappers, streamlined the test data, and tightened up public access. If there’s anything I missed or further tweaks you’d suggest, I’m all ears. Thanks again for taking the time to go through this so thoughtfully. Your sharp eye and collaborative approach made this a great process. Looking forward to any more thoughts! 🙏 |
fortmarek
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some additional feedback. Mostly nits. Some bigger pieces of feedback from this round I'd like to call out:
- When we run into unknown objects and scenarios, I'd lean into throwing instead of swallowing these objects as
nil. That will make it easier for us to spot our blinds spots. Otherwise, the conversion might be lossier than one would expect and then it's gonna be hard to figure out why. - I'd consider removing
ProjectParserand makingGraphMapperas the public interface. To do that, we'll need to make a PR inXcodeProjto accept anAbsolutePathin itsinit - Let's also update
XcodeProjto hold its ownpath– I'd like us to move away from theProjectProviderandWorkspaceProviderhelpers and useXcodeProjdirectly.
| #if DEBUG | ||
| extension GraphTarget { | ||
| public static func test( | ||
| static func test( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually use these in tuist/tuist – this would be a breaking change. We could have a debate on whether this is a good pattern or not, but for the purposes of this PR, I'd keep these test extension in XcodeGraph as public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A reminder on this one.
| extension AbsolutePath { | ||
| /// Maps a path by its extension to a `TargetDependency` if applicable. | ||
| /// | ||
| /// - Parameter condition: Optional platform condition. | ||
| /// - Returns: A `TargetDependency` if the extension matches known dependency types. | ||
| func mapByExtension(condition: PlatformCondition?) -> TargetDependency? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd move this to its own mapper that follows the structure of other mappers. Something akin to:
struct PathDependencyMapper: PathDependencyMapping {
func map(_ pathDependency: AbsolutePath) -> TargetDependency? { ... }
}
Sources/XcodeProjMapper/Extensions/XCWorkspaceDataFileRef+Extensions.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjMapper/Mappers/Targets/PBXTarget+PlatformInference.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjMapper/Mappers/Targets/PBXTargetDependencyMapper.swift
Outdated
Show resolved
Hide resolved
| ) | ||
| } | ||
|
|
||
| // MARK: - Helpers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's mark the helpers as private
Tests/XcodeProjMapperTests/TestData/XCConfigurationList+TestData.swift
Outdated
Show resolved
Hide resolved
| } | ||
| } | ||
|
|
||
| // |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this can be removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be removed?
|
|
||
| // MARK: - WorkspaceFixture | ||
|
|
||
| /// Provides references to sample Xcode workspace fixtures used for integration testing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's create XcodeProjMapperIntegrationTests
| @Suite | ||
| struct XCWorkspaceMapperTests { | ||
| @Test("Maps workspace without any projects or schemes") | ||
| func testMap_NoProjectsOrSchemes() throws { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd consider following the // Given // When // Then comments convention that we have in tuist/tuist. It makes tests easier to parse.
Tests/XcodeProjMapperTests/MapperTests/Workspace/XCWorkspaceMapperTests.swift
Outdated
Show resolved
Hide resolved
Tests/XcodeProjMapperTests/MapperTests/Target/TargetDependencyExtensionsTests.swift
Outdated
Show resolved
Hide resolved
Tests/XcodeProjMapperTests/IntegrationTests/IntegrationTests.swift
Outdated
Show resolved
Hide resolved
Sources/XcodeProjMapper/Mappers/Workspace/XCWorkspaceMapper.swift
Outdated
Show resolved
Hide resolved
|
That was not intended. We should be able to remove now the |
|
Apologies for the delay. I was away due to the holidays. I will respond and update from the feedback within the next few days. I haven't forgotten about it. |
No worries, I figured 🙂 Hope you were able to recharge over the holidays! Looking forward to updates on this, we are still very much excited about this work landing. |
|
Hey @fortmarek! Thanks for all the thorough feedback. I’ve just pushed the latest changes that (hopefully) address the remaining points. Apologies if I missed anything; please let me know if you spot any lingering issues or have more suggestions. 😬 |
fortmarek
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is huge @ajkolean. I have just a couple of last nits after which we can finally merge this 🚀 I am very excited about it 🙂
| import XcodeGraphMapper | ||
|
|
||
| let mapper: XcodeGraphMapping = XcodeGraphMapper() | ||
| let path = try AbsolutePath(validating: "/path/to/MyProjectOrWorkspace") | ||
| let graph = try await mapper.map(at: path) | ||
|
|
||
| *// You now have a Graph containing projects, targets, packages, and dependencies.* | ||
| *// Example: print all target names across all projects* | ||
| for project in graph.projects.values { | ||
| for (targetName, _) in project.targets { | ||
| print("Found target: \(targetName)") | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| import XcodeGraphMapper | |
| let mapper: XcodeGraphMapping = XcodeGraphMapper() | |
| let path = try AbsolutePath(validating: "/path/to/MyProjectOrWorkspace") | |
| let graph = try await mapper.map(at: path) | |
| *// You now have a Graph containing projects, targets, packages, and dependencies.* | |
| *// Example: print all target names across all projects* | |
| for project in graph.projects.values { | |
| for (targetName, _) in project.targets { | |
| print("Found target: \(targetName)") | |
| } | |
| } | |
| ```swift | |
| import XcodeGraphMapper | |
| let mapper: XcodeGraphMapping = XcodeGraphMapper() | |
| let path = try AbsolutePath(validating: "/path/to/MyProjectOrWorkspace") | |
| let graph = try await mapper.map(at: path) | |
| // You now have a Graph containing projects, targets, packages, and dependencies.* | |
| // Example: print all target names across all projects* | |
| for project in graph.projects.values { | |
| for (targetName, _) in project.targets { | |
| print("Found target: \(targetName)") | |
| } | |
| } |
| ) | ||
| ``` | ||
|
|
||
| ## XcodeGraphMapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also should start generating a docc documentation and serve that at tuist.dev. I'll add a note to myself to do that.
|
|
||
| // MARK: - InfoPlist | ||
|
|
||
| public struct infoPlist: Equatable, Codable, Sendable { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels a bit odd that we differentiate between InfoPlist and infoPlist with just a different casing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops, this was an oversight, I was trying to decide between wrapper struct or include configs as associated values. I removed this, good catch!
|
|
||
| extension XCWorkspace { | ||
| /// A computed property that either returns the workspace’s `path` | ||
| /// or throws a `MissingWorkspacePathError` if it’s `nil`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// or throws a `MissingWorkspacePathError` if it’s `nil`. |
We crash if that happens, so we can remove that part of the comment.
|
|
||
| extension XcodeProj { | ||
| /// A computed property that either returns the project’s `path` | ||
| /// or throws a `MissingProjectPathError` if it’s `nil`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// or throws a `MissingProjectPathError` if it’s `nil`. |
|
|
||
| // MARK: - Public API | ||
|
|
||
| public func map(at path: AbsolutePath) async throws -> Graph { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd put this function also in the XcodeGraphMapping protocol.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is in the protocol. I updated the internal parameter name of the protocol to match. Must of missed it when updating and compiler doesn't care if internal ones match
|
A few notes/reminders on somethings:
|
I'm aware of that, that's fine. Performance testing can be done a bit more ad-hoc instead of having these automated. We have a fixture generator in the
Yes! Would you be up to take that work? If not, I could try to carve out some time for it as well. |
|
@ajkolean would you have some time to look into the CI failures? Those look legit. |
Sure I can update after this in.
Updated. I had to bump the macOS version and swift version due to some of the dependencies:
|
That's fine. But looks like there are still some CI issues. |
Seems like at least the linux ones are due to inaccessible |
|
Great work @ajkolean 👏 Let's iterate on the blogpost to share the news that this is now available with the community 💯 |
Description:
This pull request adds new functionality to convert
.xcworkspaceand.xcodeprojfiles into a high-level graph model (XcodeGraph). It introduces:XcodeProjToGraphModuleA domain-focused module responsible for mapping raw Xcode structures (projects, workspaces, schemes, targets, and dependencies) into a cohesive
XcodeGraphrepresentation. This includes:Mappers:
.xcworkspacecontents into aWorkspacedomain model, identifying referenced.xcodeprojfiles and shared schemes.TargetandSchemedomain entities, capturing essential build, test, and run information.Extensions & Helpers:
Additional utilities to bridge the gap between raw XcodeProj objects and the domain model. For example,
AbsolutePath+Extensionshelps resolve file references, andPlatform+Extensionsassists in correctly inferring the platform from PBXTarget attributes.ProcessRunner & LipoTool:
A utility to run external processes (like
lipo) and integrate their output—such as architecture detection—into the graph model. This allows for richer metadata about binary artifacts and supported architectures.TestSupportModuleThis module centralizes mocks, helpers, and utilities that streamline testing. By providing reusable mock objects (e.g., mock project providers, file structures, and build settings), it reduces boilerplate and fosters consistency across all test suites. Key highlights:
XcodeProjToGraphTestsA comprehensive test suite validating each piece of the mapping logic. The tests are thoughtfully organized by domain:
MapperTests:
ParserTests (ProjectParserTests):
Verifies that the parsing logic for
.xcodeprojand.xcworkspacedirectories is robust and reliable, providing a consistent foundation for the mappers.ProcessTests (LipoToolTests, ProcessRunnerTests):
Checks the integration with external tools (
lipo) and process running capabilities, ensuring errors are handled gracefully and output is captured correctly.Next Steps - Integration Tests:
While this PR focuses on unit-level validation, an upcoming PR will introduce integration tests that run end-to-end scenarios using real
.xcworkspaceand.xcodeprojfixtures. This will confirm that the entire pipeline—from reading disk-based Xcode structures to producing a finalXcodeGraph—functions correctly in practice.While this PR focuses on core functionality, the integration tests validating end-to-end scenarios (e.g., parsing
.xcworkspaceand.xcodeprojintoXcodeGraph) are complete. However, due to the size of the test fixtures (15,000 lines), they were removed to reduce noise.To review and run these tests, check out this commit (before they were removed) : 75bf656. This includes real-world project fixtures and ensures the entire pipeline functions as expected in practical scenarios.
The results for these are browsable here: https://github.com/tuist/XcodeGraph/blob/75bf656adcec3b5fe7f4c034f797961361f9f584/Tests/XcodeProjToGraphTests/IntegrationTests/Projects/IntegrationTests%2BcommandLineToolWithDynamicFramework.swift
Testing & Verification:
swift buildto compile all modules.swift testto execute the full test suite. All tests should pass, confirming correct functionality.Once integrated with CI (e.g., GitHub Actions), these tests will run automatically on each PR or commit, ensuring ongoing stability and reliability.
No Breaking Changes:
This PR adds new modules and tests without altering existing functionality. It forms the foundation for further enhancements and refinements.
Documentation & Maintenance:
Code comments and some documentation are included to guide future developers. With a clear architecture, it will be easier to extend capabilities, add integration tests, and enhance performance or features over time.
Summary:
This PR significantly expands the repository’s capabilities by introducing
XcodeProjToGraphandTestSupportmodules, along with a robust test suite inXcodeProjToGraphTests. Together, they provide a structured path from raw Xcode project/workspace data to a domain-levelXcodeGraphmodel. The next step will be to add integration tests to validate complex real-world scenarios, ensuring the tool’s practicality and reliability for future use cases.