Hi,
Until now we have been using tuist 4.12. Our approach was the following:
- App
--- AppCore // A dynamic framework that embeds shared code and dependencies between App and extensions
----- External dep A (SPM)
----- External dep B (SPM)
----- ...
--- AppCoreUI // A dynamic framework that embeds shared code and dependencies between App and extensions
----- External dep C (SPM)
----- ...
--- Extension // Extension are always embedded inside app
----- AppCore
----- AppCoreUI
Because AppCore
is dynamic we can access all its dependencies from the App or the extension. This resulted in a rather clean graph:
But now we want to migrate to the latest version of tuist. From what I understand we have to be more implicit about the dependencies we declare.
To make it work we did something like this:
- App
--- External dep A (SPM)
--- External dep B (SPM)
--- External dep C (SPM)
--- AppCore // A dynamic framework that embeds shared code and dependencies between App and extensions
----- External dep A (SPM)
----- External dep B (SPM)
----- ...
--- AppCoreUI // A dynamic framework that embeds shared code and dependencies between App and extensions
----- External dep C (SPM)
----- ...
--- Extension // Extension are always embedded inside app
----- AppCore
----- AppCoreUI
----- External dep A (SPM)
----- External dep B (SPM)
----- External dep C (SPM)
We also had to mark all our external dependencies as framework
.
Everything compiles fine. However our graph looks way more messy now:
We are okay with a messy graph if it’s the correct but is it really the correct way to do it ?
Hi @Philippe
First of all, thanks for posting in the community forum. It makes discussions more public to the Internet, which will make it easier for anyone to find them.
Answering to your question. Most recent versions of Tuist don’t require you to be more implicit about the declaration of dependencies. It includes a new command which you can use to detect implicit imports through static code analysis, which are recommended to minimise for the reliability and determinism of the build system and the editor. But I think this is a bit unrelated to the topic you are bringing up.
Static or dynamic, framework or library… There’s no right and wrong answer, and it really depends on your project’s graph. We plan to come up with a flow chart that helps people decide, but since the scenarios that we are coming across are so broad, it’s a bit difficult to do so. I’ll provide some guidelines:
- Static is preferred in release builds (fast launch time), while dynamic is preferred in development (faster compilation). We recommend using dynamic configuration to change between one or the other at generation time. This is recommended over embeddable frameworks, which add some build-time complexity and potentially non-determinism.
- But… you might find constrained by the dependencies that you use (e.g. some of them might come precompiled as static or dynamic) or your own graph (e.g. I need to share code across an app and a extension).
- So within those constraints, try to make as many targets as dynamic in development (i.e. the
.framework
or product in the case of iOS) and .staticLibrary
in release. Note that iOS does not support static frameworks, so you’ll have to use synthesized accessors to decouple the accessing of resources from the underlying product type.
- The above is the ideal, but comes with some burden (i.e., reasoning about your graph), which we can’t prevent you from doing. So what we find most people do is making everything dynamic. It’s not ideal, specially if there’s a large percentage of users in your user base with old iOS versions and devices, where the launch of the app might take longer, but if that’s not a problem for you, then lean on the side of your convenience. If it is, then I’d recommend to think through the graph.
Tuist unfortunately can only take the role of ensuring the linking is done right, decoupling the access of the resources from the underlying product type, and letting you know if some linking might lead to side effects (e.g. duplicated symbols or increased binary size). Whether something should be static or dynamic is something that you’ll have to decide based on the knowledge that you have over your project and your users.
Thanks for the explanation.
Are those assumptions correct ?
Local dependency:
static → if used by only one target (target, extension, framework)
dynamic → if used by multiple targets
External dependency:
static → if used by only one local dependency (target, extension, framework)
dynamic → if used by multiple targets
With an exception for dependency in tests targets. If a dependency is in both a main target and a test target it can be static for production builds.
I’d wouldn’t differentiate between external and internal dependencies. Look at each element as a node in a graph. The node has two properties, product (framework or library), and linking (static or dynamic). Then keep the following things in mind:
- Debug: You should optimise for faster compilation, and that’s associated with dynamic linking.
- Release: You should optimise for fast launch and small binary size, and that’s usually associated with static.
Whether something can be made or not static or dynamic can’t be simplified using the rules that you mentioned. If something that you are doing might lead to duplicated symbols or increased binary size, Tuist will tell you you might be introducing “side effects”. In those cases, I’d recommend to inspect the built products to see if there are unnecessary duplication of binaries across multiple dynamic frameworks.
Let me shed some light with a few scenarios:
Scenario A
That one is perfect. Note that the linking is done from the app, so the symbols of the three dependencies end up being part of the app binary.
Scenario B
The introduction of an extension without changing the linking of the dependencies causes an increase in binary size. The app will work, but you are shipping unnecessary copies of the dependencies in the app bundle.
Scenario C
To solve that issue, you make the target the extension depends on dynamic, and that reduces the binary size, but notice that there are 2 copies of C, one in A, where C is linked from, and also in the app. If the symbols of C inside A are not private, this might lead to the app blowing up at runtime because of duplicated symbols.
To solve this, you’d need to turn C into a dynamic target as well. So you’d end up with:
- A dynamic
- B static
- C dynamic
Note: how the solution the setup we came up with is not “let’s make everything dynamic” because I have an extension. Thanks to an analysis, we could notice that B can be static, and thanks to keep it as static we might be contributing positively to the launch time of the app.
Thanks again for the detailed answer. The graph helps a lot.
To be sure that I’m understanding correctly. In scenario C, it’s not possible to access symbols in Static C
from the App or the Extension “through” Dynamic A
? Is it what we would call a transitive dependency ?
If they are not made private, you’ll be able to access them, for example if Dynamic A
uses them in a public API. Exactly from the eyes of the app or the extension, C is a transitive dependency.
1 Like
Thanks a lot for those answers !
1 Like