Add a tool to switch the license to Apache 2.0, matching OpenSSL

This is intended to automate most of the header rewrites.

Bug: 364634028
Change-Id: I9fa9a96c392d893330788412f17b84ccd9315c5b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/75851
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
This commit is contained in:
David Benjamin
2025-01-08 12:41:44 -05:00
committed by Boringssl LUCI CQ
parent bf381fd8bd
commit bc5a527fd6
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2025 The BoringSSL Authors
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//go:build ignore
// relicense.go rewrites the license headers of the files it is passed in. It is
// intended to be run as:
//
// git ls-tree -r --name-only HEAD | xargs go run ./util/relicense.go
package main
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
"slices"
"strings"
)
type commentStyle int
const (
commentStyleC commentStyle = iota
commentStyleHash
)
func lineComment(style commentStyle) string {
switch style {
case commentStyleC:
return "//"
case commentStyleHash:
return "#"
}
panic("unknown comment type")
}
func commentBlockLength(style commentStyle, lines []string) int {
if len(lines) == 0 {
return 0
}
if style == commentStyleC && strings.HasPrefix(lines[0], "/*") {
if idx := strings.Index(lines[0][2:], "*/"); idx >= 0 {
idx += 2
if idx+2 != len(lines[0]) {
// The comment does not reach the end of the line.
return 0
}
return 1
}
for i := 1; i < len(lines); i++ {
if idx := strings.Index(lines[i], "*/"); idx >= 0 {
if idx+2 != len(lines[i]) {
// The comment does not reach the end of the line.
return 0
}
return i + 1
}
}
// Could not find the end of the comment
return 0
}
// Treat consecutive line comments as
prefix := lineComment(style)
l := 0
for l < len(lines) && strings.HasPrefix(lines[l], prefix) {
l++
// Some of our Perl files do not include a blank line at the end of the
// license notice. Treat that as ending the comment block.
if strings.HasPrefix(lines[l-1], "# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE") {
break
}
if strings.HasPrefix(lines[l-1], "# https://www.openssl.org/source/license.html") {
break
}
if strings.HasPrefix(lines[l-1], "# found in the LICENSE file.") {
break
}
}
return l
}
func commentStyleForPath(path string) (commentStyle, error) {
for _, suffix := range []string{".c", ".cc", ".h", ".cc.inc", ".go", ".S", ".rs"} {
if strings.HasSuffix(path, suffix) {
return commentStyleC, nil
}
}
for _, suffix := range []string{".pl", ".py", ".peg", ".bazel", ".bzl", "/BUILD.toplevel", "/WORKSPACE.toplevel", ".txt", "/DEPS", ".cmake", ".sh", ".bazelrc", ".toml"} {
if strings.HasSuffix(path, suffix) {
return commentStyleHash, nil
}
}
return 0, errors.New("unknown comment style")
}
var licenseRegexp = regexp.MustCompile(`(?i)\b(copyright|authors?|licen[cs]e[ds]?|permission|warranty|warranties)\b`)
func findLicenseKeyword(lines []string) (string, bool) {
for _, line := range lines {
m := licenseRegexp.FindString(line)
if len(m) != 0 {
return m, true
}
}
return "", false
}
func process(path string) error {
inp, err := os.ReadFile(path)
if err != nil {
return err
}
// If the file does not currently have any license or copyright text,
// ignore it.
lines := strings.Split(string(inp), "\n")
if _, ok := findLicenseKeyword(lines); !ok {
return nil
}
// Clear a bunch of false positives so the remainder can be looked at by
// hand.
// gen files are generated and third_party should remain untouched.
if strings.HasPrefix(path, "gen/") || strings.HasPrefix(path, "third_party/") {
return nil
}
// Fuzzer corpora sometimes contain stray strings.
if strings.HasPrefix(path, "fuzz/") && strings.Contains(path, "_corpus") {
return nil
}
// These files do not have license headers but are false positives.
if slices.Contains([]string{"AUTHORS", "CONTRIBUTING.md", "LICENSE", "MODULE.bazel.lock"}, path) {
return nil
}
style, err := commentStyleForPath(path)
if err != nil {
return err
}
var b bytes.Buffer
// Copy over the #! line in perlasm files.
if style == commentStyleHash && len(lines) > 0 && strings.HasPrefix(lines[0], "#!") {
fmt.Fprintf(&b, "%s\n", lines[0])
lines = lines[1:]
}
// Copy over the coding= line in Python files.
if style == commentStyleHash && len(lines) > 0 && strings.HasPrefix(lines[0], "# coding=") {
fmt.Fprintf(&b, "%s\n", lines[0])
lines = lines[1:]
}
// Sometimes there is a blank line before the license.
if len(lines) > 0 && len(lines[0]) == 0 {
fmt.Fprintf(&b, "%s\n", lines[0])
lines = lines[1:]
}
// Look for the existing license header.
n := commentBlockLength(style, lines)
if n == 0 {
return errors.New("could not find comment block")
}
comment, lines := lines[:n], lines[n:]
// Trim comment markers from comment lines.
for i := range comment {
comment[i] = strings.TrimPrefix(comment[i], lineComment(style))
comment[i] = strings.TrimPrefix(comment[i], "/*")
comment[i] = strings.TrimSuffix(comment[i], "*/")
comment[i] = strings.TrimPrefix(comment[i], " *")
comment[i] = strings.TrimSpace(comment[i])
}
// Remove leading and trailing whitespace.
for len(comment) > 0 && len(comment[0]) == 0 {
comment = comment[1:]
}
for len(comment) > 0 && len(comment[len(comment)-1]) == 0 {
comment = comment[:len(comment)-1]
}
// Collect copyright lines.
for n = 0; n < len(comment) && (strings.HasPrefix(comment[n], "Copyright ") || strings.HasPrefix(comment[n], "Author:")); n++ {
}
copyright, comment := comment[:n], comment[n:]
if len(copyright) == 0 {
return errors.New("could not find copyright notices")
}
// Leave support code alone for now.
if strings.HasPrefix(path, "ssl/test/runner/") && strings.HasSuffix(copyright[0], "The Go Authors. All rights reserved.") {
return nil
}
if strings.HasPrefix(path, "util/bot/") && strings.HasSuffix(copyright[0], "The Chromium Authors. All rights reserved.") {
return nil
}
// The remainder must be one of the expected licenses.
license := strings.Join(comment, "\n")
if !slices.Contains(allowedLicenses, license) {
const maxLicenseBlock = 25
trunc := license
if len(trunc) > maxLicenseBlock {
trunc = trunc[:maxLicenseBlock] + "..."
}
return fmt.Errorf("license block %q unexpected", trunc)
}
// Assemble the new file contents.
for _, line := range copyright {
fmt.Fprintf(&b, "%s %s\n", lineComment(style), line)
}
for _, line := range strings.Split(apacheFileHeader, "\n") {
if len(line) == 0 {
fmt.Fprintf(&b, "%s\n", lineComment(style))
} else {
fmt.Fprintf(&b, "%s %s\n", lineComment(style), line)
}
}
b.WriteString(strings.Join(lines, "\n"))
if err := os.WriteFile(path, b.Bytes(), 0666); err != nil {
return err
}
// If any text after the header contains license keywords, warn that we need
// to check it by hand.
if keyword, ok := findLicenseKeyword(lines); ok {
return fmt.Errorf("file body contains %q, double-check by hand", keyword)
}
return nil
}
func main() {
for _, path := range os.Args[1:] {
if err := process(path); err != nil {
fmt.Fprintf(os.Stderr, "Error processing %q: %s\n", path, err)
}
}
}
var allowedLicenses = []string{
`
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.`,
`
Licensed under the OpenSSL license (the "License"). You may not use
this file except in compliance with the License. You can obtain a copy
in the file LICENSE in the source distribution or at
https://www.openssl.org/source/license.html`,
`Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.`,
`Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.`,
}
const apacheFileHeader = `
Licensed 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
https://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.`