245 lines
7.1 KiB
Go
245 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/yuin/goldmark"
|
|
"github.com/yuin/goldmark/ast"
|
|
"github.com/yuin/goldmark/text"
|
|
"golang.org/x/mod/modfile"
|
|
)
|
|
|
|
//go:embed data/spdx_mapping.json
|
|
var spdxMappingFile []byte
|
|
|
|
var debug bool
|
|
var nameToSPDX map[string]string
|
|
|
|
func debugf(format string, v ...any) {
|
|
if !debug {
|
|
return
|
|
}
|
|
log.Printf("DEBUG: "+format, v...)
|
|
}
|
|
|
|
func main() {
|
|
var help, verbose bool
|
|
var threshold float64
|
|
var whitelistFn, userpkg string
|
|
|
|
flag.BoolVar(&debug, "debug", false, "output debugging information")
|
|
flag.BoolVar(&help, "help", false, "output this help text")
|
|
flag.BoolVar(&verbose, "verbose", false, "output verbose information instead of just errors")
|
|
flag.Float64Var(&threshold, "threshold", 0.8, "threshold for license classification")
|
|
flag.StringVar(&whitelistFn, "whitelist", "", "use the given white-list file for comparison")
|
|
flag.StringVar(&userpkg, "package", "", "only test the given package (all by default)")
|
|
flag.Parse()
|
|
|
|
if help || flag.NArg() > 1 {
|
|
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [options] [telegraf root dir]\n", os.Args[0])
|
|
fmt.Fprintf(flag.CommandLine.Output(), "Options:\n")
|
|
flag.PrintDefaults()
|
|
fmt.Fprintf(flag.CommandLine.Output(), "\n")
|
|
fmt.Fprintf(flag.CommandLine.Output(), "Arguments:\n")
|
|
fmt.Fprintf(flag.CommandLine.Output(), " telegraf root dir (optional)\n")
|
|
fmt.Fprintf(flag.CommandLine.Output(), " path to the root directory of telegraf (default: .)\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Setup full-name to license SPDX identifier mapping
|
|
if err := json.Unmarshal(spdxMappingFile, &nameToSPDX); err != nil {
|
|
log.Fatalf("Unmarshalling license name to SPDX mapping failed: %v", err)
|
|
}
|
|
|
|
// Get required files
|
|
path := "."
|
|
if flag.NArg() == 1 {
|
|
path = flag.Arg(0)
|
|
}
|
|
|
|
moduleFilename := filepath.Join(path, "go.mod")
|
|
licenseFilename := filepath.Join(path, "docs", "LICENSE_OF_DEPENDENCIES.md")
|
|
|
|
var override whitelist
|
|
if whitelistFn != "" {
|
|
log.Printf("Reading whitelist file %q...", whitelistFn)
|
|
if err := override.Parse(whitelistFn); err != nil {
|
|
log.Fatalf("Reading whitelist failed: %v", err)
|
|
}
|
|
}
|
|
|
|
log.Printf("Reading module file %q...", moduleFilename)
|
|
modbuf, err := os.ReadFile(moduleFilename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
depModules, err := modfile.Parse(moduleFilename, modbuf, nil)
|
|
if err != nil {
|
|
log.Fatalf("Parsing modules failed: %f", err)
|
|
}
|
|
debugf("found %d required packages", len(depModules.Require))
|
|
|
|
dependencies := make(map[string]string)
|
|
for _, d := range depModules.Require {
|
|
dependencies[d.Mod.Path] = d.Mod.Version
|
|
}
|
|
|
|
log.Printf("Reading license file %q...", licenseFilename)
|
|
licensesMarkdown, err := os.ReadFile(licenseFilename)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Parse the markdown document
|
|
parser := goldmark.DefaultParser()
|
|
root := parser.Parse(text.NewReader(licensesMarkdown))
|
|
|
|
// Prepare a line parser
|
|
lineParser := goldmark.DefaultParser()
|
|
|
|
// Collect the licenses
|
|
// For each list we search for the items and parse them.
|
|
// Expect a pattern of <package name> <link>.
|
|
ignored := 0
|
|
var packageInfos []packageInfo
|
|
for node := root.FirstChild(); node != nil; node = node.NextSibling() {
|
|
listNode, ok := node.(*ast.List)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for inode := listNode.FirstChild(); inode != nil; inode = inode.NextSibling() {
|
|
itemNode, ok := inode.(*ast.ListItem)
|
|
if !ok || itemNode.ChildCount() != 1 {
|
|
continue
|
|
}
|
|
textNode, ok := itemNode.FirstChild().(*ast.TextBlock)
|
|
if !ok || textNode.Lines().Len() != 1 {
|
|
continue
|
|
}
|
|
|
|
lineSegment := textNode.Lines().At(0)
|
|
line := lineSegment.Value(licensesMarkdown)
|
|
lineRoot := lineParser.Parse(text.NewReader(line))
|
|
if lineRoot.ChildCount() != 1 || lineRoot.FirstChild().ChildCount() < 2 {
|
|
log.Printf("WARN: Ignoring item %q due to wrong count (%d/%d)", string(line), lineRoot.ChildCount(), lineRoot.FirstChild().ChildCount())
|
|
ignored++
|
|
continue
|
|
}
|
|
|
|
var name, license, link string
|
|
for lineElementNode := lineRoot.FirstChild().FirstChild(); lineElementNode != nil; lineElementNode = lineElementNode.NextSibling() {
|
|
switch v := lineElementNode.(type) {
|
|
case *ast.Text:
|
|
name += string(v.Value(line))
|
|
case *ast.Link:
|
|
license = string(v.FirstChild().(*ast.Text).Value(line))
|
|
link = string(v.Destination)
|
|
default:
|
|
debugf("ignoring unknown element %T (%v)", v, v)
|
|
}
|
|
}
|
|
name = strings.TrimSpace(name)
|
|
|
|
info := packageInfo{
|
|
name: name,
|
|
version: dependencies[name],
|
|
url: strings.TrimSpace(link),
|
|
license: strings.TrimSpace(license),
|
|
}
|
|
info.ToSPDX()
|
|
if info.name == "" {
|
|
log.Printf("WARN: Ignoring item %q due to empty package name", string(line))
|
|
ignored++
|
|
continue
|
|
}
|
|
if info.url == "" {
|
|
log.Printf("WARN: Ignoring item %q due to empty url name", string(line))
|
|
ignored++
|
|
continue
|
|
}
|
|
if info.license == "" {
|
|
log.Printf("WARN: Ignoring item %q due to empty license name", string(line))
|
|
ignored++
|
|
continue
|
|
}
|
|
debugf("adding %q with license %q (%s) and version %q at %q...", info.name, info.license, info.spdx, info.version, info.url)
|
|
packageInfos = append(packageInfos, info)
|
|
}
|
|
}
|
|
|
|
// Get the superset of licenses
|
|
if debug {
|
|
licenseSet := make(map[string]bool, len(packageInfos))
|
|
licenseNames := make([]string, 0, len(packageInfos))
|
|
for _, info := range packageInfos {
|
|
if found := licenseSet[info.license]; !found {
|
|
licenseNames = append(licenseNames, info.license)
|
|
}
|
|
licenseSet[info.license] = true
|
|
}
|
|
sort.Strings(licenseNames)
|
|
log.Println("Using licenses:")
|
|
for _, license := range licenseNames {
|
|
log.Println(" " + license)
|
|
}
|
|
}
|
|
|
|
// Check the licenses by matching their text and compare the classification result
|
|
// with the information provided by the user
|
|
var succeeded, warn, failed int
|
|
for _, info := range packageInfos {
|
|
// Ignore all packages except the ones given by the user (if any)
|
|
if userpkg != "" && userpkg != info.name {
|
|
continue
|
|
}
|
|
|
|
// Check if we got a whitelist entry for the package
|
|
if ok, found := override.Check(info.name, info.version, info.spdx); found {
|
|
if ok {
|
|
log.Printf("OK: \"%s@%s\" (%s) (whitelist)", info.name, info.version, info.license)
|
|
succeeded++
|
|
} else {
|
|
log.Printf("ERR: \"%s@%s\" (%s) %s does not match whitelist", info.name, info.version, info.license, info.spdx)
|
|
failed++
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Perform a text classification
|
|
confidence, err := info.Classify()
|
|
if err != nil {
|
|
log.Printf("ERR: %q (%s) %v", info.name, info.license, err)
|
|
failed++
|
|
continue
|
|
}
|
|
if confidence < threshold {
|
|
log.Printf("WARN: %q (%s) has low matching confidence (%.2f%%)", info.name, info.license, confidence)
|
|
warn++
|
|
continue
|
|
}
|
|
if verbose {
|
|
log.Printf("OK: %q (%s) (%.2f%%)", info.name, info.license, confidence)
|
|
}
|
|
succeeded++
|
|
}
|
|
if verbose {
|
|
log.Printf("Checked %d licenses (%d ignored lines):", len(packageInfos), ignored)
|
|
log.Printf(" %d successful", succeeded)
|
|
log.Printf(" %d low confidence", warn)
|
|
log.Printf(" %d errors", failed)
|
|
}
|
|
|
|
if failed > 0 {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|