diff --git a/README.md b/README.md index 4406c74..d021964 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ func main() { | git | .gitmodules | | | github-actions | .github/workflows/*.yml | | | golang | go.mod, Godeps, glide.yaml, Gopkg.toml | Godeps.json, glide.lock, Gopkg.lock, vendor.json, go-resolved-dependencies.json, vendor/manifest | +| guix | manifest.scm | | | hackage | *.cabal | stack.yaml.lock, cabal.config, cabal.project.freeze | | haxelib | haxelib.json | | | hex | mix.exs, gleam.toml | mix.lock, rebar.lock | diff --git a/imports.go b/imports.go index ee7c661..a193a8a 100644 --- a/imports.go +++ b/imports.go @@ -25,6 +25,7 @@ import ( _ "github.com/git-pkgs/manifests/internal/github_actions" _ "github.com/git-pkgs/manifests/internal/gleam" _ "github.com/git-pkgs/manifests/internal/golang" + _ "github.com/git-pkgs/manifests/internal/guix" _ "github.com/git-pkgs/manifests/internal/hackage" _ "github.com/git-pkgs/manifests/internal/haxelib" _ "github.com/git-pkgs/manifests/internal/hex" diff --git a/internal/guix/guix.go b/internal/guix/guix.go new file mode 100644 index 0000000..0477364 --- /dev/null +++ b/internal/guix/guix.go @@ -0,0 +1,92 @@ +package guix + +import ( + "regexp" + "strings" + + "github.com/git-pkgs/manifests/internal/core" +) + +func init() { + core.Register("guix", core.Manifest, &manifestParser{}, + core.AnyMatch( + core.ExactMatch("manifest.scm"), + core.SuffixMatch("-manifest.scm"), + )) +} + +type manifestParser struct{} + +// Match quoted strings inside specifications->manifest or specifications->manifest+. +var specStringRegex = regexp.MustCompile(`"([^"]+)"`) + +func (p *manifestParser) Parse(filename string, content []byte) ([]core.Dependency, error) { + text := string(content) + + // Find the specifications->manifest call + idx := strings.Index(text, "specifications->manifest") + if idx == -1 { + return nil, nil + } + + // Find the opening paren of the list argument + rest := text[idx:] + listStart := strings.Index(rest, "'(") + if listStart == -1 { + listStart = strings.Index(rest, "(list") + if listStart == -1 { + return nil, nil + } + } + + // Extract the balanced parenthesized list + listText := rest[listStart:] + depth := 0 + end := -1 + for i, ch := range listText { + if ch == '(' { + depth++ + } else if ch == ')' { + depth-- + if depth == 0 { + end = i + 1 + break + } + } + } + if end == -1 { + return nil, nil + } + + block := listText[:end] + + // Strip ;; comments + var filtered []string + for _, line := range strings.Split(block, "\n") { + trimmed := strings.TrimSpace(line) + if commentIdx := strings.Index(trimmed, ";;"); commentIdx != -1 { + trimmed = trimmed[:commentIdx] + } + filtered = append(filtered, trimmed) + } + block = strings.Join(filtered, "\n") + + var deps []core.Dependency + for _, match := range specStringRegex.FindAllStringSubmatch(block, -1) { + name := match[1] + // Guix specs can include version: "package@version" + version := "" + if at := strings.Index(name, "@"); at != -1 { + version = name[at+1:] + name = name[:at] + } + deps = append(deps, core.Dependency{ + Name: name, + Version: version, + Scope: core.Runtime, + Direct: true, + }) + } + + return deps, nil +} diff --git a/internal/guix/guix_test.go b/internal/guix/guix_test.go new file mode 100644 index 0000000..47dbd11 --- /dev/null +++ b/internal/guix/guix_test.go @@ -0,0 +1,116 @@ +package guix + +import ( + "os" + "testing" +) + +func TestManifest(t *testing.T) { + content, err := os.ReadFile("../../testdata/guix/manifest.scm") + if err != nil { + t.Fatalf("failed to read fixture: %v", err) + } + + parser := &manifestParser{} + deps, err := parser.Parse("manifest.scm", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(deps) != 15 { + t.Fatalf("expected 15 dependencies, got %d", len(deps)) + } + + depMap := make(map[string]bool) + for _, d := range deps { + depMap[d.Name] = true + if d.Scope != "runtime" { + t.Errorf("%s: scope = %q, want %q", d.Name, d.Scope, "runtime") + } + if !d.Direct { + t.Errorf("%s: expected direct", d.Name) + } + } + + expected := []string{ + "bash", "coreutils", "gcc-toolchain", "git-minimal", "grep", + "gzip", "make", "nss-certs", "pkg-config", "python", + "sed", "tar", "util-linux", "wget", "xz", + } + for _, name := range expected { + if !depMap[name] { + t.Errorf("expected %s dependency", name) + } + } + + // Verify comment was filtered + if depMap["commented-out-package"] { + t.Error("commented-out-package should not be included") + } +} + +func TestManifestWithVersions(t *testing.T) { + content := []byte(`(specifications->manifest + '("python@3.10" + "node@18.0.0" + "go")) +`) + + parser := &manifestParser{} + deps, err := parser.Parse("manifest.scm", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(deps) != 3 { + t.Fatalf("expected 3 dependencies, got %d", len(deps)) + } + + cases := map[string]string{ + "python": "3.10", + "node": "18.0.0", + "go": "", + } + + for _, d := range deps { + want, ok := cases[d.Name] + if !ok { + t.Errorf("unexpected dependency %s", d.Name) + continue + } + if d.Version != want { + t.Errorf("%s: version = %q, want %q", d.Name, d.Version, want) + } + } +} + +func TestManifestNoMatch(t *testing.T) { + // File with no specifications->manifest call returns nil + content := []byte(`(use-modules (gnu packages)) +(packages->manifest (list some-binding)) +`) + + parser := &manifestParser{} + deps, err := parser.Parse("manifest.scm", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if deps != nil { + t.Fatalf("expected nil dependencies for non-specifications manifest, got %d", len(deps)) + } +} + +func TestManifestSuffixMatch(t *testing.T) { + content := []byte(`(specifications->manifest '("git" "make"))`) + + parser := &manifestParser{} + deps, err := parser.Parse("base-manifest.scm", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(deps) != 2 { + t.Fatalf("expected 2 dependencies, got %d", len(deps)) + } +} diff --git a/testdata/guix/manifest.scm b/testdata/guix/manifest.scm new file mode 100644 index 0000000..2839d6a --- /dev/null +++ b/testdata/guix/manifest.scm @@ -0,0 +1,20 @@ +;;; Guix manifest for development environment. + +(specifications->manifest + (list + "bash" + "coreutils" + "gcc-toolchain" + "git-minimal" + "grep" + "gzip" + "make" + ;; "commented-out-package" + "nss-certs" + "pkg-config" + "python" + "sed" + "tar" + "util-linux" + "wget" + "xz"))