This page describes principles that are pillars to the design and development of Tuist. They evolve with the project and are meant to ensure a sustainable growth that is well-aligned with the project foundation.
Here follows a list of the most important principles:
One of the reasons why Tuist exists is because Xcode is weak in conventions and that leads to complex projects that are hard to scale up and maintain. For that reason, Tuist takes a different approach by defaulting to simple and thoroughly designed conventions. Developers can opt-out from the conventions, but that’s a conscious decision that doesn’t feel natural.
For example, there’s a convention for defining dependencies between targets by using the provided public interface. By doing that, Tuist ensures that the projects are generated with the right configurations for the linking to work. Developers have the option to define the dependencies through build settings, but they’d be doing it implicitly and therefore breaking Tuist features such as “graph” that rely on some conventions being followed.
The reason why we default to conventions is that the more decision we can make on behalf of the developers, the more focus they’ll have crafting features for their apps. When we are left with no conventions like it’s the case in many projects, we have to make decisions that will end up not being consistent with other decisions and as a consequence, there’ll be an accidental complexity that will be hard to manage.
Having many layers of configurations and contracts between them results in a project setup that is hard to reason about and maintain. Think for a second on an average project. The definition of the project lives in the .xcodeproj directories, the CLI in scripts (e.g Fastfiles), and the CI logic in pipelines. Those are three layers with contracts between them that we need to maintain. How often have you been in a situation where you changed something in your projects, and then a week later you realized that the release scripts broke?
We can simplify this by having a single source of truth, the manifest files. Those files provide Tuist with the information that it needs to generate Xcode projects that developers can use to edit their files. Moreover, it allows having standard commands for building projects from a local or CI environment.
Tuist should own the complexity and expose a simple, safe, and enjoyable interface to describe their projects as explicitly as possible.
Xcode supports implicit configurations. A good example of that is inferring the implicitly defined dependencies. While implicitness is fine for small projects, where configurations are simple, as projects get larger it might cause slowness or odd behaviors.
Tuist should provide explicit APIs for implicit Xcode behaviors. It should also support defining Xcode implicitness but implemented in such a way that encourages developers to opt for the explicit approach. Supporting Xcode implicitness and intricacies facilitates the adoption of Tuist, after which teams can take some time to get rid of the implicitness.
The definition of dependencies is a good example of that. While developers can define dependencies through build settings and phases, Tuist provides a beautiful API that encourages its adoption.
Designing the API to be explicit allows Tuist to run some checks on the projects that otherwise wouldn’t be possible. Moreover, it enables features like “tuist graph”, which exports a representation of the dependency graph, or “tuist cache”, which caches all the frameworks as
We should treat each request to port features from Xcode as an opportunity to simplify concepts with simple and explicit APIs.
One of the main challenges when scaling Xcode projects comes from the fact that Xcode exposes a lot of complexity to the users. Due to that, teams have a high bus factor and only a few people in the team understand the project and the errors that the build system throws. That’s a bad situation to be in because the team relies on a few people.
Xcode is a great tool, but so many years of improvements, new platforms, and programming languages, are reflected on their surface, which struggled to remain simple.
Tuist should take the opportunity to keep things simple because working on simple things is fun and motivates us. No one wants to spend time trying to debug an error that happens at the very end of the compilation process, or understanding why they are not able to run the app on their devices. Xcode delegates the tasks to its underlying build system and in some cases it does a very poor job translating errors into actionable items. Have you ever got a “framework X not found” error and you didn’t know what to do? Imagine if we got a list of potential root causes for the bug.
The generation of Xcode projects is a mean and not an end in itself. Our goal is not to provide developers with another language to define their projects but rather, help them cope with the challenges they face when they scale up their projects. That means we might not provide APIs for all the features supported by Xcode, and that’s fine. If we did, we’d be re-writing Xcode projects in a new language, and that’s a task that falls into Apple’s domain.
That means we sometimes have to say no to bringing ideas from Xcode that complicate scaling. For instance, if dealing with code signing at scale is something that Xcode doesn’t do well, let’s take the opportunity to re-think how code signing is done.
We might feel tempted to follow what everyone is doing, even if that means keeping on with the inconveniences that everyone continues to complain about. Let’s not do that. Let’s imagine how day-to-day tasks would be handled by Tuist. How do I imagine archiving my app? How would I love code signing to be? What processes can I help streamline with Tuist? If we constrain our solutions to what we have seen in other tools, we might end up replicating the same inconveniences and issues that make scaling up Xcode projects a challenge. We’ll often see users opening tickets with solutions rather than problems. Whenever that happens, use “why” questions to understand what are the user’s motivations or needs. For example, a request from a user could be: Add support for linking libraries build phases. That’s a solution to the need the user has to define dependencies between targets. Once we have identified that, we can think if Xcode’s approach to defining dependencies is good at scale, and if it’s not, we can discuss with the user better approaches.
We, developers, have an inherent temptation to disregard that errors can happen. As a result, we design and test software only considering the ideal scenario.
Swift, its type system, and a well-architected code might help prevent some errors, but not all of them because some are out of our control. We can’t assume the user will always have an internet connection, or that the system commands will return successfully. The environments in which Tuist runs are not sandboxes that we control, and hence we need to make an effort to understand how they might change and impact Tuist.
Poorly handled errors result in bad user experience, and users might lose trust in the project. We want users to enjoy every single piece of Tuist, even the way we present errors to them.
We should put ourselves in the shoes of users and imagine what we’d expect the error to tell us. If the programming language is the communication channel through which errors propagate, and the users are the destination of the errors, they should be written in the same language that the target (users) speak. They should include enough information to know what happened and hide the information that is not relevant. Also, they should be actionable by telling users what steps they can take to recover from them.
And last but not least, our test cases should contemplate failing scenarios. Not only they ensure that we are handling errors as we are supposed to, but prevent future developers from breaking that logic.