diff --git a/pkg/deps/npm.go b/pkg/deps/npm.go index 63bc102..3fbc00c 100644 --- a/pkg/deps/npm.go +++ b/pkg/deps/npm.go @@ -26,6 +26,8 @@ import ( "os" "os/exec" "path/filepath" + "regexp" + "runtime" "strings" "time" @@ -33,6 +35,58 @@ import ( "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 } @@ -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) @@ -198,6 +254,12 @@ 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) @@ -318,3 +380,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) +} diff --git a/pkg/deps/result.go b/pkg/deps/result.go index 66f87df..1c3f645 100644 --- a/pkg/deps/result.go +++ b/pkg/deps/result.go @@ -38,6 +38,7 @@ type Result struct { LicenseSpdxID string ResolveErrors []error Version string + IsCrossPlatform bool } // Report is a collection of resolved Result.