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
81 changes: 81 additions & 0 deletions internal/conda/conda.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func init() {
core.Register("conda", core.Manifest, &condaEnvParser{}, core.ExactMatch("environment.yml"))
core.Register("conda", core.Manifest, &condaEnvParser{}, core.ExactMatch("environment.yaml"))
core.Register("conda", core.Lockfile, &condaLockParser{}, core.ExactMatch("conda-lock.yml"))
}

// condaEnvParser parses Conda environment.yml files.
Expand Down Expand Up @@ -57,3 +58,83 @@ func parseCondaSpec(spec string) (name, version string) {
}
return name, version
}

// condaLockParser parses conda-lock.yml files.
type condaLockParser struct{}

type condaLockFile struct {
Version int `yaml:"version"`
Package []condaLockPkg `yaml:"package"`
}

type condaLockPkg struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Manager string `yaml:"manager"`
Platform string `yaml:"platform"`
URL string `yaml:"url"`
Hash condaLockHash `yaml:"hash"`
Category string `yaml:"category"`
Optional bool `yaml:"optional"`
}

type condaLockHash struct {
MD5 string `yaml:"md5"`
SHA256 string `yaml:"sha256"`
}

func (p *condaLockParser) Parse(filename string, content []byte) ([]core.Dependency, error) {
var lock condaLockFile
if err := yaml.Unmarshal(content, &lock); err != nil {
return nil, &core.ParseError{Filename: filename, Err: err}
}

var deps []core.Dependency
seen := make(map[string]bool)

for _, pkg := range lock.Package {
// Skip pip packages - they belong to pypi ecosystem
if pkg.Manager == "pip" {
continue
}

// Deduplicate across platforms
if seen[pkg.Name] {
continue
}
seen[pkg.Name] = true

scope := core.Runtime
if pkg.Category == "dev" {
scope = core.Development
}

integrity := ""
if pkg.Hash.SHA256 != "" {
integrity = "sha256-" + pkg.Hash.SHA256
} else if pkg.Hash.MD5 != "" {
integrity = "md5-" + pkg.Hash.MD5
}

// Extract channel URL from package URL
registryURL := ""
if strings.Contains(pkg.URL, "conda.anaconda.org") {
// Extract channel: https://conda.anaconda.org/conda-forge/linux-64/...
parts := strings.Split(pkg.URL, "/")
if len(parts) >= 4 {
registryURL = strings.Join(parts[:4], "/")
}
}

deps = append(deps, core.Dependency{
Name: pkg.Name,
Version: pkg.Version,
Scope: scope,
Integrity: integrity,
Direct: false,
RegistryURL: registryURL,
})
}

return deps, nil
}
61 changes: 61 additions & 0 deletions internal/conda/conda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,64 @@ func TestCondaEnvironmentWithPip(t *testing.T) {
t.Error("expected urllib3 (pip dep) to be excluded")
}
}

func TestCondaLock(t *testing.T) {
content, err := os.ReadFile("../../testdata/conda/conda-lock.yml")
if err != nil {
t.Fatalf("failed to read fixture: %v", err)
}

parser := &condaLockParser{}
deps, err := parser.Parse("conda-lock.yml", content)
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

// 4 conda packages (pip packages are excluded)
if len(deps) != 4 {
t.Fatalf("expected 4 dependencies, got %d", len(deps))
}

depMap := make(map[string]core.Dependency)
for _, d := range deps {
depMap[d.Name] = d
}

// Verify conda packages
expected := []struct {
name string
version string
scope core.Scope
hasIntegrity bool
}{
{"python", "3.11.0", core.Runtime, true},
{"numpy", "1.24.3", core.Runtime, true},
{"pandas", "2.0.1", core.Runtime, true},
{"pytest", "7.3.1", core.Development, true},
}

for _, exp := range expected {
dep, ok := depMap[exp.name]
if !ok {
t.Errorf("expected %s dependency", exp.name)
continue
}
if dep.Version != exp.version {
t.Errorf("%s version = %q, want %q", exp.name, dep.Version, exp.version)
}
if dep.Scope != exp.scope {
t.Errorf("%s scope = %v, want %v", exp.name, dep.Scope, exp.scope)
}
if exp.hasIntegrity && dep.Integrity == "" {
t.Errorf("%s should have integrity hash", exp.name)
}
}

// pip packages should be excluded
if _, ok := depMap["requests"]; ok {
t.Error("expected requests (pip package) to be excluded")
}
if _, ok := depMap["black"]; ok {
t.Error("expected black (pip package) to be excluded")
}
}
90 changes: 90 additions & 0 deletions testdata/conda/conda-lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# This lock file was generated by conda-lock (https://github.com/conda/conda-lock). DO NOT EDIT!
#
# A "lock file" contains a concrete list of package versions (with checksums) to be installed.
#
version: 1
metadata:
content_hash:
linux-64: abc123def456
channels:
- url: https://conda.anaconda.org/conda-forge
used_env_vars: []
- url: https://conda.anaconda.org/pytorch
used_env_vars: []
platforms:
- linux-64
sources:
- environment.yml
package:
- name: python
version: 3.11.0
manager: conda
platform: linux-64
dependencies:
ld_impl_linux-64: '>=2.36.1'
libffi: '>=3.4'
url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.0-h1a5efe5_0.conda
hash:
md5: 1234567890abcdef1234567890abcdef
sha256: abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
category: main
optional: false
- name: numpy
version: 1.24.3
manager: conda
platform: linux-64
dependencies:
python: '>=3.8'
libblas: '>=3.9.0'
url: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.24.3-py311h64a7726_0.conda
hash:
md5: fedcba0987654321fedcba0987654321
sha256: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
category: main
optional: false
- name: pandas
version: 2.0.1
manager: conda
platform: linux-64
dependencies:
python: '>=3.8'
numpy: '>=1.21.0'
url: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.0.1-py311h320fe9a_0.conda
hash:
md5: aabbccdd11223344aabbccdd11223344
sha256: 5566778899aabbcc5566778899aabbcc5566778899aabbcc5566778899aabbcc
category: main
optional: false
- name: pytest
version: 7.3.1
manager: conda
platform: linux-64
dependencies:
python: '>=3.7'
url: https://conda.anaconda.org/conda-forge/noarch/pytest-7.3.1-pyhd8ed1ab_0.conda
hash:
md5: 1111222233334444555566667777888a
sha256: aaaabbbbccccddddeeeeffffaaaabbbbccccddddeeeeffffaaaabbbbccccdddd
category: dev
optional: false
- name: requests
version: 2.31.0
manager: pip
platform: linux-64
dependencies: {}
url: https://files.pythonhosted.org/packages/requests-2.31.0-py3-none-any.whl
hash:
sha256: 942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
category: main
optional: false
- name: black
version: 23.3.0
manager: pip
platform: linux-64
dependencies:
click: '>=8.0.0'
url: https://files.pythonhosted.org/packages/black-23.3.0-py3-none-any.whl
hash:
sha256: 1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940
category: dev
optional: true