blob: 35ecc211c4328b918835440dfd31e4b8f82e0fcb [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// TODO(jsimsa):
// - Add support for shell files without the .sh suffix.
// - Add support for Makefiles.
// - Decide what to do with the contents of the testdata directory.
// The following enables go generate to generate the doc.go file.
//go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go -env=CMDLINE_PREFIX=jiri .
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"v.io/jiri"
"v.io/jiri/collect"
"v.io/jiri/gitutil"
"v.io/jiri/project"
"v.io/jiri/runutil"
"v.io/jiri/tool"
"v.io/x/devtools/tooldata"
"v.io/x/lib/cmdline"
)
func init() {
tool.InitializeProjectFlags(&cmdCopyright.Flags)
tool.InitializeRunFlags(&cmdCopyright.Flags)
}
const (
defaultFileMode = os.FileMode(0644)
hashbang = "#!"
jiriIgnore = ".jiriignore"
)
var (
copyrightRE = regexp.MustCompile(`^Copyright [[:digit:]]* The Vanadium Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
$`)
)
type copyrightAssets struct {
Copyright string
MatchFiles map[string]string
MatchPrefixFiles map[string]string
}
type languageSpec struct {
CommentPrefix string
CommentSuffix string
Interpreters map[string]struct{}
FileExtension string
}
var languages map[string]languageSpec = map[string]languageSpec{
"c_source": languageSpec{
CommentPrefix: "// ",
FileExtension: ".c",
},
"css": languageSpec{
CommentPrefix: "/* ",
CommentSuffix: " */",
FileExtension: ".css",
},
"dart": languageSpec{
CommentPrefix: "// ",
FileExtension: ".dart",
},
"go": languageSpec{
CommentPrefix: "// ",
FileExtension: ".go",
},
"c_header": languageSpec{
CommentPrefix: "// ",
FileExtension: ".h",
},
"java": languageSpec{
CommentPrefix: "// ",
FileExtension: ".java",
},
"javascript": languageSpec{
CommentPrefix: "// ",
FileExtension: ".js",
},
"mojom": languageSpec{
CommentPrefix: "// ",
FileExtension: ".mojom",
},
"shell": languageSpec{
CommentPrefix: "# ",
FileExtension: ".sh",
Interpreters: map[string]struct{}{
"bash": struct{}{},
"sh": struct{}{},
},
},
"vdl": languageSpec{
CommentPrefix: "// ",
FileExtension: ".vdl",
},
}
// cmdCopyright represents the "jiri copyright" command.
var cmdCopyright = &cmdline.Command{
Name: "copyright",
Short: "Manage vanadium copyright",
Long: `
This command can be used to check if all source code files of Vanadium
projects contain the appropriate copyright header and also if all
projects contains the appropriate licensing files. Optionally, the
command can be used to fix the appropriate copyright headers and
licensing files.
In order to ignore checked in third-party assets which have their own copyright
and licensing headers a ".jiriignore" file can be added to a project. The
".jiriignore" file is expected to contain a single regular expression pattern per
line.
`,
Children: []*cmdline.Command{cmdCopyrightCheck, cmdCopyrightFix},
}
// cmdCopyrightCheck represents the "jiri copyright check" command.
var cmdCopyrightCheck = &cmdline.Command{
Runner: jiri.RunnerFunc(runCopyrightCheck),
Name: "check",
Short: "Check copyright headers and licensing files",
Long: "Check copyright headers and licensing files.",
ArgsName: "<projects>",
ArgsLong: "<projects> is a list of projects to check.",
}
func runCopyrightCheck(jirix *jiri.X, args []string) error {
return copyrightHelper(jirix, args, false)
}
// cmdCopyrightFix represents the "jiri copyright fix" command.
var cmdCopyrightFix = &cmdline.Command{
Runner: jiri.RunnerFunc(runCopyrightFix),
Name: "fix",
Short: "Fix copyright headers and licensing files",
Long: "Fix copyright headers and licensing files.",
ArgsName: "<projects>",
ArgsLong: "<projects> is a list of projects to fix.",
}
func runCopyrightFix(jirix *jiri.X, args []string) error {
return copyrightHelper(jirix, args, true)
}
// copyrightHelper implements the logic of "jiri copyright {check,fix}".
func copyrightHelper(jirix *jiri.X, args []string, fix bool) error {
dataDir, err := tooldata.DataDirPath(jirix, "jiri")
if err != nil {
return err
}
assets, err := loadAssets(jirix, dataDir)
if err != nil {
return err
}
config, err := tooldata.LoadConfig(jirix)
if err != nil {
return err
}
projects, err := project.ParseNames(jirix, args, config.CopyrightCheckProjects())
if err != nil {
return err
}
missingCopyright := false
for _, project := range projects {
if missing, err := checkProject(jirix, project, assets, fix); err != nil {
return err
} else {
if missing {
missingCopyright = true
}
}
}
if !fix && missingCopyright {
return fmt.Errorf("missing copyright")
}
return nil
}
// createComment creates a copyright header comment out of the given
// comment symbol and copyright header data.
func createComment(prefix, suffix, header string) string {
return prefix + strings.Replace(header, "\n", suffix+"\n"+prefix, -1) + suffix + "\n\n"
}
// checkFile checks that the given file contains the appropriate
// copyright header.
func checkFile(jirix *jiri.X, path string, assets *copyrightAssets, fix bool) (bool, error) {
// Some projects contain third-party files in a "third_party" subdir.
// Skip such files for the same reason that we skip the third_party project.
if strings.Contains(path, string(filepath.Separator)+"third_party"+string(filepath.Separator)) {
return false, nil
}
// Peak at the first line of the file looking for the interpreter
// directive (e.g. #!/bin/bash).
interpreter, err := detectInterpreter(jirix, path)
if err != nil {
return false, err
}
missingCopyright := false
s := jirix.NewSeq()
for _, lang := range languages {
if _, ok := lang.Interpreters[filepath.Base(interpreter)]; ok || strings.HasSuffix(path, lang.FileExtension) {
data, err := s.ReadFile(path)
if err != nil {
return false, err
}
if !hasCopyright(data, lang.CommentPrefix, lang.CommentSuffix) {
if fix {
copyright := createComment(lang.CommentPrefix, lang.CommentSuffix, assets.Copyright)
// Add the copyright header to the beginning of the file.
if interpreter != "" {
// Handle the interpreter directive.
directiveLine := hashbang + interpreter + "\n"
data = bytes.TrimPrefix(data, []byte(directiveLine))
copyright = directiveLine + copyright
}
data := append([]byte(copyright), data...)
info, err := s.Stat(path)
if err != nil {
return false, err
}
if err := s.WriteFile(path, data, info.Mode()).Done(); err != nil {
return false, err
}
} else {
missingCopyright = true
fmt.Fprintf(jirix.Stderr(), "%v copyright is missing\n", path)
}
}
}
}
return missingCopyright, nil
}
// checkProject checks that the given project contains the appropriate
// licensing files and that its source code files contain the
// appropriate copyright header. If the fix option is set, the
// function fixes up the project. Otherwise, the function reports
// violations to standard error output.
func checkProject(jirix *jiri.X, project project.Project, assets *copyrightAssets, fix bool) (m bool, e error) {
check := func(fileMap map[string]string, isValid func([]byte, []byte) bool) (bool, error) {
s := jirix.NewSeq()
missing := false
for file, want := range fileMap {
path := filepath.Join(project.Path, file)
got, err := s.ReadFile(path)
if err != nil {
if runutil.IsNotExist(err) {
if fix {
if err := s.WriteFile(path, []byte(want), defaultFileMode).Done(); err != nil {
return false, err
}
} else {
fmt.Fprintf(jirix.Stderr(), "%v is missing\n", path)
missing = true
}
continue
} else {
return false, err
}
}
if !isValid(got, []byte(want)) {
if fix {
if err := s.WriteFile(path, []byte(want), defaultFileMode).Done(); err != nil {
return false, err
}
} else {
fmt.Fprintf(jirix.Stderr(), "%v is not up-to-date\n", path)
missing = true
}
}
}
return missing, nil
}
missing := false
// Check the licensing files that require an exact match.
if missingLicense, err := check(assets.MatchFiles, bytes.Equal); err != nil {
return false, err
} else {
if missingLicense {
missing = true
}
}
// Check the licensing files that require a prefix match.
if missingLicense, err := check(assets.MatchPrefixFiles, bytes.HasPrefix); err != nil {
return false, err
} else {
if missingLicense {
missing = true
}
}
// Check the source code files.
cwd, err := os.Getwd()
if err != nil {
return false, fmt.Errorf("Getwd() failed: %v", err)
}
if err := jirix.NewSeq().Chdir(project.Path).Done(); err != nil {
return false, err
}
defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
files, err := gitutil.New(jirix.NewSeq()).TrackedFiles()
if err != nil {
return false, err
}
expressions, err := readV23Ignore(jirix, project)
if err != nil {
return false, err
}
for _, file := range files {
if ignore, err := isIgnored(file, expressions); err != nil {
return false, err
} else if !ignore {
if missingCopyright, err := checkFile(jirix, filepath.Join(project.Path, file), assets, fix); err != nil {
return missing, err
} else {
if missingCopyright {
missing = true
}
}
}
}
return missing, nil
}
// detectInterpret returns the interpreter directive of the given
// file, if it contains one.
func detectInterpreter(jirix *jiri.X, path string) (_ string, e error) {
file, err := jirix.NewSeq().Open(path)
if err != nil {
return "", err
}
defer collect.Error(file.Close, &e)
// Only consider the first 256 bytes to account for binary files
// with lines too long to fit into a memory buffer.
data := make([]byte, 256)
if _, err := file.Read(data); err != nil && err != io.EOF {
return "", fmt.Errorf("failed to read %v: %v", file.Name(), err)
}
scanner := bufio.NewScanner(bytes.NewBuffer(data))
scanner.Scan()
if err := scanner.Err(); err == nil {
line := scanner.Text()
if strings.HasPrefix(line, hashbang) {
return strings.TrimPrefix(line, hashbang), nil
}
return "", nil
} else {
return "", fmt.Errorf("failed to scan %v: %v", file.Name(), err)
}
}
// hasCopyright checks that the given byte slice contains the
// copyright header.
func hasCopyright(data []byte, prefix, suffix string) bool {
buffer := bytes.NewBuffer(data)
lines, nlines := "", 0
for nlines < 3 {
line, err := buffer.ReadString('\n')
if err != nil {
break
}
// Skip the interpreter directive (e.g. #!/bin/bash).
if strings.HasPrefix(line, hashbang) {
continue
}
lines += strings.TrimSuffix(strings.TrimPrefix(line, prefix), suffix+"\n") + "\n"
nlines++
}
return copyrightRE.MatchString(lines)
}
// loadAssets returns an in-memory representation of the copyright
// assets.
func loadAssets(jirix *jiri.X, dir string) (*copyrightAssets, error) {
result := copyrightAssets{
MatchFiles: map[string]string{},
MatchPrefixFiles: map[string]string{},
}
s := jirix.NewSeq()
load := func(files []string, fileMap map[string]string) error {
for _, file := range files {
path := filepath.Join(dir, file)
bytes, err := s.ReadFile(path)
if err != nil {
return err
}
fileMap[file] = string(bytes)
}
return nil
}
if err := load([]string{"LICENSE", "PATENTS", "VERSION"}, result.MatchFiles); err != nil {
return nil, err
}
if err := load([]string{"AUTHORS", "CONTRIBUTORS", "CONTRIBUTING.md"}, result.MatchPrefixFiles); err != nil {
return nil, err
}
path := filepath.Join(dir, "COPYRIGHT")
bytes, err := s.ReadFile(path)
if err != nil {
return nil, err
}
currentYear := strconv.Itoa(time.Now().Year())
result.Copyright = strings.Replace(string(bytes), "[YEAR]", currentYear, 1)
return &result, nil
}
// isIgnored checks a path against patterns extracted from the .jiriignore file.
func isIgnored(path string, expressions []*regexp.Regexp) (bool, error) {
for _, expression := range expressions {
if ok := expression.MatchString(path); ok {
return true, nil
}
}
// Skip copyright check for symlinks because the symlink target will
// already be checked if it is in the repo.
fi, err := os.Lstat(path)
return fi != nil && fi.Mode()&os.ModeSymlink != 0, err
}
func readV23Ignore(jirix *jiri.X, project project.Project) ([]*regexp.Regexp, error) {
// Grab the .jiriignore in from project.Path. Ignore file not found errors, not
// all projects will have one of these ignore files.
path := filepath.Join(project.Path, jiriIgnore)
file, err := jirix.NewSeq().Open(path)
if err != nil {
if !runutil.IsNotExist(err) {
return nil, err
}
return nil, nil
}
defer file.Close()
expressions := []*regexp.Regexp{}
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
// TODO(jsimsa): Consider implementing conventions similar to .gitignore (e.g.
// leading '/' implies the regular expression should start with "^").
re, err := regexp.Compile(line)
if err != nil {
return nil, fmt.Errorf("Compile(%v) failed: %v", line, err)
}
expressions = append(expressions, re)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("Scan() failed: %v", err)
}
return expressions, nil
}
func main() {
cmdline.Main(cmdCopyright)
}