| // 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 ( |
| "bufio" |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "time" |
| |
| "github.com/apache/skywalking-eyes/internal/logger" |
| "github.com/apache/skywalking-eyes/pkg/license" |
| ) |
| |
| type NpmResolver struct { |
| Resolver |
| } |
| |
| // Lcs represents the license style in package.json |
| type Lcs struct { |
| Type string `json:"type"` |
| URL string `json:"url"` |
| } |
| |
| // Package represents package.json |
| // License field has inconsistent styles, so we just store the byte array here to postpone unmarshalling |
| type Package struct { |
| Name string `json:"name"` |
| License json.RawMessage `json:"license"` |
| Licenses []Lcs `json:"licenses"` |
| Path string `json:"-"` |
| Version string `json:"version"` |
| } |
| |
| const PkgFileName = "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 == PkgFileName |
| } |
| |
| // Resolve resolves licenses of all dependencies declared in the package.json file. |
| func (resolver *NpmResolver) Resolve(pkgFile string, config *ConfigDeps, report *Report) error { |
| workDir := filepath.Dir(pkgFile) |
| if err := os.Chdir(workDir); err != nil { |
| return err |
| } |
| |
| // 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 |
| if needSkip := resolver.NeedSkipInstallPkgs(); !needSkip { |
| resolver.InstallPkgs() |
| } |
| |
| // Run command 'npm ls --all --parseable' to list all the installed packages' paths |
| // Use a package directory's relative path from the node_modules directory, to infer its package name |
| // Thus gathering all the installed packages' names and paths |
| pkgDir := filepath.Join(workDir, "node_modules") |
| pkgs := resolver.GetInstalledPkgs(pkgDir) |
| |
| // Walk through each package's root directory to resolve licenses |
| // Resolve from a package's package.json file or its license file |
| for _, pkg := range pkgs { |
| if result := resolver.ResolvePackageLicense(pkg.Name, pkg.Path, config); result.LicenseSpdxID != "" { |
| report.Resolve(result) |
| } else { |
| result.LicenseSpdxID = Unknown |
| report.Skip(result) |
| logger.Log.Warnln("Failed to resolve the license of dependency:", pkg.Name, result.ResolveErrors) |
| } |
| } |
| return 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() { |
| cmd := exec.Command("npm", "install") |
| logger.Log.Println(fmt.Sprintf("Run command: %v, please wait", cmd.String())) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| // Error occurs all the time in npm commands, so no return statement here |
| if err := cmd.Run(); err != nil { |
| logger.Log.Errorln(err) |
| } |
| } |
| |
| // ListPkgPaths runs npm command to list all the production only packages' absolute paths, one path per line |
| // Note that although the flag `--long` can show more information line like a package's name, |
| // its realization and printing format is not uniform in different npm-cli versions |
| func (resolver *NpmResolver) ListPkgPaths() (io.Reader, error) { |
| buffer := &bytes.Buffer{} |
| cmd := exec.Command("npm", "ls", "--all", "--production", "--parseable") |
| cmd.Stderr = os.Stderr |
| cmd.Stdout = buffer |
| // Error occurs all the time in npm commands, so no return statement here |
| err := cmd.Run() |
| return buffer, err |
| } |
| |
| // GetInstalledPkgs gathers all the installed packages' names and paths |
| // it uses a package directory's relative path from the node_modules directory, to infer its package name |
| func (resolver *NpmResolver) GetInstalledPkgs(pkgDir string) []*Package { |
| buffer, err := resolver.ListPkgPaths() |
| // Error occurs all the time in npm commands, so no return statement here |
| if err != nil { |
| logger.Log.Errorln(err) |
| } |
| pkgs := make([]*Package, 0) |
| sc := bufio.NewScanner(buffer) |
| for sc.Scan() { |
| absPath := sc.Text() |
| rel, err := filepath.Rel(pkgDir, absPath) |
| if err != nil { |
| logger.Log.Errorln(err) |
| continue |
| } |
| if rel == "" || rel == "." || rel == ".." { |
| continue |
| } |
| pkgName := filepath.ToSlash(rel) |
| pkgs = append(pkgs, &Package{ |
| Name: pkgName, |
| Path: absPath, |
| }) |
| } |
| return pkgs |
| } |
| |
| // ResolvePackageLicense resolves the licenses of the given packages. |
| // First, try to find and 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 |
| // It's a necessary procedure to check the LICENSE file, because the resolver needs to record the license content |
| func (resolver *NpmResolver) ResolvePackageLicense(pkgName, pkgPath string, config *ConfigDeps) *Result { |
| result := &Result{ |
| Dependency: pkgName, |
| } |
| // resolve from the package.json file |
| if err := resolver.ResolvePkgFile(result, pkgPath, config); err != nil { |
| result.ResolveErrors = append(result.ResolveErrors, err) |
| } |
| |
| // resolve from the LICENSE file |
| if err := resolver.ResolveLcsFile(result, pkgPath, config); err != nil { |
| result.ResolveErrors = append(result.ResolveErrors, err) |
| } |
| |
| return result |
| } |
| |
| // ResolvePkgFile tries to find and parse the package.json file to capture the license field |
| func (resolver *NpmResolver) ResolvePkgFile(result *Result, pkgPath string, config *ConfigDeps) error { |
| expectedPkgFile := filepath.Join(pkgPath, PkgFileName) |
| packageInfo, err := resolver.ParsePkgFile(expectedPkgFile) |
| if err != nil { |
| return err |
| } |
| |
| result.Version = packageInfo.Version |
| if l, ok := config.GetUserConfiguredLicense(packageInfo.Name, packageInfo.Version); ok { |
| result.LicenseSpdxID = l |
| return nil |
| } |
| |
| if lcs, ok := resolver.ResolveLicenseField(packageInfo.License); ok { |
| result.LicenseSpdxID = lcs |
| return nil |
| } |
| |
| if lcs, ok := resolver.ResolveLicensesField(packageInfo.Licenses); ok { |
| result.LicenseSpdxID = lcs |
| return nil |
| } |
| |
| return fmt.Errorf(`cannot parse the "license"/"licenses" field`) |
| } |
| |
| // ResolveLicenseField parses and validates the "license" field in package.json file |
| func (resolver *NpmResolver) ResolveLicenseField(rawData []byte) (string, bool) { |
| if len(rawData) > 0 { |
| switch rawData[0] { |
| case '"': |
| var lcs string |
| _ = json.Unmarshal(rawData, &lcs) |
| if lcs != "" { |
| return lcs, true |
| } |
| case '{': |
| var lcs Lcs |
| _ = json.Unmarshal(rawData, &lcs) |
| if t := lcs.Type; t != "" { |
| return t, true |
| } |
| } |
| } |
| return "", false |
| } |
| |
| // ResolveLicensesField parses and validates the "licenses" field in package.json file |
| // Additionally, the output is converted into the SPDX license expression syntax version 2.0 string, like "ISC OR GPL-3.0" |
| func (resolver *NpmResolver) ResolveLicensesField(licenses []Lcs) (string, bool) { |
| var lcs []string |
| for _, l := range licenses { |
| lcs = append(lcs, l.Type) |
| } |
| if len(lcs) == 0 { |
| return "", false |
| } |
| return strings.Join(lcs, " OR "), true |
| } |
| |
| // ResolveLcsFile tries to find the license file to identify the license |
| func (resolver *NpmResolver) ResolveLcsFile(result *Result, pkgPath string, config *ConfigDeps) error { |
| depFiles, err := os.ReadDir(pkgPath) |
| if err != nil { |
| return err |
| } |
| for _, info := range depFiles { |
| if info.IsDir() || !possibleLicenseFileName.MatchString(info.Name()) { |
| continue |
| } |
| licenseFilePath := filepath.Join(pkgPath, info.Name()) |
| result.LicenseFilePath = licenseFilePath |
| content, err := os.ReadFile(licenseFilePath) |
| if err != nil { |
| return err |
| } |
| result.LicenseContent = string(content) |
| if result.LicenseSpdxID != "" { |
| return nil |
| } |
| if l, ok := config.GetUserConfiguredLicense(info.Name(), result.Version); ok { |
| result.LicenseSpdxID = l |
| return nil |
| } |
| identifier, err := license.Identify(string(content), config.Threshold) |
| if err != nil { |
| return err |
| } |
| result.LicenseSpdxID = identifier |
| return nil |
| } |
| return fmt.Errorf("cannot find the license file") |
| } |
| |
| // ParsePkgFile parses the content of the package file |
| func (resolver *NpmResolver) ParsePkgFile(pkgFile string) (*Package, error) { |
| content, err := os.ReadFile(pkgFile) |
| if err != nil { |
| return nil, err |
| } |
| var packageInfo Package |
| if err := json.Unmarshal(content, &packageInfo); err != nil { |
| return nil, err |
| } |
| return &packageInfo, nil |
| } |