How to Find Tool Installation Paths with aqua which: A Complete Guide
Use aqua which <tool> to resolve the absolute file system path of any tool managed by aqua, or fall back to system PATH for unmanaged binaries.
The aqua which command is essential for debugging, scripting, and understanding how aqua organizes tools in your filesystem. Based on the aquaproj/aqua source code, this guide explains the complete resolution flow from CLI input to absolute path output.
Understanding the aqua which Resolution Architecture
The command operates through three distinct layers that transform a tool name into an absolute filesystem path.
CLI Layer (pkg/cli/which/command.go)
The entry point in pkg/cli/which/command.go handles argument parsing and initializes the controller. The action function sets up the config.Param structure and invokes InitializeWhichCommandController before delegating to the core logic.
func (i *command) action(ctx context.Context, args *Args) error {
// Sets up param and logger
ctrl := controller.InitializeWhichCommandController(ctx, i.r.Logger.Logger,
param, http.DefaultClient, i.r.Runtime)
which, err := ctrl.Which(ctx, logger, param, args.Command)
// Outputs which.ExePath or which.Package.Version
}
Controller Layer (pkg/controller/which/which.go)
The Which function in pkg/controller/which/which.go implements the search strategy. It scans configuration files in priority order: local aqua.yaml files first, then global configurations, finally falling back to the system PATH.
func (c *Controller) Which(ctx context.Context, logger *slog.Logger,
param *config.Param, exeName string) (*FindResult, error) {
// 1. Search local config files
for _, cfgFilePath := range c.configFinder.Finds(param.CWD, "") {
if fr, _ := c.findExecFile(ctx, logger, param, cfgFilePath, exeName); fr != nil {
return fr, nil
}
}
// 2. Search global config files
// ...
// 3. Fallback to $PATH
if exePath := c.lookPath(c.osenv.Getenv("PATH"), exeName); exePath != "" {
return &FindResult{ExePath: exePath}, nil
}
return nil, ErrCommandIsNotFound
}
Domain Model (pkg/config/package.go)
Once a package is identified, Package.ExePath in pkg/config/package.go constructs the absolute path. It computes the package directory using AbsPkgPath (which resolves to $ROOT/pkgs/<type>/github.com/<owner>/<repo>/<version>) and appends the specific file source.
func (p *Package) ExePath(rootDir string, file *registry.File, rt *runtime.Runtime) (string, error) {
pkgPath, err := p.AbsPkgPath(rootDir, rt)
fileSrc, err := p.fileSrc(file, rt) // Resolves OS/Arch placeholders
return filepath.Join(pkgPath, fileSrc), nil
}
How aqua which Searches for Tools
The resolution follows a strict priority order to ensure deterministic behavior.
Local Configuration Scanning
The controller first scans for aqua.yaml files in the current working directory and parent directories using configFinder.Finds. For each configuration found, it calls findExecFile to check if the requested command matches any package defined in that file.
Global Configuration Fallback
If no local configuration contains the tool, the search continues through global configuration files specified in the AQUA_GLOBAL_CONFIG environment variable or default global paths.
System PATH Resolution
When aqua does not manage the tool, the command falls back to lookPath, which searches the system PATH environment variable. This allows aqua which to function as a universal tool locator, returning /bin/ls for system utilities while returning aqua-managed paths for registered tools.
Building the Executable Path
Once a package match is found, the controller calls getExePath to assemble the final filesystem location.
func (c *Controller) getExePath(findResult *FindResult) (string, error) {
pkg := findResult.Package
file := findResult.File
exePath, err := pkg.ExePath(c.rootDir, file, c.runtime)
if err != nil {
return "", err
}
// Handle registry-defined symlinks
if file.Link != "" {
return filepath.Join(filepath.Dir(exePath), file.Link), nil
}
return exePath, nil
}
The ExePath method handles different package types (GitHub releases, Go modules, Cargo crates) by computing the appropriate subdirectory structure within the aqua root directory (typically ~/.aqua/pkgs/).
Practical Usage Examples
Use aqua which to verify installation locations and versions in your daily workflow.
# Locate a GitHub release tool
$ aqua which gh
/home/you/.aqua/pkgs/github_release/github.com/cli/cli/v2.4.0/gh
# Display the installed version
$ aqua which --version gh
v2.4.0
# Locate a system tool (falls back to PATH)
$ aqua which ls
/bin/ls
Programmatic Access in Go
You can integrate aqua path resolution into your own Go applications using the controller package.
import (
"context"
"log/slog"
"os"
"github.com/aquaproj/aqua/v2/pkg/config"
"github.com/aquaproj/aqua/v2/pkg/controller/which"
)
func main() {
ctx := context.Background()
logger := slog.Default()
// Initialize parameter with current working directory
param := &config.Param{
CWD: "/path/to/project",
// Additional configuration as needed
}
// Initialize controller (simplified; actual initialization requires more dependencies)
ctrl := which.NewController(/* dependencies */)
result, err := ctrl.Which(ctx, logger, param, "gh")
if err != nil {
logger.Error("Tool not found", "error", err)
os.Exit(1)
}
logger.Info("Executable path", "path", result.ExePath)
}
Summary
aqua whichresolves the absolute filesystem path for any tool managed by aqua, falling back to systemPATHfor unmanaged binaries.- The resolution traverses three layers: CLI (
pkg/cli/which/command.go), Controller (pkg/controller/which/which.go), and Domain Model (pkg/config/package.go). - Search priority is: local
aqua.yamlconfigurations → global configurations → systemPATH. - The final path is constructed by
Package.ExePath, which computes the installation directory based on package type (GitHub release, Go module, etc.).
Frequently Asked Questions
What happens if aqua which cannot find the tool?
If the tool is not defined in any local or global aqua.yaml configuration, the command falls back to searching the system PATH using exec.LookPath. If the binary is not found in PATH, the command returns ErrCommandIsNotFound and exits with an error status.
How does aqua which handle versioned binaries?
When a tool is managed by aqua, aqua which returns the path to the specific version installed in the aqua packages directory (typically ~/.aqua/pkgs/<type>/<source>/<version>/<binary>). If you use the --version flag, the command outputs the version string from the package definition instead of the path.
Can I use aqua which in shell scripts?
Yes, aqua which is designed for script integration. It outputs only the absolute path (or version with --version) to stdout, making it ideal for command substitution. For example: GH_BIN=$(aqua which gh) && $GH_BIN repo view. The command exits with a non-zero status if the tool is not found, enabling standard error handling in scripts.
Where does aqua store installed tools?
Aqua stores tools in a structured directory under the aqua root (default ~/.aqua). The path follows the pattern pkgs/<package_type>/<source>/<version>/<file>. For example, a GitHub release package appears at pkgs/github_release/github.com/owner/repo/v1.0.0/binary. The aqua which command computes this path dynamically based on the package definition in your registry.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →