The options.json parser expected declarations as []string (NixOS format),
but Home Manager uses [{name, url}] objects. This caused most HM options
to be silently skipped during parsing (27 vs 4880 options).
Changes:
- Parse declarations as json.RawMessage and try both formats
- Handle HM path format <home-manager/modules/...> in normalizeDeclarationPath
- Add /modules/ marker for HM store paths
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
170 lines
4.8 KiB
Go
170 lines
4.8 KiB
Go
package nixos
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// ParseOptions parses the options.json file from nixpkgs.
|
|
// The options.json structure is a map from option name to option definition.
|
|
func ParseOptions(r io.Reader) (map[string]*ParsedOption, error) {
|
|
var raw map[string]json.RawMessage
|
|
if err := json.NewDecoder(r).Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("failed to decode options.json: %w", err)
|
|
}
|
|
|
|
options := make(map[string]*ParsedOption, len(raw))
|
|
for name, data := range raw {
|
|
opt, err := parseOption(name, data)
|
|
if err != nil {
|
|
// Log but don't fail - some options might have unusual formats
|
|
continue
|
|
}
|
|
options[name] = opt
|
|
}
|
|
|
|
return options, nil
|
|
}
|
|
|
|
// ParsedOption represents a parsed NixOS option with all its metadata.
|
|
type ParsedOption struct {
|
|
Name string
|
|
Type string
|
|
Description string
|
|
Default string // JSON-encoded value
|
|
Example string // JSON-encoded value
|
|
ReadOnly bool
|
|
Declarations []string
|
|
}
|
|
|
|
// optionJSON is the internal structure for parsing options.json entries.
|
|
type optionJSON struct {
|
|
Declarations json.RawMessage `json:"declarations"` // Can be []string or []{name, url}
|
|
Default json.RawMessage `json:"default,omitempty"`
|
|
Description interface{} `json:"description"` // Can be string or object
|
|
Example json.RawMessage `json:"example,omitempty"`
|
|
ReadOnly bool `json:"readOnly"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
// parseOption parses a single option entry.
|
|
func parseOption(name string, data json.RawMessage) (*ParsedOption, error) {
|
|
var opt optionJSON
|
|
if err := json.Unmarshal(data, &opt); err != nil {
|
|
return nil, fmt.Errorf("failed to parse option %s: %w", name, err)
|
|
}
|
|
|
|
// Handle description which can be a string or an object with _type: "mdDoc"
|
|
description := extractDescription(opt.Description)
|
|
|
|
// Parse declarations - can be []string (NixOS) or []{name, url} (Home Manager)
|
|
declarations := parseDeclarations(opt.Declarations)
|
|
|
|
return &ParsedOption{
|
|
Name: name,
|
|
Type: opt.Type,
|
|
Description: description,
|
|
Default: string(opt.Default),
|
|
Example: string(opt.Example),
|
|
ReadOnly: opt.ReadOnly,
|
|
Declarations: declarations,
|
|
}, nil
|
|
}
|
|
|
|
// parseDeclarations handles both NixOS format ([]string) and Home Manager format ([]{name, url}).
|
|
func parseDeclarations(raw json.RawMessage) []string {
|
|
if len(raw) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Try []string first (NixOS format)
|
|
var stringDecls []string
|
|
if err := json.Unmarshal(raw, &stringDecls); err == nil {
|
|
result := make([]string, 0, len(stringDecls))
|
|
for _, d := range stringDecls {
|
|
result = append(result, normalizeDeclarationPath(d))
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Try []{name, url} format (Home Manager format)
|
|
var objectDecls []struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
}
|
|
if err := json.Unmarshal(raw, &objectDecls); err == nil {
|
|
result := make([]string, 0, len(objectDecls))
|
|
for _, d := range objectDecls {
|
|
// Use name field, normalize the path
|
|
result = append(result, normalizeDeclarationPath(d.Name))
|
|
}
|
|
return result
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractDescription extracts the description string from various formats.
|
|
func extractDescription(desc interface{}) string {
|
|
switch v := desc.(type) {
|
|
case string:
|
|
return v
|
|
case map[string]interface{}:
|
|
// Handle mdDoc format: {"_type": "mdDoc", "text": "..."}
|
|
if text, ok := v["text"].(string); ok {
|
|
return text
|
|
}
|
|
// Try "description" key
|
|
if text, ok := v["description"].(string); ok {
|
|
return text
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// normalizeDeclarationPath converts a full store path to a relative path.
|
|
// NixOS input: "/nix/store/xxx-source/nixos/modules/services/web-servers/nginx/default.nix"
|
|
// NixOS output: "nixos/modules/services/web-servers/nginx/default.nix"
|
|
// HM input: "<home-manager/modules/programs/git.nix>"
|
|
// HM output: "modules/programs/git.nix"
|
|
func normalizeDeclarationPath(path string) string {
|
|
// Handle Home Manager format: <home-manager/path> or <nixpkgs/path>
|
|
if len(path) > 2 && path[0] == '<' && path[len(path)-1] == '>' {
|
|
inner := path[1 : len(path)-1]
|
|
// Strip the prefix (home-manager/, nixpkgs/, etc.)
|
|
if idx := findSubstring(inner, "/"); idx >= 0 {
|
|
return inner[idx+1:]
|
|
}
|
|
return inner
|
|
}
|
|
|
|
// Look for common prefixes and strip them (NixOS store paths)
|
|
markers := []string{
|
|
"/nixos/",
|
|
"/pkgs/",
|
|
"/lib/",
|
|
"/maintainers/",
|
|
"/modules/", // For home-manager paths
|
|
}
|
|
|
|
for _, marker := range markers {
|
|
if idx := findSubstring(path, marker); idx >= 0 {
|
|
return path[idx+1:] // +1 to skip the leading /
|
|
}
|
|
}
|
|
|
|
// If no marker found, return as-is
|
|
return path
|
|
}
|
|
|
|
// findSubstring returns the index of the first occurrence of substr in s, or -1.
|
|
func findSubstring(s, substr string) int {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|