feat/#28 uv support (#62)

* follow proper consistent naming in pypi packagemanager

* follow proper consistent naming in pypi packagemanager - 2

* feat: add specialized command parsers to handle pip and uv command formats

* add uv support & modify extractor to be more robust

* add uv alias

* refactor var name & add error handling

* update readme & add support for `uv pip sync` cmd
This commit is contained in:
Sahil Bansal
2025-08-06 21:10:51 +05:30
committed by GitHub
parent 2cbb24b3b1
commit fd7d83704f
14 changed files with 478 additions and 197 deletions
+80
View File
@@ -0,0 +1,80 @@
package extractor
import (
"context"
"fmt"
"os"
"path/filepath"
"regexp"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/bunlock"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock"
"github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements"
"github.com/google/osv-scalibr/extractor/filesystem/language/python/uvlock"
"github.com/google/osv-scalibr/fs"
)
func getExtractorForFile(filename string) (filesystem.Extractor, error) {
filename = filepath.Base(filename)
// Regex for requirements files (match requirements.txt and requirements-{word}.txt)
reqPattern := regexp.MustCompile(`^requirements(?:-\w+)?\.txt$`)
switch {
case filename == "package-lock.json":
return packagelockjson.NewDefault(), nil
case filename == "pnpm-lock.yaml":
return pnpmlock.New(), nil
case filename == "bun.lock":
return bunlock.New(), nil
case reqPattern.MatchString(filename):
return requirements.NewDefault(), nil
case filename == "uv.lock":
return uvlock.New(), nil
default:
return nil, fmt.Errorf("unsupported lockfile type: %s", filename)
}
}
func parseLockfile(lockfilePath, scanDir string, ecosystem packagev1.Ecosystem) ([]*packagev1.PackageVersion, error) {
extractor, err := getExtractorForFile(lockfilePath)
if err != nil {
return nil, err
}
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %w", err)
}
defer file.Close()
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
inventory, err := extractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: ecosystem,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
}
+2
View File
@@ -26,6 +26,7 @@ const (
Pnpm PackageManagerName = "pnpm"
Pip PackageManagerName = "pip"
Bun PackageManagerName = "bun"
Uv PackageManagerName = "uv"
)
type ExtractorManager struct {
@@ -39,6 +40,7 @@ func NewExtractorManager() *ExtractorManager {
Pnpm: &PnpmExtractor{},
Pip: &PipExtractor{},
Bun: &BunExtractor{},
Uv: &UvExtractor{},
},
}
}
+3 -123
View File
@@ -1,16 +1,7 @@
package extractor
import (
"context"
"fmt"
"os"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/bunlock"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock"
"github.com/google/osv-scalibr/fs"
)
// NpmExtractor handles package-lock.json files
@@ -29,44 +20,7 @@ func (n *NpmExtractor) GetPackageManager() PackageManagerName {
}
func (n *NpmExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parseNpmPackageLockFile(lockfilePath, scanDir)
}
func parseNpmPackageLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
packagelockExtractor := packagelockjson.NewDefault()
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %w", err)
}
defer file.Close()
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
inventory, err := packagelockExtractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
return parseLockfile(lockfilePath, scanDir, n.GetEcosystem())
}
// PnpmExtractor handles pnpm-lock.yaml files
@@ -85,44 +39,7 @@ func (p *PnpmExtractor) GetPackageManager() PackageManagerName {
}
func (p *PnpmExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parsePnpmLockFile(lockfilePath, scanDir)
}
func parsePnpmLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
pnpmLockExtractor := pnpmlock.New()
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %w", err)
}
defer file.Close()
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
inventory, err := pnpmLockExtractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
return parseLockfile(lockfilePath, scanDir, p.GetEcosystem())
}
type BunExtractor struct{}
@@ -140,42 +57,5 @@ func (n *BunExtractor) GetPackageManager() PackageManagerName {
}
func (n *BunExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parseBunPackageLockFile(lockfilePath, scanDir)
}
func parseBunPackageLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
bunlockExtractor := bunlock.New()
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %w", err)
}
defer file.Close()
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
inventory, err := bunlockExtractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
return parseLockfile(lockfilePath, scanDir, n.GetEcosystem())
}
+14 -39
View File
@@ -1,14 +1,7 @@
package extractor
import (
"context"
"fmt"
"os"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements"
"github.com/google/osv-scalibr/fs"
)
// PipExtractor handles requirements.txt files
@@ -27,42 +20,24 @@ func (p *PipExtractor) GetPackageManager() PackageManagerName {
}
func (n *PipExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parseRequirementsTxtFile(lockfilePath, scanDir)
return parseLockfile(lockfilePath, scanDir, n.GetEcosystem())
}
func parseRequirementsTxtFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
requirementsExtractor := requirements.NewDefault()
// UvExtractor handles uv.lock files
type UvExtractor struct{}
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %w", err)
}
defer file.Close()
func (u *UvExtractor) GetSupportedFiles() []string {
return []string{"uv.lock"}
}
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
func (u *UvExtractor) GetEcosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_PYPI
}
inventory, err := requirementsExtractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %w", err)
}
func (u *UvExtractor) GetPackageManager() PackageManagerName {
return Uv
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_PYPI,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
func (u *UvExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parseLockfile(lockfilePath, scanDir, u.GetEcosystem())
}