... because "just works" beats "highly configurable" every single time.
Sprout is a symlink and source dependency manager that just works. It's boring in the best possible way.
All I wanted was to organize my dotfiles and tools. Then I blinked, and
suddenly I had a new Rust CLI and very strong opinions about symlinks. Could've
gone outside, touch grass. Instead I implemented ln -s, but in fancy and with
unit tests and colors.
You might be wondering: Why build yet another symlink tracker and source-based dependency manager? Why reinvent the wheel, the wrench, and the garage it lives in? Well...
- Ansible wants to SSH into your toaster to symlink a config.
- Chezmoi dreams of templating your entire life based on your hostname and the moon phase.
- Homebrew gives up if you want a specific Git commit or gasp a local build.
- Nix manages everything. Including your free time.
- GNU Make can technically do this. But now you're writing Makefiles for your dotfiles.
Sprout is simpler! It stays out of your way and does exactly what you ask: track configs, fetch sources, and builds dependencies. All versioned. All in one place. All in plain sight. No templating languages. No Turing-complete DSLs. No server orchestration cosplay just to install Neovim.
Just you, your tools, and your ${HOME} ... sprouted by hand.
- Move your config files into
/sprout/symlinks sprout symlinks add [--recursive]creates a symlink back to$HOMEsprout symlinks status [--all]shows modifications, deletions, and optionally up-to-date filessprout symlinks restorerepairs any missing or broken symlinkssprout symlinks rehashrecalculates symlink hashes after manual changessprout symlinks undo <path>removes symlinks and copies files back to original location- Respects both
.gitignoreand.sproutignore
- Declare Git repos or HTTP downloads (tarballs, zip files) in
manifest.sprout sprout modules fetch [package]pulls and unpacks dependencies- Embed shell commands and environment setup directly in
manifest.sprout sprout modules build [package] [--dry-run]runs it in context (or just prints a ready‑to‑run script)sprout modules install [package]fetches and builds in one stepsprout modules status [--expand] [--all]shows module status with build information and dependenciessprout modules hash [-i]computes and displays/updates module hashessprout modules clean [--dry-run]removes unused cache/source directories- Versioned directories and optional SHA256 checks for archives
- Declare environment variables (PATH, LD_LIBRARY_PATH, etc.) directly in
manifest.sprout - Create named environment sets to group dependencies for different contexts
sprout env edit [environment]interactively edit environment (toggle modules)sprout env list [environment]list environment sets and their modulessprout env generate [environment]generate environment export statements for a specific set
- Initialize a new sprout directory with example modules (defaults to
/sprout)
sprout init --empty [path]- Add the following content to your manifest:
module cmake {
depends_on = []
exports = {
PATH = "/bin"
}
fetch {
http = {
url = https://github.com/Kitware/CMake/releases/download/v4.0.3/cmake-4.0.3-linux-x86_64.tar.gz
}
}
build {
ln -sf ${SOURCE_PATH}/cmake-4.0.3-linux-x86_64/bin ${DIST_PATH}
}
}
module gcc {
depends_on = []
exports = {
PATH = "/bin"
}
fetch {
http = {
url = https://mirrors.ibiblio.org/gnu/gcc/gcc-15.1.0/gcc-15.1.0.tar.xz
}
}
build {
cd gcc-15.1.0
./contrib/download_prerequisites
mkdir -p build
cd build
../configure --disable-multilib --enable-languages=c,c++ --prefix=${DIST_PATH}
make -j8
make install
}
}
environments {
default = [cmake, gcc]
}
- Build and Install Modules (will format the manifest and add sha256sum):
sprout modules install cmake
sprout modules install gcc- Activate the Environment (e.g., add to your
.zshrc):
eval "$(sprout env generate)"sprout statusshows complete status (modules, symlinks, and git)sprout commit [-m "message"]commits all changes to gitsprout pushpushes changes to remote git repositorysprout edit [path]edits manifest.sprout with $EDITOR and validates syntaxsprout format [-i] [path]verifies and reformats manifest.sprout
/sprout
├── symlinks/ # Tracked symlinks
│ └── .zshrc
├── sprout.lock # Lockfile (portable hashes)
├── dist/ # Build artifacts (install output)
│ ├── neovim/
│ ├── ...
│ └── ripgrep/
├── sources/
│ ├── git/ # Git repos (as submodules)
│ └── http/ # Extracted HTTP downloads (tarballs, zip files)
├── cache/
│ └── http/ # Cached downloads (.tar.gz, .zip, etc.)
├── manifest.sprout # Metadata manifest (alphabetically sorted)
├── .gitignore # Sensible defaults
├── .git # Your own Git repo (optional)
Dependencies in manifest.sprout can declare environment variables they need:
Note: Comments in .sprout files start with #. However, Sprout may
reformat the manifest when adding/removing modules, which will remove comments.
Keep important documentation in a separate README or inline in build scripts.
See the example manifest.
When you run sprout env, it generates shell export statements.
Sprout includes a Tree-sitter grammar for syntax highlighting .sprout files.
cd tree-sitter-sprout
tree-sitter generate
tree-sitter build
# Copy queries to Neovim config
mkdir -p ~/.config/nvim/queries/sprout
cp queries/*.scm ~/.config/nvim/queries/sprout/
# Add to your Neovim config (init.lua)
vim.filetype.add({
extension = { sprout = "sprout" },
})
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.sprout = {
install_info = {
url = "~/git/github.com/patwie/sprout/tree-sitter-sprout",
files = {"src/parser.c"},
},
filetype = "sprout",
}Note: Neovim loads queries from ~/.config/nvim/queries/sprout/ first,
then from nvim-treesitter's cache. After updating the grammar, copy the updated
queries to both locations and restart Neovim.
# Add to ~/.config/tree-sitter/config.json
{
"parser-directories": [
"/path/to/sprout/tree-sitter-sprout"
],
"language-associations": {
"sprout": "sprout"
}
}
# Test highlighting
tree-sitter highlight file.sprout