You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by ke...@apache.org on 2021/07/25 10:56:11 UTC

[skywalking-eyes] branch main updated: Add support for resolving npm dependencies' licenses (#48)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git


The following commit(s) were added to refs/heads/main by this push:
     new d6e232b  Add support for resolving npm dependencies' licenses (#48)
d6e232b is described below

commit d6e232b01042d112b103ae6c7f51c5c677e1f37e
Author: Youhan Wu <43...@users.noreply.github.com>
AuthorDate: Sun Jul 25 18:56:03 2021 +0800

    Add support for resolving npm dependencies' licenses (#48)
---
 pkg/deps/npm.go     | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 pkg/deps/resolve.go |   1 +
 2 files changed, 220 insertions(+)

diff --git a/pkg/deps/npm.go b/pkg/deps/npm.go
new file mode 100644
index 0000000..0635b6a
--- /dev/null
+++ b/pkg/deps/npm.go
@@ -0,0 +1,219 @@
+//
+// Licensed to 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. Apache Software Foundation (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 (
+	"bufio"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"time"
+
+	"github.com/apache/skywalking-eyes/license-eye/internal/logger"
+	"github.com/apache/skywalking-eyes/license-eye/pkg/license"
+)
+
+type NpmResolver struct {
+	Resolver
+}
+
+// Package represents package.json
+type Package struct {
+	Name         string            `json:"name"`
+	Version      string            `json:"version"`
+	License      string            `json:"license"`
+	Dependencies map[string]string `json:"dependencies"`
+}
+
+const packageFile = "package.json"
+
+// CanResolve checks whether the given file is the npm package file
+func (resolver *NpmResolver) CanResolve(file string) bool {
+	base := filepath.Base(file)
+	logger.Log.Debugln("Base name:", base)
+	return base == packageFile
+}
+
+// Resolve resolves licenses of all dependencies declared in the package.json file.
+func (resolver *NpmResolver) Resolve(packageFile string, report *Report) error {
+	// Parse the project package file to gather the required dependencies
+	packageInfo, err := resolver.parsePackageFile(packageFile)
+	if err != nil {
+		return err
+	}
+	depNames := make([]string, 0, len(packageInfo.Dependencies))
+	for dep := range packageInfo.Dependencies {
+		depNames = append(depNames, dep)
+	}
+
+	// Run command 'npm install' to install or update the required node packages
+	// Query from the command line first whether to skip this procedure
+	// in case that the dependent packages are downloaded and brought up-to-date
+	root := filepath.Dir(packageFile)
+	if needSkip := resolver.NeedSkipInstallPkgs(); !needSkip {
+		if err := resolver.InstallPkgs(root); err != nil {
+			return fmt.Errorf("fail to install depNames: %+v", err)
+		}
+	}
+
+	// Change working directory to node_modules
+	depPath := filepath.Join(root, "node_modules")
+	if err := os.Chdir(depPath); err != nil {
+		return err
+	}
+
+	// Walk through each package's root directory to resolve licenses
+	// First, try to parse the package's package.json file to check the license file
+	// If the previous step fails, then try to identify the package's LICENSE file
+	for _, depName := range depNames {
+		if err := resolver.ResolvePackageLicense(depName, report); err != nil {
+			logger.Log.Warnln("Failed to resolve the license of dependency:", depName, err)
+			report.Skip(&Result{
+				Dependency:    depName,
+				LicenseSpdxID: Unknown,
+			})
+		}
+	}
+	return nil
+}
+
+// parsePackageFile parses the content of the package file
+func (resolver *NpmResolver) parsePackageFile(packageFile string) (*Package, error) {
+	content, err := ioutil.ReadFile(packageFile)
+	if err != nil {
+		return nil, err
+	}
+	var packageInfo Package
+	if err := json.Unmarshal(content, &packageInfo); err != nil {
+		return nil, err
+	}
+	return &packageInfo, nil
+}
+
+// NeedSkipInstallPkgs queries whether to skip the procedure of installing or updating packages
+func (resolver *NpmResolver) NeedSkipInstallPkgs() bool {
+	const countdown = 5
+	input := make(chan rune)
+	logger.Log.Infoln(fmt.Sprintf("Try to install nodejs packages in %v seconds, press [s/S] and ENTER to skip", countdown))
+
+	// Read the input character from console in a non-blocking way
+	go func(ch chan rune) {
+		reader := bufio.NewReader(os.Stdin)
+		c, _, err := reader.ReadRune()
+		if err != nil {
+			close(ch)
+			return
+		}
+		input <- c
+	}(input)
+
+	// Wait for the user to input a character or the countdown timer to elapse
+	select {
+	case input, ok := <-input:
+		if ok && (input == 's' || input == 'S') {
+			return true
+		}
+		logger.Log.Infoln("Unknown input, try to install packages")
+		return false
+	case <-time.After(countdown * time.Second):
+		logger.Log.Infoln("Time out, try to install packages")
+		return false
+	}
+}
+
+// InstallPkgs runs command 'npm install' to install node packages
+func (resolver *NpmResolver) InstallPkgs(root string) error {
+	cmd := exec.Command("npm", "install")
+	cmd.Dir = root
+	logger.Log.Println(fmt.Sprintf("Run command: %v, please wait", cmd.String()))
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	if err := cmd.Run(); err != nil {
+		return err
+	}
+	return nil
+}
+
+// ResolvePackageLicense resolves the licenses of the given packages.
+func (resolver *NpmResolver) ResolvePackageLicense(depName string, report *Report) error {
+	depFiles, err := ioutil.ReadDir(depName)
+	if err != nil {
+		return err
+	}
+
+	// Record the errors encountered when parsing the package.json file
+	packageErr := errors.New("cannot find the package.json file")
+
+	// STEP 1: Try to find and parse the package.json file to capture the license field
+	for _, info := range depFiles {
+		if info.IsDir() || info.Name() != packageFile {
+			continue
+		}
+		packageFilePath := filepath.Join(depName, info.Name())
+		packageInfo, err := resolver.parsePackageFile(packageFilePath)
+		if err != nil {
+			packageErr = err
+			break
+		}
+		if packageInfo.License == "" {
+			packageErr = fmt.Errorf("cannot capture the license field")
+			break
+		}
+		report.Resolve(&Result{
+			Dependency:      depName,
+			LicenseFilePath: "",
+			LicenseContent:  "",
+			LicenseSpdxID:   packageInfo.License,
+		})
+		return nil
+	}
+
+	// Record the errors encountered when identifying the license file
+	licenseErr := errors.New("cannot find the license file")
+
+	// STEP 2: Try to find the license file to identify the license
+	for _, info := range depFiles {
+		if info.IsDir() || !possibleLicenseFileName.MatchString(info.Name()) {
+			continue
+		}
+		licenseFilePath := filepath.Join(depName, info.Name())
+		content, err := ioutil.ReadFile(licenseFilePath)
+		if err != nil {
+			licenseErr = err
+			break
+		}
+		identifier, err := license.Identify(depName, string(content))
+		if err != nil {
+			licenseErr = err
+			break
+		}
+		report.Resolve(&Result{
+			Dependency:      depName,
+			LicenseFilePath: licenseFilePath,
+			LicenseContent:  string(content),
+			LicenseSpdxID:   identifier,
+		})
+		return nil
+	}
+
+	return fmt.Errorf("%+v; %+v", packageErr, licenseErr)
+}
diff --git a/pkg/deps/resolve.go b/pkg/deps/resolve.go
index 360f374..247fa1e 100644
--- a/pkg/deps/resolve.go
+++ b/pkg/deps/resolve.go
@@ -28,6 +28,7 @@ type Resolver interface {
 
 var Resolvers = []Resolver{
 	new(GoModResolver),
+	new(NpmResolver),
 }
 
 func Resolve(config *ConfigDeps, report *Report) error {