cmd/tools/copyright/licensegen.go (215 lines of code) (raw):

// Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. package main import ( "bufio" "errors" "flag" "fmt" "io/ioutil" "os" "path/filepath" "strings" ) type ( // task that adds license header to source // files, if they don't already exist addLicenseHeaderTask struct { license string // license header string to add config *config // root directory of the project source } // command line config params config struct { rootDir string verifyOnly bool temporalAddMode bool temporalModifyMode bool filePaths string } ) // licenseFileName is the name of the license file const licenseFileName = "LICENSE" // unique prefix that identifies a license header const licenseHeaderPrefixOld = "Copyright (c)" const licenseHeaderPrefix = "// The MIT License (MIT)" const cadenceCopyright = "// Copyright (c) 2017-2020 Uber Technologies Inc." const cadenceModificationHeader = "// Modifications Copyright (c) 2020 Uber Technologies Inc." const temporalCopyright = "// Copyright (c) 2020 Temporal Technologies, Inc." const temporalPartialCopyright = "// Portions of the Software are attributed to Copyright (c) 2020 Temporal Technologies Inc." const firstLinesToCheck = 10 var ( // directories to be excluded dirDenylist = []string{"vendor/"} // default perms for the newly created files defaultFilePerms = os.FileMode(0644) ) // command line utility that adds license header // to the source files. Usage as follows: // // ./cmd/tools/copyright/licensegen.go func main() { var cfg config flag.StringVar(&cfg.rootDir, "rootDir", ".", "project root directory") flag.BoolVar(&cfg.verifyOnly, "verifyOnly", false, "don't automatically add headers, just verify all files") flag.BoolVar(&cfg.temporalAddMode, "temporalAddMode", false, "add copyright for new file copied from temporal") flag.BoolVar(&cfg.temporalModifyMode, "temporalModifyMode", false, "add copyright for existing file which has parts copied from temporal") flag.StringVar(&cfg.filePaths, "filePaths", "", "comma separated list of files to run temporal license on") flag.Parse() if err := verifyCfg(cfg); err != nil { fmt.Println(err) os.Exit(-1) } task := newAddLicenseHeaderTask(&cfg) if err := task.run(); err != nil { fmt.Println(err) os.Exit(-1) } } func verifyCfg(cfg config) error { if cfg.verifyOnly { if cfg.temporalModifyMode || cfg.temporalAddMode { return errors.New("invalid config, can only specify one of temporalAddMode, temporalModifyMode or verifyOnly") } } if cfg.temporalAddMode && cfg.temporalModifyMode { return errors.New("invalid config, can only specify temporalAddMode or temporalModifyMode") } if (cfg.temporalModifyMode || cfg.temporalAddMode) && len(cfg.filePaths) == 0 { return errors.New("invalid config, when running in temporalAddMode or temporalModifyMode must provide filePaths") } return nil } func newAddLicenseHeaderTask(cfg *config) *addLicenseHeaderTask { return &addLicenseHeaderTask{ config: cfg, } } func (task *addLicenseHeaderTask) run() error { data, err := ioutil.ReadFile(task.config.rootDir + "/" + licenseFileName) if err != nil { return fmt.Errorf("error reading license file, errr=%v", err.Error()) } task.license, err = commentOutLines(string(data)) if err != nil { return fmt.Errorf("copyright header failed to comment out lines, err=%v", err.Error()) } if task.config.temporalAddMode { task.license = fmt.Sprintf("%v\n\n%v\n\n%v", cadenceModificationHeader, temporalCopyright, task.license) } else if task.config.temporalModifyMode { task.license = fmt.Sprintf("%v\n\n%v\n\n%v", cadenceCopyright, temporalPartialCopyright, task.license) } if task.config.temporalModifyMode || task.config.temporalAddMode { filePaths, fileInfos, err := getFilePaths(task.config.filePaths) if err != nil { return err } for i := 0; i < len(filePaths); i++ { if err := task.handleFile(filePaths[i], fileInfos[i], nil); err != nil { return err } } return nil } task.license = fmt.Sprintf("%v\n\n%v\n\n%v", licenseHeaderPrefix, cadenceCopyright, task.license) err = filepath.Walk(task.config.rootDir, task.handleFile) if err != nil { return fmt.Errorf("copyright header check failed, err=%v", err.Error()) } return nil } func (task *addLicenseHeaderTask) handleFile(path string, fileInfo os.FileInfo, err error) error { if err != nil { return err } if fileInfo.IsDir() { if strings.HasPrefix(fileInfo.Name(), "_vendor-") || fileInfo.Name() == ".build" || fileInfo.Name() == ".bin" { return filepath.SkipDir } return nil } if !mustProcessPath(path) { return nil } if !strings.HasSuffix(fileInfo.Name(), ".go") && !strings.HasSuffix(fileInfo.Name(), ".proto") { return nil } // Used as part of the cli to write licence headers on files, does not use user supplied input so marked as nosec // #nosec f, err := os.Open(path) if err != nil { return err } ok, err := hasCopyright(f) if err != nil { return err } if err := f.Close(); err != nil { return err } if ok { if task.config.temporalModifyMode || task.config.temporalAddMode { return fmt.Errorf("when running in temporalModifyMode or temporalAddMode please first remove existing license header: %v", path) } return nil } // at this point, src file is missing the header if task.config.verifyOnly { if !isFileAutogenerated(path) { return fmt.Errorf("%v missing license header", path) } } // Used as part of the cli to write licence headers on files, does not use user supplied input so marked as nosec // #nosec data, err := ioutil.ReadFile(path) if err != nil { return err } return ioutil.WriteFile(path, []byte(task.license+string(data)), defaultFilePerms) } func hasCopyright(f *os.File) (bool, error) { scanner := bufio.NewScanner(f) lineSuccess := scanner.Scan() if !lineSuccess { return false, fmt.Errorf("fail to read first line of file %v", f.Name()) } i := 0 for i < firstLinesToCheck && lineSuccess { i++ line := strings.TrimSpace(scanner.Text()) if err := scanner.Err(); err != nil { return false, err } if lineHasCopyright(line) { return true, nil } lineSuccess = scanner.Scan() } return false, nil } func isFileAutogenerated(path string) bool { return strings.HasPrefix(path, ".gen") } func mustProcessPath(path string) bool { for _, d := range dirDenylist { if strings.HasPrefix(path, d) { return false } } return true } func commentOutLines(str string) (string, error) { var lines []string scanner := bufio.NewScanner(strings.NewReader(str)) for scanner.Scan() { text := scanner.Text() if text == "" { // do not add trailing whitespace, gofmt / goimports removes it lines = append(lines, "//\n") } else { lines = append(lines, "// "+scanner.Text()+"\n") } } lines = append(lines, "\n") if err := scanner.Err(); err != nil { return "", err } return strings.Join(lines, ""), nil } func lineHasCopyright(line string) bool { return strings.Contains(line, licenseHeaderPrefixOld) || strings.Contains(line, licenseHeaderPrefix) } func getFilePaths(filePaths string) ([]string, []os.FileInfo, error) { paths := strings.Split(filePaths, ",") var fileInfos []os.FileInfo for _, p := range paths { fileInfo, err := os.Stat(p) if err != nil { return nil, nil, err } fileInfos = append(fileInfos, fileInfo) } return paths, fileInfos, nil }