Raw File
main.go
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package main

import (
	"fmt"
	"go/ast"
	"go/format"
	"go/parser"
	"go/token"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

const (
	usage = `spdxconv is a tool for converting Cilium's copyright and license header
to a SPDX compliant one

Usage:
    spdxconv [package_dir] - provide spdxconv with a absolute path to the go package you'd like to convert.

`
)

var (
	// RegexMap maps an identifying regexp expression
	// with the SPDX string that replaces it.
	RegexpMap = map[*regexp.Regexp]string{
		regexp.MustCompile(`(?m)Apache License, Version 2.0`): "// SPDX-License-Identifier: Apache-2.0",
	}
)

type SPDXConverter struct {
	// the root directory to begin replacing
	// license clauses to spdx clauses
	rootDir string
}

// Walk will begin walking the tree rooted at
// rootDir.
//
// For each .go file it encounters it will parse
// that file's AST and discover the comments.
//
// If the comments indicate a non spdx license
// it will convert it to one and write the file back
// to disk.
func (s *SPDXConverter) Walk() error {
	err := filepath.WalkDir(s.rootDir, func(path string, d os.DirEntry, e error) error {
		if e != nil {
			return e
		}
		if filepath.Ext(path) != ".go" {
			return nil
		}
		fs := token.NewFileSet()
		astFd, err := parser.ParseFile(fs, path, nil, parser.ParseComments)
		if err != nil {
			return fmt.Errorf("error while parsing %v: %w", path, err)
		}

		err = s.conv(path, fs, astFd)
		if err != nil {
			return err
		}

		return nil
	})
	return err
}

func (s *SPDXConverter) conv(path string, fset *token.FileSet, f *ast.File) error {
	if len(f.Comments) == 0 {
		return nil
	}
	cg := f.Comments[0]
	if !strings.HasPrefix(cg.List[0].Text, "// Copyright") {
		log.Printf("file %v did not start with a Copyright string. skipping...", path)
		return nil
	}
	var spdx string
	for exp, tmp := range RegexpMap {
		if exp.MatchString(cg.Text()) {
			spdx = tmp
		}
	}
	if spdx == "" {
		log.Printf("could not determine license for %v", path)
		return nil
	}

	copyRight := cg.List[0].Text
	cg.List[0].Text, cg.List[1].Text = spdx, copyRight
	cg.List = cg.List[:2]

	fd, err := os.OpenFile(path, os.O_TRUNC|os.O_RDWR, 0660)
	if err != nil {
		return fmt.Errorf("failed to open original source file for conversion: %w", err)
	}
	defer fd.Close()

	err = format.Node(fd, fset, f)
	if err != nil {
		return fmt.Errorf("failed to write converted file %v: %w", path, err)
	}

	return nil
}

func main() {
	switch {
	case len(os.Args) != 2:
		fmt.Print(usage)
		os.Exit(1)
	case os.Args[1] == "help":
		fmt.Print(usage)
		os.Exit(0)
	}

	path := os.Args[1]
	dir, err := os.Stat(path)
	if err != nil {
		fmt.Printf("provided path %v could not be opened: %v", path, err)
		os.Exit(1)
	}
	if !dir.IsDir() {
		fmt.Println("provided path is not a directory")
		os.Exit(1)
	}

	conv := &SPDXConverter{
		rootDir: path,
	}
	if err := conv.Walk(); err != nil {
		log.Fatal(err.Error())
	}
}
back to top