Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
92 changes: 92 additions & 0 deletions internal/guix/guix.go
Original file line number Diff line number Diff line change
@@ -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
}
116 changes: 116 additions & 0 deletions internal/guix/guix_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
20 changes: 20 additions & 0 deletions testdata/guix/manifest.scm
Original file line number Diff line number Diff line change
@@ -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"))