add support for SPDX license headers

This adds a new "-s" flag that will append an SPDX-License-Identifier
line to license headers.  If "-s=only" is specified, then only the SPDX
identifier will be used.

This also changes the "-l" flag to use SPDX identifiers, with mappings
to support the legacy "apache", "mit", and "mpl" values.  Together with
the "-s" flag, this allows SPDX headers for any arbitrary license type
to be added to files.

Co-authored-by: Bob Callaway <bcallawa@redhat.com>
This commit is contained in:
Will Norris
2021-07-26 20:26:45 -07:00
parent c2fdf83882
commit 97ae522f98
3 changed files with 113 additions and 19 deletions

36
main.go
View File

@@ -49,6 +49,7 @@ Flags:
var ( var (
skipExtensionFlags skipExtensionFlag skipExtensionFlags skipExtensionFlag
spdx spdxFlag
holder = flag.String("c", "Google LLC", "copyright holder") holder = flag.String("c", "Google LLC", "copyright holder")
license = flag.String("l", "apache", "license type: apache, bsd, mit, mpl") license = flag.String("l", "apache", "license type: apache, bsd, mit, mpl")
@@ -64,6 +65,7 @@ func init() {
flag.PrintDefaults() flag.PrintDefaults()
} }
flag.Var(&skipExtensionFlags, "skip", "To skip files to check/add the header file, for example: -skip rb -skip go") flag.Var(&skipExtensionFlags, "skip", "To skip files to check/add the header file, for example: -skip rb -skip go")
flag.Var(&spdx, "s", "Include SPDX identifier in license header. Set -s=only to only include SPDX identifier.")
} }
type skipExtensionFlag []string type skipExtensionFlag []string
@@ -77,6 +79,29 @@ func (i *skipExtensionFlag) Set(value string) error {
return nil return nil
} }
// spdxFlag defines the line flag behavior for specifying SPDX support.
type spdxFlag string
const (
spdxOff spdxFlag = ""
spdxOn spdxFlag = "true" // value set by flag package on bool flag
spdxOnly spdxFlag = "only"
)
// IsBoolFlag causes a bare '-s' flag to be set as the string 'true'. This
// allows the use of the bare '-s' or setting a string '-s=only'.
func (i *spdxFlag) IsBoolFlag() bool { return true }
func (i *spdxFlag) String() string { return string(*i) }
func (i *spdxFlag) Set(value string) error {
v := spdxFlag(value)
if v != spdxOn && v != spdxOnly {
return fmt.Errorf("error: flag 's' expects '%v' or '%v'", spdxOn, spdxOnly)
}
*i = v
return nil
}
func main() { func main() {
flag.Parse() flag.Parse()
if flag.NArg() == 0 { if flag.NArg() == 0 {
@@ -84,12 +109,18 @@ func main() {
os.Exit(1) os.Exit(1)
} }
// map legacy license values
if t, ok := legacyLicenseTypes[*license]; ok {
*license = t
}
data := licenseData{ data := licenseData{
Year: *year, Year: *year,
Holder: *holder, Holder: *holder,
SPDXID: *license,
} }
tpl, err := fetchTemplate(*license, *licensef) tpl, err := fetchTemplate(*license, *licensef, spdx)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@@ -300,5 +331,6 @@ func hasLicense(b []byte) bool {
n = len(b) n = len(b)
} }
return bytes.Contains(bytes.ToLower(b[:n]), []byte("copyright")) || return bytes.Contains(bytes.ToLower(b[:n]), []byte("copyright")) ||
bytes.Contains(bytes.ToLower(b[:n]), []byte("mozilla public")) bytes.Contains(bytes.ToLower(b[:n]), []byte("mozilla public")) ||
bytes.Contains(bytes.ToLower(b[:n]), []byte("SPDX-License-Identifier"))
} }

38
tmpl.go
View File

@@ -25,25 +25,36 @@ import (
) )
var licenseTemplate = map[string]string{ var licenseTemplate = map[string]string{
"apache": tmplApache, "Apache-2.0": tmplApache,
"mit": tmplMIT, "MIT": tmplMIT,
"bsd": tmplBSD, "bsd": tmplBSD,
"mpl": tmplMPL, "MPL-2.0": tmplMPL,
}
// maintain backwards compatibility by mapping legacy license types to their
// SPDX equivalents.
var legacyLicenseTypes = map[string]string{
"apache": "Apache-2.0",
"mit": "MIT",
"mpl": "MPL-2.0",
} }
// licenseData specifies the data used to fill out a license template. // licenseData specifies the data used to fill out a license template.
type licenseData struct { type licenseData struct {
Year string // Copyright year(s). Year string // Copyright year(s).
Holder string // Name of the copyright holder. Holder string // Name of the copyright holder.
SPDXID string // SPDX Identifier
} }
// fetchTemplate returns the license template for the specified license and // fetchTemplate returns the license template for the specified license and
// optional templateFile. If templateFile is provided, the license is read // optional templateFile. If templateFile is provided, the license is read
// from the specified file. Otherwise, a template is loaded for the specified // from the specified file. Otherwise, a template is loaded for the specified
// license, if recognized. // license, if recognized.
func fetchTemplate(license string, templateFile string) (string, error) { func fetchTemplate(license string, templateFile string, spdx spdxFlag) (string, error) {
var t string var t string
if templateFile != "" { if spdx == spdxOnly {
t = tmplSPDX
} else if templateFile != "" {
d, err := ioutil.ReadFile(templateFile) d, err := ioutil.ReadFile(templateFile)
if err != nil { if err != nil {
return "", fmt.Errorf("license file: %w", err) return "", fmt.Errorf("license file: %w", err)
@@ -53,7 +64,15 @@ func fetchTemplate(license string, templateFile string) (string, error) {
} else { } else {
t = licenseTemplate[license] t = licenseTemplate[license]
if t == "" { if t == "" {
return "", fmt.Errorf("unknown license: %q", license) if spdx == spdxOn {
// unknown license, but SPDX headers requested
t = tmplSPDX
} else {
return "", fmt.Errorf("unknown license: %q. Include the '-s' flag to request SPDX style headers using this license.", license)
}
} else if spdx == spdxOn {
// append spdx headers to recognized license
t = t + spdxSuffix
} }
} }
@@ -122,3 +141,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
const tmplMPL = `This Source Code Form is subject to the terms of the Mozilla Public const tmplMPL = `This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.` file, You can obtain one at https://mozilla.org/MPL/2.0/.`
const tmplSPDX = `{{ if and .Year .Holder }}Copyright {{.Year}} {{.Holder}}
{{ end }}SPDX-License-Identifier: {{.SPDXID}}`
const spdxSuffix = "\n\nSPDX-License-Identifier: {{.SPDXID}}"

View File

@@ -31,16 +31,19 @@ func init() {
func TestFetchTemplate(t *testing.T) { func TestFetchTemplate(t *testing.T) {
tests := []struct { tests := []struct {
description string // test case description description string // test case description
license string // license passed to fetchTemplate license string // license passed to fetchTemplate
templateFile string // templatefile passed to fetchTemplate templateFile string // templatefile passed to fetchTemplate
wantTemplate string // expected returned template spdx spdxFlag // spdx value passed to fetchTemplate
wantErr error // expected returned error wantTemplate string // expected returned template
wantErr error // expected returned error
}{ }{
// custom template files
{ {
"non-existant template file", "non-existant template file",
"", "",
"/does/not/exist", "/does/not/exist",
spdxOff,
"", "",
os.ErrNotExist, os.ErrNotExist,
}, },
@@ -48,27 +51,34 @@ func TestFetchTemplate(t *testing.T) {
"custom template file", "custom template file",
"", "",
"testdata/custom.tpl", "testdata/custom.tpl",
spdxOff,
"Copyright {{.Year}} {{.Holder}}\n\nCustom License Template\n", "Copyright {{.Year}} {{.Holder}}\n\nCustom License Template\n",
nil, nil,
}, },
{ {
"unknown license", "unknown license",
"unknown", "unknown",
"", "",
spdxOff,
"", "",
errors.New(`unknown license: "unknown"`), errors.New(`unknown license: "unknown". Include the '-s' flag to request SPDX style headers using this license.`),
}, },
// pre-defined license templates, no SPDX
{ {
"apache license template", "apache license template",
"apache", "Apache-2.0",
"", "",
spdxOff,
tmplApache, tmplApache,
nil, nil,
}, },
{ {
"mit license template", "mit license template",
"mit", "MIT",
"", "",
spdxOff,
tmplMIT, tmplMIT,
nil, nil,
}, },
@@ -76,21 +86,49 @@ func TestFetchTemplate(t *testing.T) {
"bsd license template", "bsd license template",
"bsd", "bsd",
"", "",
spdxOff,
tmplBSD, tmplBSD,
nil, nil,
}, },
{ {
"mpl license template", "mpl license template",
"mpl", "MPL-2.0",
"", "",
spdxOff,
tmplMPL, tmplMPL,
nil, nil,
}, },
// SPDX variants
{
"apache license template with SPDX added",
"Apache-2.0",
"",
spdxOn,
tmplApache + spdxSuffix,
nil,
},
{
"apache license template with SPDX only",
"Apache-2.0",
"",
spdxOnly,
tmplSPDX,
nil,
},
{
"unknown license with SPDX only",
"unknown",
"",
spdxOnly,
tmplSPDX,
nil,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) { t.Run(tt.description, func(t *testing.T) {
tpl, err := fetchTemplate(tt.license, tt.templateFile) tpl, err := fetchTemplate(tt.license, tt.templateFile, tt.spdx)
if tt.wantErr != nil && (err == nil || (!errors.Is(err, tt.wantErr) && err.Error() != tt.wantErr.Error())) { if tt.wantErr != nil && (err == nil || (!errors.Is(err, tt.wantErr) && err.Error() != tt.wantErr.Error())) {
t.Fatalf("fetchTemplate(%q, %q) returned error: %#v, want %#v", tt.license, tt.templateFile, err, tt.wantErr) t.Fatalf("fetchTemplate(%q, %q) returned error: %#v, want %#v", tt.license, tt.templateFile, err, tt.wantErr)
} }