Skip to content
xcross
Documentation menu

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.

Declarative

One file, canonical YAML, diff-friendly. No per-command flags to remember.

Opt-in

No file means nothing changes. A missing configuration is never an error.

Validated

Absolute paths, real executables, closed key set. Typos fail at edit time, not build time.

Inherited

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.

ModeWhenBehavior
LegacyNo config file foundTools resolve from PATH, environment is inherited verbatim. Nothing changes from a stock install.
ConfiguredA config file is found and parsesRoots, 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.

  1. $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.
  2. <config dir>/config.yaml - the preferred default name.
  3. <config dir>/config.yml - the fallback spelling.

The config directory follows host convention:

HostConfig 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

xcross config *
 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:

TabWhat it edits
RootsDarwin SDK, Flutter SDK, xcross launcher, JAVA_HOME, Konan data
ToolchainsSwift bin directory, LLVM bin directories
ToolsExplicit executable overrides by tool name
EnvironmentAllowlisted environment values, including PATH
SetupThe provisioning script path or URL
CommandsTop-level commands to hide entirely
KeyAction
Tab Switch tab
Move selection
EnterEdit the selected row, or run the selected action
DeleteRemove the selected entry, with confirmation
s v r qSave, 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.

KeyMeaning
darwinSdkDarwin SDK install bundle override
flutterSdkFlutter SDK root used for resolution and FLUTTER_ROOT
xcrossThe xcross launcher itself, used when xcross re-invokes itself or an IDE spawns it
javaHomeExported as JAVA_HOME to children
konanDataKotlin/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.

No control characters

NUL, \n and \r are rejected in every value and in every expansion result.

Absolute only

Path-like values are checked against the host's path semantics, not the parser's.

Real executables

Tool overrides must exist, be regular files, and carry the executable bit or a Windows extension.

Always checked

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 PATH prepended - plus JAVA_HOME, KONAN_DATA_DIR, and XCROSS_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

MessageCause
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 pathRelative paths are never accepted.
Tool <name> is not executableThe override exists but lacks the executable bit or a Windows executable extension.
Environment variable <NAME> is not allowlistedOnly the listed variables may be exported.
Environment variable <NAME> is not definedA $VAR / %VAR% reference could not be expanded.
Environment expansion contains a cycleTwo values expand into each other.
Interactive configuration requires a TTY.Use xcross config show or validate in scripts.

Next steps

ESC