Skip to content
116 changes: 116 additions & 0 deletions pkg/deps/npm.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,67 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"

"github.com/apache/skywalking-eyes/internal/logger"
"github.com/apache/skywalking-eyes/pkg/license"
)

// Constants for architecture names to avoid string duplication
const (
archAMD64 = "amd64"
archARM64 = "arm64"
archARM = "arm"
)

// Cross-platform package pattern recognition (for precise matching)
// These patterns work for both scoped (@scope/package-platform-arch) and
// non-scoped (package-platform-arch) npm packages, as the platform/arch
// suffix always appears at the end of the package name.
// Examples:
// - Scoped: @scope/foo-linux-x64
// - Non-scoped: foo-linux-x64
//
// regex: matches package names ending with a specific string (e.g., "-linux-x64")
// os: target operating system (e.g., "linux", "darwin", "windows")
// arch: target CPU architecture (e.g., "x64", "arm64")
var platformPatterns = []struct {
regex *regexp.Regexp
os string
arch string
}{
// Android
{regexp.MustCompile(`-android-arm64$`), "android", archARM64},
{regexp.MustCompile(`-android-arm$`), "android", archARM},
{regexp.MustCompile(`-android-x64$`), "android", "x64"},

// Darwin (macOS)
{regexp.MustCompile(`-darwin-arm64$`), "darwin", archARM64},
{regexp.MustCompile(`-darwin-x64$`), "darwin", "x64"},

// Linux
{regexp.MustCompile(`-linux-arm64-glibc$`), "linux", archARM64},
{regexp.MustCompile(`-linux-arm64-musl$`), "linux", archARM64},
{regexp.MustCompile(`-linux-arm-glibc$`), "linux", archARM},
{regexp.MustCompile(`-linux-arm-musl$`), "linux", archARM},
{regexp.MustCompile(`-linux-x64-glibc$`), "linux", "x64"},
{regexp.MustCompile(`-linux-x64-musl$`), "linux", "x64"},
{regexp.MustCompile(`-linux-x64$`), "linux", "x64"},
{regexp.MustCompile(`-linux-arm64$`), "linux", archARM64},
{regexp.MustCompile(`-linux-arm$`), "linux", archARM},

// Windows
{regexp.MustCompile(`-win32-arm64$`), "windows", archARM64},
{regexp.MustCompile(`-win32-ia32$`), "windows", "ia32"},
{regexp.MustCompile(`-win32-x64$`), "windows", "x64"},

// FreeBSD
{regexp.MustCompile(`-freebsd-x64$`), "freebsd", "x64"},
}

type NpmResolver struct {
Resolver
}
Expand Down Expand Up @@ -87,6 +141,8 @@ func (resolver *NpmResolver) Resolve(pkgFile string, config *ConfigDeps, report
for _, pkg := range pkgs {
if result := resolver.ResolvePackageLicense(pkg.Name, pkg.Path, config); result.LicenseSpdxID != "" {
report.Resolve(result)
} else if result.IsCrossPlatform {
logger.Log.Warnf("Skipping cross-platform package %s (not for current platform %s %s)", pkg.Name, runtime.GOOS, runtime.GOARCH)
} else {
result.LicenseSpdxID = Unknown
report.Skip(result)
Expand Down Expand Up @@ -198,6 +254,13 @@ func (resolver *NpmResolver) ResolvePackageLicense(pkgName, pkgPath string, conf
result := &Result{
Dependency: pkgName,
}

// 在开始解析前检查是否为跨平台包且非当前平台
if !resolver.isForCurrentPlatform(pkgName) {
result.IsCrossPlatform = true
return result
}

// resolve from the package.json file
if err := resolver.ResolvePkgFile(result, pkgPath, config); err != nil {
result.ResolveErrors = append(result.ResolveErrors, err)
Expand Down Expand Up @@ -318,3 +381,56 @@ func (resolver *NpmResolver) ParsePkgFile(pkgFile string) (*Package, error) {
}
return &packageInfo, nil
}

// normalizeArch converts various architecture aliases into Go's canonical naming.
func normalizeArch(arch string) string {
// Convert to lowercase to handle case variations (e.g., "AMD64").
arch = strings.ToLower(arch)
switch arch {
// x86-64 family (64-bit Intel/AMD)
case "x64", "x86_64", "amd64", "x86-64":
return archAMD64
// x86 32-bit family (legacy)
case "ia32", "x86", "386", "i386", "i686":
return "386"
// ARM 64-bit
case "arm64", "aarch64":
return archARM64
// ARM 32-bit
case "arm", "armv7", "armhf", "armv7l", "armel":
return archARM
// Unknown architecture: return as-is (alternatively, could return empty to indicate incompatibility).
default:
return arch
}
}

// analyzePackagePlatform extracts the target OS and architecture from a package name.
func (resolver *NpmResolver) analyzePackagePlatform(pkgName string) (pkgOS, pkgArch string) {
for _, pattern := range platformPatterns {
if pattern.regex.MatchString(pkgName) {
return pattern.os, pattern.arch
}
}
return "", ""
}

// isForCurrentPlatform checks whether the package is intended for the current OS and architecture.
func (resolver *NpmResolver) isForCurrentPlatform(pkgName string) bool {
pkgPlatform, pkgArch := resolver.analyzePackagePlatform(pkgName)
// If no platform/arch info is embedded in the package name, assume it's universal.
if pkgPlatform == "" && pkgArch == "" {
return true
}

currentOS := runtime.GOOS
currentArch := runtime.GOARCH

// The package matches only if both OS and architecture are compatible.
return pkgPlatform == currentOS && resolver.isArchCompatible(pkgArch, currentArch)
}

// isArchCompatible determines whether the package's architecture is compatible with the current machine's architecture.
func (resolver *NpmResolver) isArchCompatible(pkgArch, currentArch string) bool {
return normalizeArch(pkgArch) == normalizeArch(currentArch)
}
1 change: 1 addition & 0 deletions pkg/deps/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Result struct {
LicenseSpdxID string
ResolveErrors []error
Version string
IsCrossPlatform bool
}

// Report is a collection of resolved Result.
Expand Down