A declarative system configuration management framework for Arch Linux written in Go.
Note: This is a complete rewrite from the original Ruby implementation. The Go version provides better performance, type safety, and easier distribution as a single binary.
fest allows you to declare your entire system state (packages, services, files, configurations) as Go code, and synchronizes your Arch Linux system to match that declared state. Think of it as a type-safe, compiled alternative to configuration management tools like Ansible or NixOS, specifically tailored for Arch Linux.
- Declarative Configuration: Define what you want, not how to get it
- Package Management: Manages pacman packages (including AUR via embedded yay)
- Multiple Package Managers: Support for Flatpak, npm, Go packages, Ruby gems
- System Configuration: Timezone, locale, keyboard settings
- Systemd Services: Enable/disable system and user services, timers, and sockets
- User Groups: Manage user group memberships
- System Files: Deploy and track system configuration files
- Dotfile Management: GNU Stow integration for user dotfiles
- Broken Symlink Cleanup: Automatically detect and remove broken symlinks
- Two-Phase Execution: Preview changes before applying (diff mode)
- State Tracking: Knows what it installed and can clean up unwanted resources
- Dependency Awareness: Won't remove packages that other wanted packages depend on
Before using this framework, ensure you have:
- Arch Linux system
- Go 1.25+ installed
stowfor dotfile management
Note: This project includes an embedded copy of yay for AUR package management. No separate yay installation is required.
- Create a new Go module for your system configuration:
mkdir -p ~/mysystem
cd ~/mysystem
go mod init mysystem
go get github.com/emad-elsaid/fest- Create your configuration file (e.g.,
main.go):
package main
import "github.com/emad-elsaid/fest"
func init() {
// Declare packages
fest.Package(
"vim",
"git",
"docker",
"firefox",
)
// Enable services
fest.SystemService("docker")
// Configure system
fest.Timedate("America/New_York", true)
fest.Locale("en_US.UTF-8 UTF-8")
}
func main() {
fest.Main()
}- Run commands:
# Preview what would change
go run . diff
# Apply configuration
go run . apply
# Save current system state back to Go files
go run . save-
apply: Synchronize your system to match the declared configuration- Installs missing packages
- Enables/disables services
- Deploys system files
- Removes unwanted resources (with confirmation)
-
diff: Show what would change without making any modifications- Useful for previewing changes before applying
- Safe to run anytime
-
save: Capture current system state as Go code- Generates
.gofiles with function calls that match your system - Useful after manual installations to capture them declaratively
- Generates
// Individual packages
fest.Package("vim", "git", "docker")
// Package groups
fest.PackageGroup("base-devel")fest.Flatpak(
"com.slack.Slack",
"org.mozilla.firefox",
)fest.NpmPackage(
"typescript",
"@vue/cli",
"eslint@8.50.0", // Version pinning
)fest.GoPackage(
"github.com/golangci/golangci-lint/cmd/golangci-lint@latest",
"golang.org/x/tools/cmd/goimports",
)fest.RubyGem(
"bundler",
"rails@7.0.0", // Version pinning
)fest.Timedate("America/New_York", true) // timezone, enable NTPfest.Locale("en_US.UTF-8 UTF-8")fest.Keyboard(
"us", // keymap
"us", // layout
"pc105", // model
"", // variant
"ctrl:nocaps", // options
)// System services
fest.SystemService("docker", "sshd")
fest.SystemTimer("fstrim")
fest.SystemSocket("docker")
// User services
fest.Service("syncthing")
fest.Timer("backup")
fest.Socket("pipewire")fest.Group("docker", "wheel", "audio", "video")Place files in a system/ directory mirroring their target paths:
system/
etc/
hosts
pacman.conf
usr/
local/
bin/
myscript
// Automatically discovers and manages files in system/
fest.SystemFilesDir("system")Place your dotfiles in user/ directory:
user/
.config/
nvim/
init.vim
.bashrc
.vimrc
Dotfiles are automatically managed when using apply or save commands.
Execute custom code before or after resource synchronization:
// Run after docker is installed
fest.After(fest.ResourcePackages, func() {
// Custom setup logic
})
// Run before applying configuration
fest.OnCommand(fest.PhaseBeforeApply, func() {
// Pre-apply checks
})- Configuration Phase: Builds lists of desired state by executing your Go code
- Synchronization Phase: Compares current state with desired state and applies changes
All resource types implement the same interface:
type packageManager interface {
ResourceName() string
Wanted() []string
Match(want, have string) bool
ListInstalled() ([]string, error)
ListExplicit() ([]string, error)
Install(pkgs []string) error
Uninstall(pkgs []string) error
MarkExplicit(pkgs []string) error
GetDependencies() (map[string][]string, error)
SaveAsGo(wanted []string) error
}- Pacman packages: Uses pacman's built-in explicit/dependency tracking
- System files: Maintains state in
~/.local/share/dotfiles/system-files-state.json - Dotfiles: Managed via GNU Stow
- Services: Tracked via systemd's enabled/disabled state
Organize your configuration into multiple files:
mysystem/
main.go # Entry point
packages.go # Package declarations
services.go # Service declarations
system.go # System configuration
Each file can have init() functions that declare resources:
// packages.go
package main
import "github.com/emad-elsaid/fest"
func init() {
fest.Package("vim", "git")
}Use build tags or environment variables for machine-specific config:
// +build workstation
package main
import "github.com/emad-elsaid/fest"
func init() {
fest.Package("docker", "kubectl")
}Add custom dependency checks:
fest.OnCommand(fest.PhaseBeforeApply, func() {
// Custom validation logic
})If you see dependency errors, ensure all required tools are installed:
go run . diff # Will check and attempt to install missing dependenciesSome operations require sudo. The tool will prompt for sudo password when needed.
If the state file becomes corrupted:
rm ~/.local/share/dotfiles/system-files-state.json
go run . apply # Rebuilds state| Feature | fest | Ansible | NixOS |
|---|---|---|---|
| Language | Go | YAML | Nix |
| Type Safety | ✓ | ✗ | ✓ |
| Compilation | ✓ | ✗ | ✓ |
| Arch-Specific | ✓ | ✗ | ✗ |
| Requires New Distro | ✗ | ✗ | ✓ |
| Learning Curve | Low | Medium | High |
Contributions are welcome! This framework is designed to be extended with new package managers and resource types.
To add a new resource manager:
- Implement the
packageManagerinterface - Add it to
allManagers()inmain.go - Create public functions for users to declare resources
If you're migrating from the original Ruby implementation:
- The API is very similar but uses Go syntax instead of Ruby
- Replace
requirestatements withimport - Replace
doblocks withfunc init()functions - The command structure is the same (
apply,save,diff)
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
This project incorporates code from yay which is also licensed under GPL v3.