blob: 4cf816b2a6f395f8fa331274886d88b1d0590601 [file] [log] [blame]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package genutil
import (
"bufio"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"go.chromium.org/tast/core/errors"
)
// Params controls the detailed behavior of GenerateConstants.
type Params struct {
PackageName string // Go package name, e.g. "input"
RepoName string // repo name, e.g "Linux kernel"
PreludeCode string // Go code to include at the top of file (typically "//go:generate go run ...")
CopyrightYear int // copyright year used in the license, e.g "2018"
MainGoFilePath string // name of the main .go file for generating the constants, e.g "gen/gen_constants.go"
Types []TypeSpec // specification of types to be defined
Groups []GroupSpec // specification of groups of constants
LineParser LineParser // parser for each line in the input file
}
// TypeSpec describes a Go type to be defined in the generated code.
type TypeSpec struct {
Name string // type name, e.g "EventCode"
BuiltInType string // Go built-in type, e.g "uint16"
Desc string // human-readable type description used in comment
}
// GroupSpec describes how to make a group of constants.
type GroupSpec struct {
Prefix string // constant prefix used as group identifier, e.g, the prefix for "KEY_*" should be "KEY".
TypeName string // constant type name, e.g. "EventCode"
Desc string // human-readable group description used in comment
}
// LineParser is a type to parse a line.
type LineParser func(line string) (name, sval string, ok bool)
// GenerateConstants reads consts from input and generate output Go source file
// using a text/template.
func GenerateConstants(input, output string, params Params) error {
repoPath, err := gitRelPath(input)
if err != nil {
return errors.Wrapf(err, "failed to get repo path for %v", input)
}
repoRev, err := gitRev(input)
if err != nil {
return errors.Wrapf(err, "failed to get repo revision for %v", input)
}
consts, err := readConstants(input, params.LineParser)
if err != nil {
return errors.Wrapf(err, "failed to read %v", input)
}
a := tmplArgs{
CopyrightYear: params.CopyrightYear,
PackageName: params.PackageName,
MainGoFilePath: params.MainGoFilePath,
RepoPath: repoPath,
RepoName: params.RepoName,
RepoRev: repoRev,
PreludeCode: params.PreludeCode,
Types: params.Types,
}
if err := writeConstants(classifyConstants(consts, params.Groups), a, output); err != nil {
return errors.Wrapf(err, "failed to write %v", output)
}
return nil
}
const tmplStr = `// Copyright {{printf "%d" .CopyrightYear}} The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package {{.PackageName}}
// Code generated by {{.MainGoFilePath}}. DO NOT EDIT.
//
// Do not change the above line; see https://golang.org/pkg/cmd/go/internal/generate/
//
// This file contains constants from {{.RepoPath}}
// in the {{.RepoName}} repository at revision {{.RepoRev}}.
// Run "go generate" to regenerate it.
{{/* String that could be used to add any valid Go code, including comments, like the go:generate rule */}}
{{- .PreludeCode}}
{{/* Defines types */}}
{{- range .Types}}
// {{.Name}} {{.Desc}}
type {{.Name}} {{.BuiltInType}}
{{- end}}
{{/* Generates the constants for each group */}}
const (
{{- range .Groups}}
// {{.Desc}}{{$type := .TypeName -}}
{{range .Constants}}
{{.Name}} {{$type}} = {{printf "%#x" .Val}}
{{- end}}
{{end}}
)
`
// tmplArgs represents the arguments, besides groups, used in the template.
type tmplArgs struct {
CopyrightYear int // copyright year used in the license, e.g 2018
PackageName string // Go package name, e.g. "input"
MainGoFilePath string // name of the main .go file for generating the constants, e.g "gen/gen_constants.go"
RepoPath string // filepath relative to the repo root, e.g "include/uapi/linux/input-event-codes.h"
RepoName string // repo name, e.g "Linux kernel"
RepoRev string // repo git revision
PreludeCode string // Go code to include at the top of file (typically "//go:generate go run ...")
Types []TypeSpec // specification of types to be defined
}
// constant describes an individual constant.
type constant struct {
Name string // name of the constant, e.g "KEY_ENTER"
Val int64 // value of the constant, e.g. 0x1c
}
type group struct {
TypeName string // type name of this group.
Desc string // description to be embedded at the beginning
Constants []constant // constants of this group
}
// gitRelPath returns the path to the file or directory of the p relative to its git
// repository's root.
func gitRelPath(p string) (string, error) {
out, err := exec.Command("git", "-C", filepath.Dir(p), "ls-files", "--full-name", filepath.Base(p)).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
// gitRev returns the revision of the git repository containing the p.
func gitRev(p string) (string, error) {
// This prints the base path of the repo on the first line and HEAD's revision on the second.
cmd := exec.Command("git", "-C", filepath.Dir(p), "rev-parse", "--show-toplevel", "HEAD")
out, err := cmd.Output()
if err != nil {
return "", err
}
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) != 2 {
return "", errors.Errorf("%q printed %q: wanted 2 lines", strings.Join(cmd.Args, " "), string(out))
}
return lines[1], nil
}
// writeConstants writes consts to path as a Go source file, using a text/template.
// groups and args are used to populate the template.
func writeConstants(groups []group, args tmplArgs, path string) error {
data := struct {
tmplArgs
Groups []group
}{
args,
groups,
}
f, err := ioutil.TempFile(filepath.Dir(path), "."+filepath.Base(path)+".")
if err != nil {
return err
}
defer func() {
if err == nil {
return
}
f.Close()
os.Remove(f.Name())
}()
if err := template.Must(template.New("header").Parse(tmplStr)).Execute(f, data); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
return os.Rename(f.Name(), path)
}
// readConstants reads the constants from the file at path.
// For each line, parser is called. The parser is expected to return two tokens,
// name and its integer value in string format, with ok = true. If the line should be
// skipped, it should return with ok = false.
func readConstants(path string, parser func(line string) (name, sval string, ok bool)) ([]constant, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var result []constant
sc := bufio.NewScanner(f)
for sc.Scan() {
name, sval, ok := parser(sc.Text())
if !ok {
continue
}
val, err := strconv.ParseInt(sval, 0, 64)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse int literal %q for %q", sval, name)
}
result = append(result, constant{name, val})
}
return result, nil
}
// classifyConstants makes groups by their name prefixes. The values in each group are sorted in
// ascending order of value.
func classifyConstants(cs []constant, groups []GroupSpec) []group {
result := make([]group, len(groups))
for i, g := range groups {
result[i] = group{TypeName: g.TypeName, Desc: g.Desc}
}
for _, c := range cs {
// Note if a corresponding group is not found, the constant will be ignored intentionally.
for i, g := range groups {
if strings.HasPrefix(c.Name, g.Prefix) {
result[i].Constants = append(result[i].Constants, c)
break
}
}
}
// Sort each group by ascending value.
for _, g := range result {
sort.Slice(g.Constants, func(i, j int) bool { return g.Constants[i].Val < g.Constants[j].Val })
}
return result
}