| // 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 |
| } |