Add rust cargo support for dep command. (#121)
* Improve license comparation with operator.
* Add rust cargo support.
diff --git a/Dockerfile b/Dockerfile
index 1df4281..8024a92 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,7 +32,7 @@
# Go
COPY --from=build /usr/local/go/bin/go /usr/local/go/bin/go
ENV PATH="/usr/local/go/bin:$PATH"
-RUN apk add --no-cache bash gcc musl-dev npm
+RUN apk add --no-cache bash gcc musl-dev npm cargo
# Go
WORKDIR /github/workspace/
diff --git a/pkg/deps/cargo.go b/pkg/deps/cargo.go
new file mode 100644
index 0000000..e2613d2
--- /dev/null
+++ b/pkg/deps/cargo.go
@@ -0,0 +1,158 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps
+
+import (
+ "encoding/json"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+
+ "github.com/apache/skywalking-eyes/internal/logger"
+ "github.com/apache/skywalking-eyes/pkg/license"
+)
+
+type CargoMetadata struct {
+ Packages []CargoPackage `json:"packages"`
+}
+
+type CargoPackage struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ License string `json:"license"`
+ LicenseFile string `json:"license_file"`
+ ManifestPath string `json:"manifest_path"`
+}
+
+type CargoTomlResolver struct {
+ Resolver
+}
+
+func (resolver *CargoTomlResolver) CanResolve(file string) bool {
+ base := filepath.Base(file)
+ logger.Log.Debugln("Base name:", base)
+ return base == "Cargo.toml"
+}
+
+// Resolve resolves licenses of all dependencies declared in the Cargo.toml file.
+func (resolver *CargoTomlResolver) Resolve(cargoTomlFile string, config *ConfigDeps, report *Report) error {
+ dir := filepath.Dir(cargoTomlFile)
+
+ download := exec.Command("cargo", "fetch")
+ logger.Log.Debugf("Run command: %v, please wait", download.String())
+ download.Stdout = os.Stdout
+ download.Stderr = os.Stderr
+ download.Dir = dir
+ if err := download.Run(); err != nil {
+ return err
+ }
+
+ cmd := exec.Command("cargo", "metadata", "--format-version=1", "--all-features")
+ cmd.Dir = dir
+ output, err := cmd.Output()
+ if err != nil {
+ return err
+ }
+
+ var metadata CargoMetadata
+ if err := json.Unmarshal(output, &metadata); err != nil {
+ return err
+ }
+
+ logger.Log.Debugln("Package size:", len(metadata.Packages))
+
+ return resolver.ResolvePackages(metadata.Packages, config, report)
+}
+
+// ResolvePackages resolves the licenses of the given packages.
+func (resolver *CargoTomlResolver) ResolvePackages(packages []CargoPackage, config *ConfigDeps, report *Report) error {
+ for i := range packages {
+ pkg := packages[i]
+
+ if config.IsExcluded(pkg.Name, pkg.Version) {
+ continue
+ }
+ if l, ok := config.GetUserConfiguredLicense(pkg.Name, pkg.Version); ok {
+ report.Resolve(&Result{
+ Dependency: pkg.Name,
+ LicenseSpdxID: l,
+ Version: pkg.Version,
+ })
+ continue
+ }
+ err := resolver.ResolvePackageLicense(config, &pkg, report)
+ if err != nil {
+ logger.Log.Warnf("Failed to resolve the license of <%s@%s>: %v\n", pkg.Name, pkg.Version, err)
+ report.Skip(&Result{
+ Dependency: pkg.Name,
+ LicenseSpdxID: Unknown,
+ Version: pkg.Version,
+ })
+ }
+ }
+ return nil
+}
+
+var cargoPossibleLicenseFileName = regexp.MustCompile(`(?i)^LICENSE|LICENCE(\.txt)?|LICENSE-.+|COPYING(\.txt)?$`)
+
+// ResolvePackageLicense resolve the package license.
+// The CargoPackage.LicenseFile is generally used for non-standard licenses and is ignored now.
+func (resolver *CargoTomlResolver) ResolvePackageLicense(config *ConfigDeps, pkg *CargoPackage, report *Report) error {
+ dir := filepath.Dir(pkg.ManifestPath)
+ logger.Log.Debugf("Directory of %+v is %+v", pkg.Name, dir)
+ files, err := os.ReadDir(dir)
+ if err != nil {
+ return nil
+ }
+
+ var licenseFilePath string
+ var licenseContent []byte
+
+ licenseID := pkg.License
+
+ for _, info := range files {
+ if !cargoPossibleLicenseFileName.MatchString(info.Name()) {
+ continue
+ }
+
+ licenseFilePath = filepath.Join(dir, info.Name())
+ licenseContent, err = os.ReadFile(licenseFilePath)
+ if err != nil {
+ return err
+ }
+
+ break
+ }
+
+ if licenseID == "" { // If pkg.License is empty, identify the license ID from the license file content
+ if licenseID, err = license.Identify(string(licenseContent), config.Threshold); err != nil {
+ return err
+ }
+ }
+
+ report.Resolve(&Result{
+ Dependency: pkg.Name,
+ LicenseFilePath: licenseFilePath,
+ LicenseContent: string(licenseContent),
+ LicenseSpdxID: licenseID,
+ Version: pkg.Version,
+ })
+
+ return nil
+}
diff --git a/pkg/deps/cargo_test.go b/pkg/deps/cargo_test.go
new file mode 100644
index 0000000..db401a3
--- /dev/null
+++ b/pkg/deps/cargo_test.go
@@ -0,0 +1,198 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps_test
+
+import (
+ "github.com/apache/skywalking-eyes/internal/logger"
+ "github.com/apache/skywalking-eyes/pkg/deps"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+)
+
+func TestCanResolveCargo(t *testing.T) {
+ resolver := new(deps.CargoTomlResolver)
+ if !resolver.CanResolve("Cargo.toml") {
+ t.Error("CargoTomlResolver should resolve Cargo.toml")
+ return
+ }
+ if resolver.CanResolve("go.mod") {
+ t.Error("CargoTomlResolver shouldn't resolve go.mod")
+ }
+}
+
+func TestResolveCargos(t *testing.T) {
+ if _, err := exec.Command("cargo", "--version").Output(); err != nil {
+ logger.Log.Warnf("Failed to find cargo, the test `TestResolveCargo` was skipped")
+ return
+ }
+
+ {
+ cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+ config := deps.ConfigDeps{
+ Threshold: 0,
+ Files: []string{"Cargo.toml"},
+ Licenses: []*deps.ConfigDepLicense{},
+ Excludes: []deps.Exclude{},
+ }
+
+ report := resolveTmpCargo(t, cargoToml, &config)
+ if len(report.Resolved) != 1 {
+ t.Error("len(report.Resolved) != 1")
+ }
+ if report.Resolved[0].LicenseSpdxID != "Apache-2.0" {
+ t.Error("Package foo license isn't Apache-2.0")
+ }
+ }
+
+ {
+ cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+ config := deps.ConfigDeps{
+ Threshold: 0,
+ Files: []string{"Cargo.toml"},
+ Licenses: []*deps.ConfigDepLicense{},
+ Excludes: []deps.Exclude{{Name: "foo", Version: "0.0.0"}},
+ }
+
+ report := resolveTmpCargo(t, cargoToml, &config)
+ if len(report.Resolved) != 0 {
+ t.Error("len(report.Resolved) != 0")
+ }
+ }
+
+ {
+ cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+ config := deps.ConfigDeps{
+ Threshold: 0,
+ Files: []string{},
+ Licenses: []*deps.ConfigDepLicense{
+ {
+ Name: "foo",
+ Version: "0.0.0",
+ License: "MIT",
+ },
+ },
+ Excludes: []deps.Exclude{},
+ }
+
+ report := resolveTmpCargo(t, cargoToml, &config)
+ if len(report.Resolved) != 1 {
+ t.Error("len(report.Resolved) != 1")
+ }
+ if report.Resolved[0].LicenseSpdxID != "MIT" {
+ t.Error("Package foo license isn't modified to MIT")
+ }
+ }
+
+ {
+ cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+
+[dependencies]
+libc = "0.2.126"
+`
+
+ config := deps.ConfigDeps{
+ Threshold: 0,
+ Files: []string{"Cargo.toml"},
+ Licenses: []*deps.ConfigDepLicense{},
+ Excludes: []deps.Exclude{},
+ }
+
+ report := resolveTmpCargo(t, cargoToml, &config)
+ if len(report.Resolved) != 2 {
+ t.Error("len(report.Resolved) != 2")
+ }
+ for _, result := range report.Resolved {
+ if result.Dependency == "libc" {
+ if result.LicenseSpdxID != "MIT OR Apache-2.0" || result.LicenseContent == "" {
+ t.Error("Resolve dependency libc failed")
+ }
+ }
+ }
+ }
+}
+
+func resolveTmpCargo(t *testing.T, cargoTomlContent string, config *deps.ConfigDeps) *deps.Report {
+ dir, err := os.MkdirTemp("", "skywalking-eyes-test-cargo-")
+ if err != nil {
+ t.Error("Make temp dir failed", err)
+ return nil
+ }
+ defer func(path string) {
+ err := os.RemoveAll(path)
+ if err != nil {
+ logger.Log.Warn(err)
+ }
+ }(dir) // clean up
+
+ if err := os.Chdir(dir); err != nil {
+ t.Error("Chdir failed", err)
+ return nil
+ }
+
+ if _, err := exec.Command("cargo", "init", "--lib").Output(); err != nil {
+ t.Error("Cargo init failed", err)
+ return nil
+ }
+
+ cargoFile := filepath.Join(dir, "Cargo.toml")
+ if err := os.WriteFile(cargoFile, []byte(cargoTomlContent), 0644); err != nil {
+ t.Error("Write Cargo.toml failed", err)
+ return nil
+ }
+
+ resolver := new(deps.CargoTomlResolver)
+
+ var report deps.Report
+ if err := resolver.Resolve(cargoFile, config, &report); err != nil {
+ t.Error("CargoTomlResolver resolve failed", err)
+ return nil
+ }
+ return &report
+}
diff --git a/pkg/deps/resolve.go b/pkg/deps/resolve.go
index 8769d79..fc2f562 100644
--- a/pkg/deps/resolve.go
+++ b/pkg/deps/resolve.go
@@ -31,6 +31,7 @@
new(NpmResolver),
new(MavenPomResolver),
new(JarResolver),
+ new(CargoTomlResolver),
}
func Resolve(config *ConfigDeps, report *Report) error {