Summary
This RFC proposes adding support for a tuist.toml configuration file as an alternative to Tuist.swift. The primary motivation is enabling a practical experience on Linux, where Swift may not be installed and Tuist.swift cannot be compiled. Today, Linux users must pass --full-handle (and potentially --server-url or url) to every command invocation. A static configuration file removes that friction and unlocks a much larger set of server-interaction commands on the platform.
Motivation
With PR #9291, Tuist gained Linux support for auth and cache config commands. The natural next step is to bring more server-interaction commands to Linux — build list, build show, bundle list, project list, organization list, and many others. These commands only need a server URL, a project handle, and authentication; they don’t require Xcode or the Swift toolchain.
The blocker is configuration. On macOS, Tuist.swift stores the fullHandle and url so users don’t have to repeat them. On Linux, Tuist.swift can’t be compiled without Swift, so these values must be passed as flags on every invocation:
# Today on Linux — every command needs the handle
tuist cache config --full-handle tuist/tuist
tuist build list --full-handle tuist/tuist
tuist project show --full-handle tuist/tuist
This is tedious and error-prone. A static configuration file that doesn’t require compilation solves the problem:
# With tuist.toml in the repository root
tuist cache config
tuist build list
tuist project show
Beyond Linux, tuist.toml can also benefit macOS environments as parsing the tuist.toml file will always be faster than compiling Tuist.swift.
Gradle plugin: a single source of truth
Supporting Android engineers is the primary reason we added Linux support. The Tuist Gradle plugin already integrates Gradle builds with Tuist’s remote build cache. Today, the plugin requires the fullHandle to be configured in settings.gradle.kts:
tuist {
fullHandle = "myorg/android-app"
buildCache {
enabled = true
push = true
}
}
This means Android projects that also use the Tuist CLI (e.g., for tuist build list or tuist auth) must declare the project handle in two places — the Gradle DSL and as a CLI flag. If tuist.toml exists, the Gradle plugin could read fullHandle and url from it when those values are not explicitly set in the Gradle DSL, making tuist.toml the single source of truth for Tuist server configuration across both tools.
The flow would be:
- Gradle DSL values — highest priority (explicit plugin configuration)
tuist.toml— fallback when Gradle DSL values are omitted- Defaults — lowest priority
This lets teams write the handle once in tuist.toml and have both the Gradle plugin and the CLI pick it up:
android-project/
tuist.toml # project handle defined once here
settings.gradle.kts # plugin reads from tuist.toml, no need to repeat fullHandle
app/
...
Proposed solution
The file
A tuist.toml file placed at the project root (the directory containing .git or Tuist/):
full-handle = "tuist/tuist"
url = "https://tuist.dev" # Optional, defaults to https://tuist.dev
Scope
tuist.toml is intentionally limited to server-interaction configuration — the fields needed to identify a project to the Tuist server. It is not a replacement for Tuist.swift. Generation options, plugins, cache profiles, Swift version constraints, and other project-level settings remain exclusively in Tuist.swift.
The supported fields are:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
full-handle |
String | No | — | Project handle, e.g. "tuist/tuist" |
url |
String | No | "https://tuist.dev" |
Tuist server URL |
Namespace design
With only two fields, we propose placing them as top-level keys — no table needed. This is the same pattern Fly.io uses in fly.toml, where the most important identifier (app) is a root-level key. The file is already scoped to Tuist by its name, so a wrapping table adds nesting without adding clarity.
If the file grows to cover more concerns in the future, tables can be introduced alongside existing top-level keys without breaking anything — TOML supports this cleanly.
Alternatives we considered:
[server]table — groups server-related fields ([server]/full-handle,url). Clear about where the config points to, but “server” is an infrastructure detail users shouldn’t need to think about. Also adds a level of nesting that feels unnecessary for two fields.[project]table — groups fields under the thing being identified ([project]/full-handle,url). More user-facing, but could be confused with the Tuist concept of a “project” (e.g.Project.swift).
Naming consideration: full-handle vs project
This RFC proposes full-handle to stay consistent with the existing naming in Tuist.swift (fullHandle) and the Gradle plugin (fullHandle). However, it’s worth considering whether project would be a better key name.
The value "tuist/tuist" is a project identifier — analogous to a GitHub repository slug like github.com/tuist/tuist. The name full-handle describes the format of the value (it’s a handle, and it’s the full one), while project describes what it identifies. Other developer tools lean toward the latter:
- Fly.io uses
appinfly.toml— the thing being identified, not the format - Sentry uses
project(andorg) in.sentryclircand environment variables (SENTRY_PROJECT) - Vercel uses
projectIdinvercel.json
With project, the TOML would read:
project = "tuist/tuist"
url = "https://tuist.dev"
If the community agrees that project is the better name, adopting it in tuist.toml would be the natural starting point — but for full consistency, we should eventually rename fullHandle to project across Tuist.swift and the Gradle plugin as well (with a deprecation period for the old name). This is a broader change and doesn’t need to happen in the first iteration, but tuist.toml gives us the opportunity to start with the right name from day one if we choose to.
Loading precedence
When resolving configuration, the loader follows this order:
- Command-line flags (
--full-handle,--server-url) — highest priority Tuist.swift— if present and Swift is availabletuist.toml— if present- Defaults — lowest priority
If both Tuist.swift and tuist.toml exist, Tuist.swift wins. This means teams can adopt tuist.toml for Linux/CI usage alongside their existing Tuist.swift without any conflict. The TOML file acts as a fallback for environments where Swift compilation isn’t available.
Location
tuist.toml is discovered by traversing up from the current directory and looking for the first match of any of these: tuist.toml, Tuist.swift, or a Tuist/ directory. Alternatively, the traversal stops at a .git directory. This preserves the existing root-resolution behavior while naturally picking up tuist.toml in projects that don’t have a Tuist.swift or Tuist/ directory at all (e.g., Android-only projects).
Example workflows
Linux CI pipeline pulling build analytics:
# tuist.toml at repo root
full-handle = "myorg/ios-app"
tuist auth login
tuist build list --branch main --json
Android project with Gradle plugin and CLI:
# tuist.toml at repo root
full-handle = "myorg/android-app"
// settings.gradle.kts — no fullHandle needed, read from tuist.toml
plugins {
id("dev.tuist") version "0.1.0"
}
tuist {
buildCache {
enabled = true
push = true
}
}
# CLI commands also pick up the handle from tuist.toml
tuist auth login
tuist build list --json
Known limitation: iOS projects using Linux for server commands
For iOS projects that already have a Tuist.swift, teams wanting to run server commands from Linux (e.g., querying build analytics from a Linux CI node) would still need to either duplicate the fullHandle into a tuist.toml or pass --full-handle explicitly. The first iteration does not attempt to solve this — this work is primarily aimed at Android support, where Tuist.swift doesn’t exist in the first place. Down the line, we could consider having the CLI on Linux read fullHandle and url directly from Tuist.swift as a static text file (these fields are simple string literals), generate tuist.toml automatically from Tuist.swift during a macOS build step, or add full support for compiling the Tuist.swift manifest on Linux — though that would require users to ensure Swift is installed on the system.
Alternatives considered
Require --full-handle everywhere on Linux
This is the status quo. It works but creates friction, especially in CI scripts that run multiple Tuist commands. Every command invocation must carry the handle, and forgetting it produces an unhelpful error.
Environment variables
A TUIST_FULL_HANDLE environment variable could serve a similar purpose. However, environment variables are invisible in the repository, not version-controlled, and easy to misconfigure across machines. They also don’t compose well — if Tuist adds more server-side features requiring configuration, the number of required environment variables grows. A configuration file scales better.
That said, environment variables could complement tuist.toml as an additional override layer in the future if needed.
Support the full Tuist.swift surface in TOML
We considered making tuist.toml a complete alternative to Tuist.swift, supporting generation options, plugins, cache profiles, and everything else. This was rejected because:
- The Swift manifest is expressive (enums, computed properties, conditionals) in ways that TOML cannot replicate.
- Maintaining feature parity across two formats would be an ongoing burden.
- The use case is specifically server-interaction commands on platforms without Swift. Those commands don’t need generation options.
Parse settings.gradle.kts for Gradle projects
Instead of introducing a new file, the CLI could extract fullHandle directly from the Gradle plugin configuration in settings.gradle.kts. This would avoid adding another config file to Gradle-based projects. However, settings.gradle.kts is executable Kotlin code, not a static config format. Values can be set from environment variables, function calls, string interpolation, or conditionals — there is no way to reliably extract them without evaluating the Kotlin, which requires a JVM and the Gradle runtime. A regex-based approach would work for trivial cases but silently produce wrong results for anything dynamic, which is a worse experience than not supporting it at all. A static file like tuist.toml is trivially parseable by any tool regardless of runtime.
JSON or YAML
TOML was preferred over JSON (no comments, verbose) and YAML (ambiguous parsing, implicit typing). TOML’s explicit typing and comment support make it the better fit for a configuration file that developers will hand-edit.
Implementation hints
- Add a TOML parsing dependency (e.g., swift-toml or similar).
- Extend
ConfigLoaderto check fortuist.tomlwhenTuist.swiftis not found (or not compilable). - On Linux, skip the
Tuist.swiftcompilation path entirely and go straight totuist.tomlresolution. - The
TuistCore.Tuistmodel already supports all needed fields — the TOML loader just needs to produce aTuistvalue with the server fields populated and everything else set to defaults. - Gating of commands behind
#if os(macOS)can be progressively relaxed astuist.tomlprovides the configuration those commands need. - The Gradle plugin (
TuistPlugin.kt) can be updated to look fortuist.tomlrelative tosettings.gradle.ktswhenfullHandleis not set in the Gradle DSL. TOML parsing libraries are readily available for Kotlin/JVM (e.g., tomlj).