Skip to content

Commit 5537368

Browse files
authored
fix: compatibility issue with Node.js 24 (#252)
closes apache/skywalking#13517
1 parent 13c0e5b commit 5537368

File tree

3 files changed

+358
-0
lines changed

3 files changed

+358
-0
lines changed

pkg/deps/npm.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,67 @@ import (
2626
"os"
2727
"os/exec"
2828
"path/filepath"
29+
"regexp"
30+
"runtime"
2931
"strings"
3032
"time"
3133

3234
"github.com/apache/skywalking-eyes/pkg/license"
3335
"github.com/apache/skywalking-eyes/pkg/logger"
3436
)
3537

38+
// Constants for architecture names to avoid string duplication
39+
const (
40+
archAMD64 = "amd64"
41+
archARM64 = "arm64"
42+
archARM = "arm"
43+
)
44+
45+
// Cross-platform package pattern recognition (for precise matching)
46+
// These patterns work for both scoped (@scope/package-platform-arch) and
47+
// non-scoped (package-platform-arch) npm packages, as the platform/arch
48+
// suffix always appears at the end of the package name.
49+
// Examples:
50+
// - Scoped: @scope/foo-linux-x64
51+
// - Non-scoped: foo-linux-x64
52+
//
53+
// regex: matches package names ending with a specific string (e.g., "-linux-x64")
54+
// os: target operating system (e.g., "linux", "darwin", "windows")
55+
// arch: target CPU architecture (e.g., "x64", "arm64")
56+
var platformPatterns = []struct {
57+
regex *regexp.Regexp
58+
os string
59+
arch string
60+
}{
61+
// Android
62+
{regexp.MustCompile(`-android-arm64$`), "android", archARM64},
63+
{regexp.MustCompile(`-android-arm$`), "android", archARM},
64+
{regexp.MustCompile(`-android-x64$`), "android", "x64"},
65+
66+
// Darwin (macOS)
67+
{regexp.MustCompile(`-darwin-arm64$`), "darwin", archARM64},
68+
{regexp.MustCompile(`-darwin-x64$`), "darwin", "x64"},
69+
70+
// Linux
71+
{regexp.MustCompile(`-linux-arm64-glibc$`), "linux", archARM64},
72+
{regexp.MustCompile(`-linux-arm64-musl$`), "linux", archARM64},
73+
{regexp.MustCompile(`-linux-arm-glibc$`), "linux", archARM},
74+
{regexp.MustCompile(`-linux-arm-musl$`), "linux", archARM},
75+
{regexp.MustCompile(`-linux-x64-glibc$`), "linux", "x64"},
76+
{regexp.MustCompile(`-linux-x64-musl$`), "linux", "x64"},
77+
{regexp.MustCompile(`-linux-x64$`), "linux", "x64"},
78+
{regexp.MustCompile(`-linux-arm64$`), "linux", archARM64},
79+
{regexp.MustCompile(`-linux-arm$`), "linux", archARM},
80+
81+
// Windows
82+
{regexp.MustCompile(`-win32-arm64$`), "windows", archARM64},
83+
{regexp.MustCompile(`-win32-ia32$`), "windows", "ia32"},
84+
{regexp.MustCompile(`-win32-x64$`), "windows", "x64"},
85+
86+
// FreeBSD
87+
{regexp.MustCompile(`-freebsd-x64$`), "freebsd", "x64"},
88+
}
89+
3690
type NpmResolver struct {
3791
Resolver
3892
}
@@ -87,6 +141,8 @@ func (resolver *NpmResolver) Resolve(pkgFile string, config *ConfigDeps, report
87141
for _, pkg := range pkgs {
88142
if result := resolver.ResolvePackageLicense(pkg.Name, pkg.Path, config); result.LicenseSpdxID != "" {
89143
report.Resolve(result)
144+
} else if result.IsCrossPlatform {
145+
logger.Log.Warnf("Skipping cross-platform package %s (not for current platform %s %s)", pkg.Name, runtime.GOOS, runtime.GOARCH)
90146
} else {
91147
result.LicenseSpdxID = Unknown
92148
report.Skip(result)
@@ -198,6 +254,12 @@ func (resolver *NpmResolver) ResolvePackageLicense(pkgName, pkgPath string, conf
198254
result := &Result{
199255
Dependency: pkgName,
200256
}
257+
258+
if !resolver.isForCurrentPlatform(pkgName) {
259+
result.IsCrossPlatform = true
260+
return result
261+
}
262+
201263
// resolve from the package.json file
202264
if err := resolver.ResolvePkgFile(result, pkgPath, config); err != nil {
203265
result.ResolveErrors = append(result.ResolveErrors, err)
@@ -318,3 +380,82 @@ func (resolver *NpmResolver) ParsePkgFile(pkgFile string) (*Package, error) {
318380
}
319381
return &packageInfo, nil
320382
}
383+
384+
// normalizeArch converts various architecture aliases into Go's canonical naming.
385+
func normalizeArch(arch string) string {
386+
// Convert to lowercase to handle case variations (e.g., "AMD64").
387+
arch = strings.ToLower(arch)
388+
switch arch {
389+
// x86-64 family (64-bit Intel/AMD)
390+
case "x64", "x86_64", "amd64", "x86-64":
391+
return archAMD64
392+
// x86 32-bit family (legacy)
393+
case "ia32", "x86", "386", "i386", "i686":
394+
return "386"
395+
// ARM 64-bit
396+
case "arm64", "aarch64":
397+
return archARM64
398+
// ARM 32-bit
399+
case "arm", "armv7", "armhf", "armv7l", "armel":
400+
return archARM
401+
// Unknown architecture: return as-is (alternatively, could return empty to indicate incompatibility).
402+
default:
403+
return arch
404+
}
405+
}
406+
407+
// analyzePackagePlatform extracts the target OS and architecture from a package name.
408+
func (resolver *NpmResolver) analyzePackagePlatform(pkgName string) (pkgOS, pkgArch string, partial bool) {
409+
for _, pattern := range platformPatterns {
410+
if pattern.regex.MatchString(pkgName) {
411+
return pattern.os, pattern.arch, false
412+
}
413+
}
414+
415+
// Detect OS-only suffixes like "-linux", "-darwin", "-win32"
416+
osOnlyPatterns := []string{
417+
"-linux",
418+
"-darwin",
419+
"-win32",
420+
"-windows",
421+
"-android",
422+
"-freebsd",
423+
}
424+
for _, osSuffix := range osOnlyPatterns {
425+
if strings.HasSuffix(pkgName, osSuffix) {
426+
return strings.TrimPrefix(osSuffix, "-"), "", true
427+
}
428+
}
429+
430+
return "", "", false
431+
}
432+
433+
// isForCurrentPlatform checks whether the package is intended for the current OS and architecture.
434+
func (resolver *NpmResolver) isForCurrentPlatform(pkgName string) bool {
435+
pkgOS, pkgArch, partial := resolver.analyzePackagePlatform(pkgName)
436+
437+
// OS-only package: explicitly reject with friendly error
438+
if partial {
439+
logger.Log.Warnf(
440+
"Package %q declares a platform without architecture. "+
441+
"Please use a full platform-arch suffix (e.g. -linux-x64, -darwin-arm64).",
442+
pkgName,
443+
)
444+
return false
445+
}
446+
447+
// Universal package
448+
if pkgOS == "" && pkgArch == "" {
449+
return true
450+
}
451+
452+
currentOS := runtime.GOOS
453+
currentArch := runtime.GOARCH
454+
455+
return pkgOS == currentOS && resolver.isArchCompatible(pkgArch, currentArch)
456+
}
457+
458+
// isArchCompatible determines whether the package's architecture is compatible with the current machine's architecture.
459+
func (resolver *NpmResolver) isArchCompatible(pkgArch, currentArch string) bool {
460+
return normalizeArch(pkgArch) == normalizeArch(currentArch)
461+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package deps_test
19+
20+
import (
21+
"os"
22+
"path/filepath"
23+
"runtime"
24+
"strings"
25+
"testing"
26+
27+
"github.com/apache/skywalking-eyes/pkg/deps"
28+
)
29+
30+
const (
31+
npmLicenseMIT = "MIT"
32+
npmLicenseApache20 = "Apache-2.0"
33+
)
34+
35+
// TC-NEW-001
36+
// Regression test: cross-platform npm binary packages must be skipped safely.
37+
func TestResolvePackageLicense_SkipCrossPlatformPackages(t *testing.T) {
38+
resolver := &deps.NpmResolver{}
39+
cfg := &deps.ConfigDeps{}
40+
41+
var crossPlatformPkgs []string
42+
switch runtime.GOOS {
43+
case "linux":
44+
crossPlatformPkgs = []string{
45+
"@parcel/watcher-darwin-arm64",
46+
"@parcel/watcher-win32-x64",
47+
}
48+
case "darwin":
49+
crossPlatformPkgs = []string{
50+
"@parcel/watcher-linux-x64",
51+
"@parcel/watcher-win32-x64",
52+
}
53+
default: // windows
54+
crossPlatformPkgs = []string{
55+
"@parcel/watcher-linux-x64",
56+
}
57+
}
58+
59+
for _, pkg := range crossPlatformPkgs {
60+
pkg := pkg // capture loop variable
61+
62+
t.Run(pkg+"/path-not-exist", func(t *testing.T) {
63+
// 001-A: cross-platform + path not exist
64+
result := resolver.ResolvePackageLicense(pkg, "/non/existent/path", cfg)
65+
if result.LicenseSpdxID != "" {
66+
t.Fatalf(
67+
"expected empty license for cross-platform package %q, got %q",
68+
pkg,
69+
result.LicenseSpdxID,
70+
)
71+
}
72+
})
73+
74+
t.Run(pkg+"/package-json-exists", func(t *testing.T) {
75+
// 001-B: cross-platform + package.json exists
76+
tmp := t.TempDir()
77+
err := os.WriteFile(
78+
filepath.Join(tmp, "package.json"),
79+
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseMIT+"\"}"),
80+
0o600,
81+
)
82+
if err != nil {
83+
t.Fatal(err)
84+
}
85+
86+
result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
87+
if result.LicenseSpdxID != "" {
88+
t.Fatalf(
89+
"expected empty license for cross-platform package %q, got %q",
90+
pkg,
91+
result.LicenseSpdxID,
92+
)
93+
}
94+
})
95+
96+
t.Run(pkg+"/valid-license", func(t *testing.T) {
97+
// 001-C: cross-platform + valid SPDX license
98+
tmp := t.TempDir()
99+
err := os.WriteFile(
100+
filepath.Join(tmp, "package.json"),
101+
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseApache20+"\"}"),
102+
0o600,
103+
)
104+
if err != nil {
105+
t.Fatal(err)
106+
}
107+
108+
result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
109+
if result.LicenseSpdxID != "" {
110+
t.Fatalf(
111+
"expected empty license for cross-platform package %q, got %q",
112+
pkg,
113+
result.LicenseSpdxID,
114+
)
115+
}
116+
})
117+
}
118+
}
119+
120+
// TC-NEW-002
121+
// Functional test: current-platform packages should be resolved normally.
122+
func TestResolvePackageLicense_CurrentPlatformPackages(t *testing.T) {
123+
resolver := &deps.NpmResolver{}
124+
cfg := &deps.ConfigDeps{}
125+
126+
t.Run("normal package with license field", func(t *testing.T) {
127+
tmp := t.TempDir()
128+
err := os.WriteFile(
129+
filepath.Join(tmp, "package.json"),
130+
[]byte("{\"name\":\"normal-pkg\",\"license\":\""+npmLicenseApache20+"\"}"),
131+
0o600,
132+
)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
137+
result := resolver.ResolvePackageLicense("normal-pkg", tmp, cfg)
138+
if result.LicenseSpdxID != npmLicenseApache20 {
139+
t.Fatalf(
140+
"expected license %s, got %q",
141+
npmLicenseApache20,
142+
result.LicenseSpdxID,
143+
)
144+
}
145+
})
146+
147+
t.Run("package without license field", func(t *testing.T) {
148+
tmp := t.TempDir()
149+
err := os.WriteFile(
150+
filepath.Join(tmp, "package.json"),
151+
[]byte("{\"name\":\"no-license-pkg\"}"),
152+
0o600,
153+
)
154+
if err != nil {
155+
t.Fatal(err)
156+
}
157+
158+
result := resolver.ResolvePackageLicense("no-license-pkg", tmp, cfg)
159+
if result.LicenseSpdxID != "" {
160+
t.Fatalf(
161+
"expected empty license, got %q",
162+
result.LicenseSpdxID,
163+
)
164+
}
165+
})
166+
}
167+
168+
// TC-NEW-003
169+
// Stability & defensive tests: malformed inputs must never cause panic.
170+
func TestResolvePackageLicense_DefensiveScenarios(t *testing.T) {
171+
resolver := &deps.NpmResolver{}
172+
cfg := &deps.ConfigDeps{}
173+
174+
t.Run("non-existent path", func(_ *testing.T) {
175+
_ = resolver.ResolvePackageLicense("some-pkg", "/definitely/not/exist", cfg)
176+
})
177+
178+
t.Run("malformed package.json", func(t *testing.T) {
179+
tmp := t.TempDir()
180+
err := os.WriteFile(
181+
filepath.Join(tmp, "package.json"),
182+
[]byte("{\"name\":\"bad-json\",\"license\":"),
183+
0o600,
184+
)
185+
if err != nil {
186+
t.Fatal(err)
187+
}
188+
_ = resolver.ResolvePackageLicense("bad-json", tmp, cfg)
189+
})
190+
191+
t.Run("invalid license field type", func(t *testing.T) {
192+
tmp := t.TempDir()
193+
err := os.WriteFile(
194+
filepath.Join(tmp, "package.json"),
195+
[]byte("{\"name\":\"weird-pkg\",\"license\":123}"),
196+
0o600,
197+
)
198+
if err != nil {
199+
t.Fatal(err)
200+
}
201+
_ = resolver.ResolvePackageLicense("weird-pkg", tmp, cfg)
202+
})
203+
204+
t.Run("empty package name", func(_ *testing.T) {
205+
_ = resolver.ResolvePackageLicense("", "/not/exist", cfg)
206+
})
207+
208+
t.Run("overly long package name", func(_ *testing.T) {
209+
longName := strings.Repeat("a", 10_000)
210+
_ = resolver.ResolvePackageLicense(longName, "/not/exist", cfg)
211+
})
212+
213+
t.Run("path traversal-like package name", func(_ *testing.T) {
214+
_ = resolver.ResolvePackageLicense("../../../../etc/passwd", "/not/exist", cfg)
215+
})
216+
}

pkg/deps/result.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Result struct {
3838
LicenseSpdxID string
3939
ResolveErrors []error
4040
Version string
41+
IsCrossPlatform bool
4142
}
4243

4344
// Report is a collection of resolved Result.

0 commit comments

Comments
 (0)