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>
diff --git a/util/relicense.go b/util/relicense.go new file mode 100644 index 0000000..288f9e5 --- /dev/null +++ b/util/relicense.go
@@ -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.`