Perserve Workspace.swift Manifest Ordering

I would love to have replied to the original thread with this information but apparently I can’t make more than 3 replies to a thread. Hopefully this new thread can be merged with the original one.

Context

In the original thread there was a discussion as to what would be required to preserve manifest ordering within the Workspace.swift file. This could be desirable if a project has a lot of modules with inconsistent naming strategies and ordering of the generated modules was desired to increase the navigability between those modules. For example two closely related modules are easier to work with and navigate around if they are close to each other in the Xcode navigator than to have them far apart leading to a lot of scrolling up and down (ignoring CMD+SHIFT+O).

By default the ordering of modules is alphabetical. The desire was to introduce a new generation option called .manifestOrder so the user has more control over the generation order of their projects.

I don’t personally have a lot of time to familiarise myself with all of the inner workings of Tuist, but I did a small investigation to see what would be involved with making changes to support this. These are my findings.

Investigation

I added the following in WorkspaceGenerationOptions.swift and the equivalent XcodeGraph files:

public enum ProjectsOrder: Codable, Equatable, Sendable {
    /// Alphabetical order 
    case alphabetical
    /// Keep the order as declared in `Workspace.projects`
    case manifestOrder
}

public var projectsOrder: ProjectsOrder

That was then passed through and accessible in WorkspaceStructureGenerator.swift which is where I then tried to apply the workspace defined ordering, as advised by @pepicrft. Unfortunately the ordering has already been scrambled within the Workspace object by the time it is passed to the generateStructure(…) function which means that simply removing the sorted(by: …) from there isn’t enough.

I went digging a bit further in to this and found that we are calling sorted(by: …) elsewhere throughout the process which makes any changes a bit more wide reaching. But after digging a bit more I found that the ordering within the manifest is being lost when applying the graph mapper in ManifestGraphLoader.swift. Before the mappers are applied my ordering looks something like:

(lldb) p graph.workspace.projects.map(.basename)
([String]) 21 values {
[0] = “My App”
[1] = “AFeature”
[2] = “BFeature”
[3] = “CFeature”
[4] = “DFeature”
[5] = “EFeature”
[6] = “FFeature”
[7] = “GFeature”
[8] = “HFeature”
[9] = “SharedData”
[10] = “SharedUI”
[11] = “FeedData”
[12] = “Networking”
[13] = “Tracking”
[14] = “Authentication”
[15] = “FeatureFlagging”
[16] = “Core”
[17] = “Theming”
[18] = “JSONAPI”
[19] = “TestingUtilities”
[20] = “AsyncImageView”
}

But afterwards, and once the dependencies had been added, the ordering looks something like this:

(lldb) p mappedGraph.workspace.projects.map(.basename)
([String]) 50 values {
[0] = “BFeature”
[1] = “Core”
[2] = “SharedData”
[3] = “google-ads-on-device-conversion-ios-sdk”
[4] = “FFeature”
[5] = “promises”
[6] = “PromiseKit”
[7] = “Kingfisher”
[8] = “GFeature”
[9] = “FeedData”
[10] = “combine-schedulers”
[11] = “Authentication”
[12] = “Networking”
[13] = “swift-concurrency-extras”
[14] = “JSONAPI”
[15] = “swift-custom-dump”
[16] = “CFeature”
[17] = “JWTDecode.swift”
[18] = “firebase-ios-sdk”
[19] = “Starscream”
[20] = “swift-navigation”
[21] = “swift-case-paths”
[22] = “swift-clocks”
[23] = “BFeature”
[24] = “SharedUI”
[25] = “swift-syntax”
[26] = “AsyncImageView”
[27] = “DFeature”
[28] = “xctest-dynamic-overlay”
[29] = “swift-collections”
[30] = “FFeature”
[31] = “AFeature”
...
}

So lets say we try to preserve the ordering here (I made a rough and ready modification to ManifestGraphLoader.swift to do this, but it’s still not right) to give us something like:

(lldb) p mappedGraph.workspace.projects.map(.basename)
([String]) 50 values {
[0] = “My App”
[1] = “AFeature”
[2] = “BFeature”
[3] = “CFeature”
[4] = “DFeature”
[5] = “EFeature”
[6] = “FFeature”
[7] = “GFeature”
[8] = “FeedFeature”
[9] = “SharedData”
[10] = “SharedUI”
[11] = “FeedData”
[12] = “Networking”
[13] = “Tracking”
[14] = “Authentication”
[15] = “FeatureFlagging”
[16] = “Core”
[17] = “Theming”
[18] = “JSONAPI”
[19] = “TestingUtilities”
[20] = “AsyncImageView”
[21] = “HFeature”
[22] = “Auth0.swift”
[23] = “GoogleAppMeasurement”
[24] = “GoogleUtilities”
[25] = “JWTDecode.swift”
[26] = “Kingfisher”
[27] = “PromiseKit”
[28] = “SimpleKeychain”
[29] = “Starscream”
[30] = “SwiftyJSON”
[31] = “combine-schedulers”
[32] = “firebase-ios-sdk”
[33] = “google-ads-on-device-conversion-ios-sdk”
...
}

That order gets preserved all of the way down to the WorkspaceDescriptorGenerator.swift at which point it creates a dictionary of generated projects (and also tries to sort the projects so the ordering is lost again) which are then finally passed in to the workspace structure generator.

At this point the ordering of the workspace is lost again as we simply pass on the generated projects and a couple of things out of the workspace. If I make a modification to pass in the [AbsolutePath] array from the workspace for the ordering then I can refer back to it and finally order the projects of the root graph which is returned.

Questions

Now this is a pretty dirty conceptual approach just to prove out what needs to be done here (for my use case it could potentially work with some more tweaks), but I have some concerns. I’m unsure as to why we are sorting in multiple places - it could be performance or logic related, or potentially a code smell which I’m unaware of. But making changes to these places increases blast radius of these changes and I’m unsure if this will have some unintended consequences as I’m not intimately familiar with the project.

I’m open to ideas from any of the regular maintainers as to what should be done here. I’m unsure if there is appetite for such a change, or if making a change like this is opening a can of worms. I don’t have much huge amount of time to dig in much further due to other commitments, but hopefully some healthy discussion can start to move this investigation forwards a little bit.

Hey @dlbuckley :waving_hand:

This must be some Discourse-default moderation rules. Apologies for that.

It’s been a while without working in that area, but I’d be surprised if the sorting in multiple places is done intentionally and for performance reasons. I’d need to dive into the code to better understand why you don’t see the results that you expect, but with the context that you have, I think you are very close to having something working, so I’d encourage you to push a bit further and maybe lean on coding agents to shed some light on why it’s not working as expected.

I don’t think it’ll open a can of worms, but let’s see what @core thinks about it. We can’t unfortunately prioritize this since we haven’t gotten many requests for this, but having this here might invite others to give it a short and push your work to the finish line.