package config import ( "errors" "fmt" "os" "path/filepath" "sort" "strings" "github.com/aritmolab/thothii/tools/tht/internal/project" ) // Resolve selects one installation descriptor without searching arbitrary descendants. // Explicit paths and THOTHII_INSTALLATION are authoritative; automatic discovery considers // only the current directory, its parents, and immediate deploy/* descriptors. func Resolve(explicit string, environment func(string) string, workingDirectory string) (string, error) { if explicit != "" { return validateSelectedPath(explicit, "--installation") } if environment != nil { if value := strings.TrimSpace(environment("THOTHII_INSTALLATION")); value != "" { return validateSelectedPath(value, "THOTHII_INSTALLATION") } } friendlyRoot, err := filepath.Abs(workingDirectory) if err != nil { return "", errors.New("installation discovery could not resolve the current directory") } info, err := os.Stat(friendlyRoot) if err != nil || !info.IsDir() { return "", errors.New("installation discovery could not read the current directory") } root, err := project.Discover(friendlyRoot) if err != nil { return "", fmt.Errorf("installation descriptor not found from %s; run tht setup or use --installation /%s", filepath.Clean(friendlyRoot), installationFileName) } start, err := filepath.EvalSymlinks(friendlyRoot) if err != nil { return "", errors.New("installation discovery could not resolve the current directory") } for directory := filepath.Clean(start); ; directory = filepath.Dir(directory) { candidates := make(map[string]string) for _, path := range discoveryCandidates(directory) { if hasArtifactsComponent(path) { continue } info, statErr := os.Stat(path) if statErr != nil || !info.Mode().IsRegular() { continue } if _, loadErr := Load(path); loadErr != nil { continue } canonical := canonicalPath(path) candidates[canonical] = path } paths := make([]string, 0, len(candidates)) for _, path := range candidates { paths = append(paths, path) } sort.Strings(paths) if len(paths) == 1 { return paths[0], nil } if len(paths) > 1 { return "", fmt.Errorf("multiple valid installation descriptors found: %s; use --installation /%s", formatCandidates(paths, root.Path), installationFileName) } if directory == root.Path { break } parent := filepath.Dir(directory) if parent == directory { break } } return "", fmt.Errorf("installation descriptor not found from %s; run tht setup or use --installation /%s", filepath.Clean(friendlyRoot), installationFileName) } func formatCandidates(paths []string, root string) string { formatted := make([]string, 0, len(paths)) for _, path := range paths { id := filepath.Base(filepath.Dir(path)) relative, err := filepath.Rel(filepath.Join(root, "deploy"), path) if err == nil { parts := strings.Split(filepath.Clean(relative), string(filepath.Separator)) if len(parts) == 2 && parts[1] == installationFileName { id = parts[0] } } formatted = append(formatted, fmt.Sprintf("%s: %s", id, path)) } return strings.Join(formatted, ", ") } func validateSelectedPath(path, source string) (string, error) { if !filepath.IsAbs(path) { return "", fmt.Errorf("%s must be an absolute path", source) } path = filepath.Clean(path) if filepath.Base(path) != installationFileName { return "", fmt.Errorf("%s must name %s", source, installationFileName) } if _, err := Load(path); err != nil { return "", fmt.Errorf("%s is unavailable: %w", source, err) } return path, nil } func discoveryCandidates(directory string) []string { candidates := []string{filepath.Join(directory, installationFileName)} deploy := filepath.Join(directory, "deploy") entries, err := os.ReadDir(deploy) if err != nil { return candidates } for _, entry := range entries { if entry.IsDir() { candidates = append(candidates, filepath.Join(deploy, entry.Name(), installationFileName)) } } sort.Strings(candidates) return candidates } func canonicalPath(path string) string { if resolved, err := filepath.EvalSymlinks(path); err == nil { return filepath.Clean(resolved) } return filepath.Clean(path) } func hasArtifactsComponent(path string) bool { for _, component := range strings.Split(filepath.Clean(path), string(filepath.Separator)) { if component == ".artifacts" { return true } } return false }