Configuration
How xcross config works - the YAML file, its discovery rules, every key, the interactive TUI, and how the configuration reshapes every command at runtime.
Out of the box xcross takes the world as it finds it: whatever is on PATH, wherever the installer put things. That is fine until it isn’t - a locked-down machine, a second LLVM, a Flutter SDK on another drive, a machine where nothing may be installed globally.
xcross config is the answer. One YAML file declares where everything lives, and every xcross command - plus every process it spawns, including IDE-driven runs - is rebuilt around that declaration before a single byte is compiled.
One file, canonical YAML, diff-friendly. No per-command flags to remember.
No file means nothing changes. A missing configuration is never an error.
Absolute paths, real executables, closed key set. Typos fail at edit time, not build time.
Children get the same file through XCROSS_CONFIG, so IDE runs match your terminal.
The two modes
xcross runs in exactly one of two modes, decided by whether a configuration file exists.
| Mode | When | Behavior |
|---|---|---|
| Legacy | No config file found | Tools resolve from PATH, environment is inherited verbatim. Nothing changes from a stock install. |
| Configured | A config file is found and parses | Roots, tool overrides, toolchain directories and allowlisted environment values are overlaid onto process behavior before any command runs. |
Where the file lives
Discovery is deterministic and stops at the first hit.
$XCROSS_CONFIG- if set and non-empty, that exact path is the configuration. It is honored even when the file does not exist yet, so you can point at a path and create it with a save.<config dir>/config.yaml- the preferred default name.<config dir>/config.yml- the fallback spelling.
The config directory follows host convention:
| Host | Config directory |
|---|---|
| Windows | %APPDATA%\xcross (falling back to %LOCALAPPDATA%, then %USERPROFILE%) |
| Linux / macOS | $XDG_CONFIG_HOME/xcross, falling back to ~/.config/xcross |
So the default file is %APPDATA%\xcross\config.yaml on Windows and ~/.config/xcross/config.yaml elsewhere - the same root that holds the Darwin SDK bundle.
NoteWhen a configuration is active, xcross exports XCROSS_CONFIG into every child process it spawns. Sub-invocations - the DAP server, IDE launches, nested xcross calls - inherit the exact same file rather than re-discovering one.
The three commands
xcross config
Opens the interactive editor. Requires a TTY.
xcross config show
Prints the selected path, then the normalized YAML.
xcross config validate
Loads and checks every path. Non-zero exit on failure.
show and validate both fail loudly when no configuration exists, which makes them safe to script.
The interactive editor
Roots │ Toolchains │ Tools │ Environment │ Setup │ Commands
──────────────────────────────────────────────────────────
> darwinSdk: /opt/xcross/sdk
flutterSdk: /opt/flutter
xcross: <unset>
javaHome: /usr/lib/jvm/temurin-21
konanData: <unset>
[Save] [Validate] [Discard] [Quit]
Tab/←/→ tabs ↑/↓ select Enter edit Delete remove s/v/r/q actions
Six tabs, one small ANSI TUI:
| Tab | What it edits |
|---|---|
| Roots | Darwin SDK, Flutter SDK, xcross launcher, JAVA_HOME, Konan data |
| Toolchains | Swift bin directory, LLVM bin directories |
| Tools | Explicit executable overrides by tool name |
| Environment | Allowlisted environment values, including PATH |
| Setup | The provisioning script path or URL |
| Commands | Top-level commands to hide entirely |
| Key | Action |
|---|---|
| Tab ← → | Switch tab |
| ↑ ↓ | Move selection |
| Enter | Edit the selected row, or run the selected action |
| Delete | Remove the selected entry, with confirmation |
| s v r q | Save, Validate, Discard, Quit |
The title shows xcross config * while there are unsaved changes, and quitting dirty asks first. Dirtiness is computed by comparing serialized YAML, so a no-op edit is correctly seen as no change. On a terminal that cannot do full-screen ANSI, the editor degrades to plain line-by-line updates rather than refusing to run.
TipSaving validates first, then writes atomically - new YAML to a temporary file, flushed, renamed over the target. On Windows the previous file is moved aside as a backup and restored if the swap fails. A crash mid-save can never leave a truncated configuration.
The file format
Written output is canonical - fixed section order, sorted maps, JSON-quoted strings - so the file diffs cleanly in version control.
roots:
darwinSdk: "/opt/xcross/sdk"
flutterSdk: "/opt/flutter"
xcross: "/usr/local/bin/xcross"
javaHome: "/usr/lib/jvm/temurin-21"
konanData: "/var/cache/konan"
toolchains:
swift: "/opt/swift/usr/bin"
llvm:
- "/usr/lib/llvm-17/bin"
tools:
flutter: "/opt/flutter/bin/flutter"
xcrun: "/opt/xcross/bin/xcrun"
setup: "https://example.com/setup.sh"
excluded_commands:
- "update"
environment:
PATH:
- "/opt/swift/usr/bin"
JAVA_HOME: "/usr/lib/jvm/temurin-21"
Top-level keys are closed: roots, toolchains, tools, environment, excluded_commands, setup. An unknown key is a hard error naming the JSON-path of the offender, for example Unknown key $.roots.flutter. Typos fail immediately instead of being silently ignored.
roots
Install locations xcross reasons about. Every root must be an absolute path, but is not required to exist yet.
| Key | Meaning |
|---|---|
darwinSdk | Darwin SDK install bundle override |
flutterSdk | Flutter SDK root used for resolution and FLUTTER_ROOT |
xcross | The xcross launcher itself, used when xcross re-invokes itself or an IDE spawns it |
javaHome | Exported as JAVA_HOME to children |
konanData | Kotlin/Native cache root, exported as KONAN_DATA_DIR |
toolchains
swift is a single bin directory; llvm is either one directory or a list, searched in order. Both must be absolute. These become toolchain search directories, so tool lookup prefers them over PATH.
tools
Explicit executable overrides, keyed by tool name. Names are normalized: lowercased, trimmed, and stripped of a trailing .exe, .com, .bat, or .cmd - so Flutter.EXE and flutter are the same key, and declaring both is a duplicate-key error rather than a coin flip.
Validation here is the strictest in the file. A tool path must be absolute, must be a regular file that exists, and must actually be executable: the executable bit on POSIX, a recognized executable extension on Windows.
environment
Only an explicit allowlist may be set:
PATH · CC · CXX · SWIFT_EXEC · SWIFT_EXEC_MANIFEST · JAVA_HOME · FLUTTER_ROOT · KONAN_DATA_DIR · LIBRARY_PATH · C_INCLUDE_PATH · CPLUS_INCLUDE_PATH
Anything else is rejected. PATH is a list of absolute paths and is prepended to the inherited PATH with the host separator, so configured directories win while the rest of your shell stays intact. Every other key is a single non-empty string that replaces the inherited value.
NoteThe allowlist governs what xcross writes into children. It is deliberately separate from what it may read: $HOME, %APPDATA% and friends stay usable as expansion sources without being exportable.
setup
An absolute local path or an http(s) URL to a provisioning script, used by xcross setup. When set, it replaces the built-in host setup entirely. See Setup script for caching, integrity, and refresh behavior.
excluded_commands
Top-level command names to hide from the CLI entirely. They are filtered out while the command runner is built, so an excluded command is absent from --help, from completion, and from dispatch. This is how you ship a locked-down xcross - for example dropping update on a managed image where the binary is owned by the package manager.
Variable expansion
String values are expanded against the host’s native syntax before validation: %VAR% on Windows, $VAR and ${VAR} elsewhere, plus a leading ~ for $HOME on POSIX.
roots:
flutterSdk: "$HOME/sdks/flutter"
tools:
flutter: "~/sdks/flutter/bin/flutter"
Expansion is recursive, so a variable may resolve into another. It is also bounded: a cycle is detected and rejected, and nesting deeper than 32 levels fails rather than looping. An undefined variable is an error naming the variable, never a silently empty string.
Safety rules
The parser is intentionally suspicious, because these values become process arguments and environment entries.
NUL, \n and \r are rejected in every value and in every expansion result.
Path-like values are checked against the host's path semantics, not the parser's.
Tool overrides must exist, be regular files, and carry the executable bit or a Windows extension.
Validation runs on parse, on validate, and before every save. Invalid YAML is never written.
What happens at startup
Before your command runs, xcross initializes a process-global runtime configuration exactly once, and that initialization rewires the world:
xcross <command>
└─ discover configuration
├─ $XCROSS_CONFIG explicit selector wins
├─ <config dir>/config.yaml
└─ <config dir>/config.yml
├─ none found → legacy mode (PATH resolution, inherited env)
└─ found → parse → expand variables → validate
└─ apply overlay
├─ tool overrides + toolchain search directories
├─ child environment (PATH prepended, JAVA_HOME, KONAN_DATA_DIR, XCROSS_CONFIG)
├─ Darwin SDK bundle · Flutter root · Konan cache root
├─ xcross launcher · DAP router · Apple tool shims
└─ command runner filtering (excluded_commands)
Concretely, the overlay:
- registers normalized tool overrides and toolchain directories with the process runner, so every tool lookup consults them first;
- composes the child environment - inherited values, then allowlisted overrides, with
PATHprepended - plusJAVA_HOME,KONAN_DATA_DIR, andXCROSS_CONFIG; - points the Darwin SDK bundle, Flutter resolution, and the Kotlin/Native cache root at the configured roots;
- redirects the xcross launcher, the DAP router, and the Apple tool shims, so IDE-driven runs use the same toolchain as the terminal;
- filters excluded commands out of the CLI.
Configuration is therefore not a per-command flag you must remember. It is a single declaration that every path through xcross, including the ones started by your editor, is built on top of.
Recipes
Each of these is a complete, valid file - drop it in, then run xcross config validate.
Pin a second LLVM without touching PATH
Your distro’s default lld is too old to link iOS, but a newer one sits beside it. Point
xcross at that one only, leaving the rest of the system alone.
toolchains:
llvm:
- "/usr/lib/llvm-19/bin"
Use a Flutter SDK that isn’t installed globally
A checkout in your home directory, not on PATH. Set the root so xcross knows the SDK
layout, and the tool so it invokes the right binary.
roots:
flutterSdk: "$HOME/sdks/flutter"
tools:
flutter: "$HOME/sdks/flutter/bin/flutter"
Keep several Flutter versions side by side
One file per version, selected by an environment variable. Nothing is installed or symlinked, and switching is instant.
# ~/.config/xcross/flutter-3.29.yaml and flutter-3.35.yaml
XCROSS_CONFIG=~/.config/xcross/flutter-3.35.yaml xcross flutter run
Point your shell at one for the whole session:
export XCROSS_CONFIG=~/.config/xcross/flutter-3.35.yaml
Pin the whole toolchain explicitly
Nothing on PATH is trusted: every tool is named outright, so a system upgrade cannot
silently swap a compiler underneath your builds. update and setup are removed because
this host is provisioned elsewhere.
roots:
darwinSdk: "/opt/xcross/xcross-darwin.artifactbundle"
flutterSdk: "/opt/flutter"
xcross: "/usr/local/bin/xcross"
toolchains:
swift: "/opt/swift/usr/bin"
llvm:
- "/usr/lib/llvm-19/bin"
tools:
flutter: "/opt/flutter/bin/flutter"
swift: "/opt/swift/usr/bin/swift"
clang: "/usr/lib/llvm-19/bin/clang"
"clang++": "/usr/lib/llvm-19/bin/clang++"
"llvm-ar": "/usr/lib/llvm-19/bin/llvm-ar"
"ld64.lld": "/usr/lib/llvm-19/bin/ld64.lld"
excluded_commands:
- "update"
- "setup"
TipRun xcross config validate right after provisioning. It fails in a second on a bad
path, instead of thirty minutes into a build.
A managed developer fleet
Hosts provision themselves from a script you control, and the self-updater is removed so the
binary stays owned by your package manager. xcross setup runs the script and nothing else.
setup: "https://tools.example.com/xcross/setup.sh"
excluded_commands:
- "update"
Roll out a new script without running it, then let hosts pick it up on their next setup:
xcross setup --refresh-script
Swift toolchain outside the system prefix
A swiftly-managed or hand-unpacked toolchain. toolchains.swift makes tool lookup prefer
it, environment.PATH makes the child processes it spawns agree, and SWIFT_EXEC pins the
exact compiler binary used for manifest and build invocations.
toolchains:
swift: "$HOME/.swiftly/toolchains/6.0.3/usr/bin"
environment:
PATH:
- "$HOME/.swiftly/toolchains/6.0.3/usr/bin"
SWIFT_EXEC: "$HOME/.swiftly/toolchains/6.0.3/usr/bin/swift-frontend"
Compose Multiplatform on a shared machine
A JDK that isn’t the system default, and a Kotlin/Native cache on a fast or roomy volume
instead of ~/.konan. Both are exported to Gradle and the Kotlin compiler for you.
roots:
javaHome: "/usr/lib/jvm/temurin-21"
konanData: "/mnt/fast/konan"
Windows with a non-default install layout
Native %VAR% expansion, and Windows tool names normalize - .exe is stripped, so
flutter and flutter.exe are the same key.
roots:
flutterSdk: "C:\\src\\flutter"
darwinSdk: "%APPDATA%\\xcross\\swift-sdks\\xcross-darwin.artifactbundle"
tools:
flutter: "C:\\src\\flutter\\bin\\flutter.bat"
clang: "C:\\Program Files\\LLVM\\bin\\clang.exe"
Make an IDE match your terminal exactly
Editors spawn xcross with their own, usually thinner, environment. Setting the launcher root means the DAP server, hot reload, and the Apple tool shims all re-enter the same binary with the same configuration attached - so a run from the Debug button behaves identically to one from your shell.
roots:
xcross: "/usr/local/bin/xcross"
flutterSdk: "$HOME/sdks/flutter"
Try a change without committing to it
XCROSS_CONFIG is honored even when the file does not exist yet, so you can branch your
configuration, edit the copy in the TUI, and throw it away.
cp ~/.config/xcross/config.yaml /tmp/experiment.yaml
XCROSS_CONFIG=/tmp/experiment.yaml xcross config # edit the copy
XCROSS_CONFIG=/tmp/experiment.yaml xcross flutter build # try it
Troubleshooting
| Message | Cause |
|---|---|
No xcross configuration found. | show/validate ran in legacy mode. Create one with xcross config. |
Unknown key $.<path> | A typo or an unsupported key. Top-level keys are closed. |
Root <name> must be an absolute path | Relative paths are never accepted. |
Tool <name> is not executable | The override exists but lacks the executable bit or a Windows executable extension. |
Environment variable <NAME> is not allowlisted | Only the listed variables may be exported. |
Environment variable <NAME> is not defined | A $VAR / %VAR% reference could not be expanded. |
Environment expansion contains a cycle | Two values expand into each other. |
Interactive configuration requires a TTY. | Use xcross config show or validate in scripts. |
Next steps
- CLI reference - every command the configuration can reshape
- Architecture - how the toolchain is assembled
- Troubleshooting - when a build still fails