diff --git a/main.go b/main.go index 143b797..10be4ec 100644 --- a/main.go +++ b/main.go @@ -49,6 +49,7 @@ Flags: var ( skipExtensionFlags skipExtensionFlag + spdx spdxFlag holder = flag.String("c", "Google LLC", "copyright holder") license = flag.String("l", "apache", "license type: apache, bsd, mit, mpl") @@ -64,6 +65,7 @@ func init() { flag.PrintDefaults() } 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 @@ -77,6 +79,29 @@ func (i *skipExtensionFlag) Set(value string) error { 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() { flag.Parse() if flag.NArg() == 0 { @@ -84,12 +109,18 @@ func main() { os.Exit(1) } + // map legacy license values + if t, ok := legacyLicenseTypes[*license]; ok { + *license = t + } + data := licenseData{ Year: *year, Holder: *holder, + SPDXID: *license, } - tpl, err := fetchTemplate(*license, *licensef) + tpl, err := fetchTemplate(*license, *licensef, spdx) if err != nil { log.Fatal(err) } @@ -300,5 +331,6 @@ func hasLicense(b []byte) bool { n = len(b) } 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")) } diff --git a/tmpl.go b/tmpl.go index 47dda73..e12bcc8 100644 --- a/tmpl.go +++ b/tmpl.go @@ -25,25 +25,36 @@ import ( ) var licenseTemplate = map[string]string{ - "apache": tmplApache, - "mit": tmplMIT, - "bsd": tmplBSD, - "mpl": tmplMPL, + "Apache-2.0": tmplApache, + "MIT": tmplMIT, + "bsd": tmplBSD, + "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. type licenseData struct { Year string // Copyright year(s). Holder string // Name of the copyright holder. + SPDXID string // SPDX Identifier } // fetchTemplate returns the license template for the specified license and // optional templateFile. If templateFile is provided, the license is read // from the specified file. Otherwise, a template is loaded for the specified // license, if recognized. -func fetchTemplate(license string, templateFile string) (string, error) { +func fetchTemplate(license string, templateFile string, spdx spdxFlag) (string, error) { var t string - if templateFile != "" { + if spdx == spdxOnly { + t = tmplSPDX + } else if templateFile != "" { d, err := ioutil.ReadFile(templateFile) if err != nil { return "", fmt.Errorf("license file: %w", err) @@ -53,7 +64,15 @@ func fetchTemplate(license string, templateFile string) (string, error) { } else { t = licenseTemplate[license] 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 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/.` + +const tmplSPDX = `{{ if and .Year .Holder }}Copyright {{.Year}} {{.Holder}} +{{ end }}SPDX-License-Identifier: {{.SPDXID}}` + +const spdxSuffix = "\n\nSPDX-License-Identifier: {{.SPDXID}}" diff --git a/tmpl_test.go b/tmpl_test.go index be156fd..4ed972c 100644 --- a/tmpl_test.go +++ b/tmpl_test.go @@ -31,16 +31,19 @@ func init() { func TestFetchTemplate(t *testing.T) { tests := []struct { - description string // test case description - license string // license passed to fetchTemplate - templateFile string // templatefile passed to fetchTemplate - wantTemplate string // expected returned template - wantErr error // expected returned error + description string // test case description + license string // license passed to fetchTemplate + templateFile string // templatefile passed to fetchTemplate + spdx spdxFlag // spdx value passed to fetchTemplate + wantTemplate string // expected returned template + wantErr error // expected returned error }{ + // custom template files { "non-existant template file", "", "/does/not/exist", + spdxOff, "", os.ErrNotExist, }, @@ -48,27 +51,34 @@ func TestFetchTemplate(t *testing.T) { "custom template file", "", "testdata/custom.tpl", + spdxOff, "Copyright {{.Year}} {{.Holder}}\n\nCustom License Template\n", nil, }, + { "unknown license", "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", + "Apache-2.0", "", + spdxOff, tmplApache, nil, }, { "mit license template", - "mit", + "MIT", "", + spdxOff, tmplMIT, nil, }, @@ -76,21 +86,49 @@ func TestFetchTemplate(t *testing.T) { "bsd license template", "bsd", "", + spdxOff, tmplBSD, nil, }, { "mpl license template", - "mpl", + "MPL-2.0", "", + spdxOff, tmplMPL, 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 { 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())) { t.Fatalf("fetchTemplate(%q, %q) returned error: %#v, want %#v", tt.license, tt.templateFile, err, tt.wantErr) }