blob: a21d84c4a9c60de335aa21e126191555aad94bdb [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package checklicenses
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime/trace"
"sort"
"strings"
"time"
"go.fuchsia.dev/fuchsia/tools/check-licenses/noticetxt"
"golang.org/x/sync/errgroup"
)
type UnlicensedFiles struct {
files []string
}
const exampleHeader = "# Copyright %d The Fuchsia Authors. All rights reserved.\n# Use of this source code is governed by a BSD-style license that can be\n# found in the LICENSE file."
// Run executes the license verification according to the provided config file.
func Run(ctx context.Context, config *Config) error {
var eg errgroup.Group
var err error
metrics := NewMetrics()
file_tree, err := NewFileTree(ctx, config.BaseDir, nil, config, metrics)
if err != nil {
return err
}
licenses, err := NewLicenses(ctx, config)
if err != nil {
return err
}
unlicensedFiles := &UnlicensedFiles{}
r := trace.StartRegion(ctx, "singleLicenseFile walk")
for tree := range file_tree.getFileTreeIterator() {
tree := tree
for licenseFile := range tree.SingleLicenseFiles {
licenseFile := licenseFile
eg.Go(func() error {
if err := processLicenseFile(licenseFile, metrics, licenses, config, tree); err != nil {
// error safe to ignore because eg. io.EOF means symlink hasn't been handled yet
// TODO(jcecil): Correctly skip symlink.
log.Printf("warning: %s. Skipping file: %s.\n", err, licenseFile)
}
return nil
})
}
}
eg.Wait()
r.End()
file_tree.propagateProjectLicenses(config)
r = trace.StartRegion(ctx, "processFlutterLicenses")
if err = processFlutterLicenses(licenses, config, metrics, file_tree); err != nil {
log.Printf("error processing flutter licenses: %v", err)
}
r.End()
r = trace.StartRegion(ctx, "processNoticeTxtFiles")
if err = processNoticeTxtFiles(licenses, config, metrics, file_tree); err != nil {
log.Printf("error processing NOTICE.txt files: %v", err)
}
r.End()
r = trace.StartRegion(ctx, "regular file walk")
for file := range file_tree.getFileIterator() {
file := file
eg.Go(func() error {
if err := processFile(file, metrics, licenses, unlicensedFiles, config); err != nil {
// TODO(jcecil): Correctly skip symlink and return errors.
log.Printf("warning: %s. Skipping file: %s.\n", err, file.Path)
}
return nil
})
}
eg.Wait()
r.End()
defer trace.StartRegion(ctx, "finalization").End()
if config.ExitOnProhibitedLicenseTypes {
filesWithProhibitedLicenses := licenses.GetFilesWithProhibitedLicenses()
if len(filesWithProhibitedLicenses) > 0 {
sort.Strings(filesWithProhibitedLicenses)
files := strings.Join(filesWithProhibitedLicenses, "\n")
return fmt.Errorf("Encountered prohibited license types. File paths are:\n\n%v\n\nPlease remove the offending files, or reach out to //tools/check-licenses/OWNERS for license exceptions or errors.", files)
}
}
year, _, _ := time.Now().Date()
header := fmt.Sprintf(exampleHeader, year)
if config.ExitOnUnlicensedFiles && len(unlicensedFiles.files) > 0 {
sort.Strings(unlicensedFiles.files)
files := strings.Join(unlicensedFiles.files, "\n")
return fmt.Errorf("Encountered files with licenses that are either malformed or missing. File paths are:\n\n%v\n\nPlease add license information to the headers of each file. If this is Fuchsia code (e.g. not in //prebuilt, //third_party, etc), paste this example header text into the top of each file (replacing '#' with the proper comment character for your file):\n\n%s\n\nReach out to //tools/check-licenses/OWNERS for file exceptions or errors.\n", files, header)
}
if config.ExitOnDirRestrictedLicense {
filesWithBadLicenseUsage := licenses.GetFilesWithBadLicenseUsage()
if len(filesWithBadLicenseUsage) > 0 {
sort.Strings(filesWithBadLicenseUsage)
files := strings.Join(filesWithBadLicenseUsage, "\n")
return fmt.Errorf("Encountered files with licenses that may not be used in those directories. File paths are:\n\n%v\n\nPlease remove the offending files, or reach out to //tools/check-licenses/OWNERS for license exceptions or errors.", files)
}
}
if config.OutputLicenseFile {
for _, extension := range config.OutputFileExtensions {
path := config.OutputFilePrefix + "." + extension
if err := saveToOutputFile(filepath.Join(config.OutDir, path), licenses, config); err != nil {
return err
}
}
}
metrics.print()
return nil
}
func processLicenseFile(path string, metrics *Metrics, licenses *Licenses, config *Config, file_tree *FileTree) error {
// For license files, we read the whole file.
// TODO: "traverse.go" shouldn't have to worry about file access.
// Read file data in file.go, and pass around the file object everywhere.
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
file, _ := NewFile(path, file_tree)
if contains(config.NoticeFiles, path) {
licenses.MatchNoticeFile(data, file.Path, metrics, file_tree)
} else {
licenses.MatchSingleLicenseFile(data, file.Path, metrics, file_tree)
}
return nil
}
func processFile(file *File, metrics *Metrics, licenses *Licenses, unlicensedFiles *UnlicensedFiles, config *Config) error {
file_tree := file.Parent
path := file.Path
log.Printf("visited file or dir: %q", path)
// TODO(omerlevran): Reuse the buffer.
data := make([]byte, config.MaxReadSize)
n, err := readFile(path, data)
if err != nil {
return err
}
data = data[:n]
isMatched, matchedLic, _ := licenses.MatchFile(data, file.Path, metrics)
if isMatched {
file.Licenses = append(file.Licenses, matchedLic)
} else {
if len(file_tree.SingleLicenseFiles) == 0 {
metrics.increment("num_unlicensed")
unlicensedFiles.files = append(unlicensedFiles.files, path)
log.Printf("File license: missing. Project license: missing. path: %s\n", path)
} else {
// If we find a LICENSE file but it doesn't match any of our license patterns,
// we should mark it as unlicensed.
foundMatchedLicense := false
for _, l := range file_tree.SingleLicenseFiles {
if len(l) > 0 {
foundMatchedLicense = true
break
}
}
if !foundMatchedLicense {
metrics.increment("num_unlicensed")
unlicensedFiles.files = append(unlicensedFiles.files, path)
log.Printf("File license: missing. Project license: missing. path: %s\n", path)
} else {
metrics.increment("num_with_project_license")
for _, matches := range file_tree.LicenseMatches {
for _, match := range matches {
match.Lock()
match.LicenseAppliesToFiles = append(match.LicenseAppliesToFiles, path)
match.Unlock()
}
}
log.Printf("File license: missing. Project license: exists. path: %s", path)
}
}
}
return nil
}
// readFile returns up to n bytes from the file.
func readFile(path string, d []byte) (int, error) {
f, err := os.Open(path)
if err != nil {
return 0, err
}
defer f.Close()
n, err := f.Read(d)
if err == io.EOF {
err = nil
}
return n, err
}
func processNoticeTxtFiles(licenses *Licenses, config *Config, metrics *Metrics, file_tree *FileTree) error {
for _, path := range config.NoticeTxtFiles {
file, _ := NewFile(path, file_tree)
data, err := noticetxt.ParseNoticeTxtFile(path)
if err != nil {
return err
}
for _, d := range data {
licenses.MatchSingleLicenseFile(d, file.Path, metrics, file_tree)
}
}
return nil
}