[specs][starlark] Add a tool for generating starlark that generates a spec

These tools are likely to be short-lived. I'll add tests for them later since
they're need to unblock others for the migration, and they aren't at-risk of
breaking a production system.

Example usage:
  bb log <build-id> "load spec" textproto | go run ./cmd/specs to_starlark -recipe fuchsia | yapf

Bug: IN-1366 #comment
Change-Id: Ia4f53dbcd8815000d1dc9d8a7b0e6fc173085586
diff --git a/cmd/specs/generators/fuchsia_py.go b/cmd/specs/generators/fuchsia_py.go
new file mode 100644
index 0000000..128d1b6
--- /dev/null
+++ b/cmd/specs/generators/fuchsia_py.go
@@ -0,0 +1,54 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package generators
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strings"
+
+	recipes "fuchsia.googlesource.com/infra/infra/fxicfg/starlark/protos/recipes"
+	"fuchsia.googlesource.com/infra/infra/starlark/starlarkgen"
+	"github.com/golang/protobuf/proto"
+)
+
+// FuchsiaPySpecGenerator produces Starlark code that generates a spec for the fuchsia.py
+// recipe.
+type FuchsiaPySpecGenerator struct {
+	w io.Writer
+}
+
+func NewFuchsiaPySpecGenerator(output io.Writer) *FuchsiaPySpecGenerator {
+	return &FuchsiaPySpecGenerator{output}
+}
+
+func (g *FuchsiaPySpecGenerator) Generate(in io.Reader) error {
+	spec, err := g.parseTextproto(in)
+	if err != nil {
+		return fmt.Errorf("failed to parse input: %v", err)
+	}
+	output := starlarkgen.Generate(spec)
+	fmt.Fprintf(g.w, output)
+	return nil
+}
+
+// parse parses a spec for the Fuchsia recipe from the given io.Reader.
+func (g *FuchsiaPySpecGenerator) parseTextproto(r io.Reader) (*recipes.Fuchsia, error) {
+	bytes, err := ioutil.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+	textproto := strings.TrimSpace(string(bytes))
+	if textproto == "" {
+		return nil, errors.New("input is empty")
+	}
+	spec := new(recipes.Fuchsia)
+	if err := proto.UnmarshalText(string(textproto), spec); err != nil {
+		return nil, err
+	}
+	return spec, nil
+}
diff --git a/cmd/specs/main.go b/cmd/specs/main.go
new file mode 100644
index 0000000..2d125a5
--- /dev/null
+++ b/cmd/specs/main.go
@@ -0,0 +1,26 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package main implements a tool for working with spec files. Some subcommands are useful
+// for migrating a recipe to a new proto API and addding new spec files. Others are useful
+// for recipe execution with spec files.
+package main
+
+import (
+	"context"
+	"flag"
+	"os"
+
+	"github.com/google/subcommands"
+)
+
+func main() {
+	subcommands.Register(subcommands.HelpCommand(), "")
+	subcommands.Register(subcommands.CommandsCommand(), "")
+	subcommands.Register(subcommands.FlagsCommand(), "")
+	subcommands.Register(&ToStarlarkCommand{}, "")
+
+	flag.Parse()
+	os.Exit(int(subcommands.Execute(context.Background())))
+}
diff --git a/cmd/specs/to_starlark.go b/cmd/specs/to_starlark.go
new file mode 100644
index 0000000..e65419c
--- /dev/null
+++ b/cmd/specs/to_starlark.go
@@ -0,0 +1,69 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package main
+
+import (
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+
+	"log"
+	"os"
+
+	"fuchsia.googlesource.com/infra/infra/cmd/specs/generators"
+	"github.com/google/subcommands"
+)
+
+// StarlarkCodeGenerator transforms a text protobuf message read from the given io.Reader
+// into the starlark code that would generate that same message when executed by fxicfg.
+// The output is written to the given io.Writer.
+//
+// This relies on the generated Golang protobuf API being checked in to the fxcicfg pkg.
+type StarlarkCodeGenerator interface {
+	Generate(io.Reader) error
+}
+
+// FuchsiaPyID is the ID used to select the Fuchsia spec generator.
+const FuchsiaPyID = "fuchsia"
+
+// ToStarlarkCommand generates starlark code from a spec.
+type ToStarlarkCommand struct {
+	// The selected StarlarkCodeGenerator.
+	recipe string
+}
+
+func (*ToStarlarkCommand) Name() string  { return "to_starlark" }
+func (*ToStarlarkCommand) Usage() string { return "cat /path/to/spec | to_starlark" }
+func (*ToStarlarkCommand) Synopsis() string {
+	return "Generates the fxicfg starlark code that would produce the input spec"
+}
+
+func (cmd *ToStarlarkCommand) SetFlags(f *flag.FlagSet) {
+	f.StringVar(&cmd.recipe, "recipe", FuchsiaPyID, fmt.Sprintf("The recipe whose api is being generated. Options: %v", []string{
+		FuchsiaPyID,
+	}))
+}
+
+func (cmd *ToStarlarkCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
+	generator, err := cmd.selectGenerator(cmd.recipe)
+	if err != nil {
+		log.Fatalf("invalid generator: %v", cmd.recipe)
+	}
+	if err := generator.Generate(os.Stdin); err != nil {
+		log.Fatalf("failed to generate spec: %v", err)
+	}
+	return subcommands.ExitFailure
+}
+
+func (cmd *ToStarlarkCommand) selectGenerator(key string) (StarlarkCodeGenerator, error) {
+	switch key {
+	case FuchsiaPyID:
+		return generators.NewFuchsiaPySpecGenerator(os.Stdout), nil
+	default:
+		return nil, errors.New("generator not found")
+	}
+}
diff --git a/starlark/starlarkfmt/starlarkfmt.go b/starlark/starlarkfmt/starlarkfmt.go
new file mode 100644
index 0000000..499df8d
--- /dev/null
+++ b/starlark/starlarkfmt/starlarkfmt.go
@@ -0,0 +1,50 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package starlarkfmt formats Go values as starlark code.
+// TODO(kjharland): Use the starlark libraries for this?
+package starlarkfmt
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+func Format(v reflect.Value) string {
+	switch v.Kind() {
+	case reflect.String:
+		return String(v)
+	case reflect.Slice:
+		return Slice(v)
+	case reflect.Bool:
+		return Bool(v)
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
+		reflect.Float32, reflect.Float64:
+		return Num(v)
+	default:
+		panic(fmt.Sprintf("unimplemented: %v", v.Kind()))
+	}
+}
+
+func Num(v reflect.Value) string {
+	return fmt.Sprintf("%d", v.Int())
+}
+
+func String(v reflect.Value) string {
+	return fmt.Sprintf("\"%s\"", v.String())
+}
+
+func Slice(v reflect.Value) string {
+	output := "["
+	for i := 0; i < v.Len(); i++ {
+		output += fmt.Sprintf("%s,", Format(v.Index(i)))
+	}
+	return output + "]"
+}
+
+func Bool(v reflect.Value) string {
+	return strings.Title(fmt.Sprintf("%t", v.Interface()))
+}
diff --git a/starlark/starlarkgen/ast.go b/starlark/starlarkgen/ast.go
new file mode 100644
index 0000000..31eeca1
--- /dev/null
+++ b/starlark/starlarkgen/ast.go
@@ -0,0 +1,48 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package starlarkgen
+
+import (
+	"bytes"
+	"fmt"
+	"reflect"
+
+	"fuchsia.googlesource.com/infra/infra/starlark/starlarkfmt"
+)
+
+type Node interface {
+	String() string
+}
+
+type ConstructorCall struct {
+	Package  string
+	Typename string
+	Kwargs   []Node
+}
+
+func (c ConstructorCall) String() string {
+	args := new(bytes.Buffer)
+	for _, arg := range c.Kwargs {
+		args.WriteString(arg.String() + ",")
+	}
+	return fmt.Sprintf("%s.%s(%s)", c.Package, c.Typename, args)
+}
+
+type Kwarg struct {
+	Keyword string
+	Value   Node
+}
+
+func (kwarg Kwarg) String() string {
+	return fmt.Sprintf("%s=%s", kwarg.Keyword, kwarg.Value)
+}
+
+type Primitive struct {
+	Value reflect.Value
+}
+
+func (a Primitive) String() string {
+	return starlarkfmt.Format(a.Value)
+}
diff --git a/starlark/starlarkgen/generator.go b/starlark/starlarkgen/generator.go
new file mode 100644
index 0000000..29c209c
--- /dev/null
+++ b/starlark/starlarkgen/generator.go
@@ -0,0 +1,74 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package starlarkgen
+
+import (
+	"reflect"
+	"strings"
+
+	"github.com/golang/protobuf/descriptor"
+	protoc "github.com/golang/protobuf/protoc-gen-go/descriptor"
+)
+
+// Generate produces the starlark code needed to generate the input protobuf message.
+func Generate(msg descriptor.Message) string {
+	ast := genMessage(reflect.ValueOf(msg))
+	return ast.String()
+}
+
+func genMessage(val reflect.Value) Node {
+	// Don't output anything for nil or zero values.
+	if isNothing(val) {
+		return nil
+	}
+
+	// Must read these values before calling val.Elem(). Otherwise the value it reflects
+	// is no longer a descriptor.Message.
+	msg := val.Interface().(descriptor.Message)
+	filedesc, desc := descriptor.ForMessage(msg)
+	node := &ConstructorCall{
+		Package:  filedesc.GetPackage(),
+		Typename: desc.GetName(),
+	}
+	val = val.Elem()
+	for _, field := range desc.GetField() {
+		fieldValue := val.FieldByName(strings.Title(field.GetJsonName()))
+		fieldAst := genField(field, fieldValue)
+		// Skip fields that have nil pointer values.
+		if fieldAst == nil {
+			continue
+		}
+		// Skip fields that are set to zero-values.
+		if primitive, ok := fieldAst.(Primitive); ok && isZero(primitive.Value) {
+			continue
+		}
+		node.Kwargs = append(node.Kwargs, Kwarg{
+			Keyword: field.GetName(),
+			Value:   fieldAst,
+		})
+	}
+	return node
+}
+
+func genField(desc *protoc.FieldDescriptorProto, val reflect.Value) Node {
+	if desc.GetTypeName() == "" {
+		// This field is a not a proto message (and the instance is not a Go struct). Just
+		// write the raw value.
+		return Primitive{val}
+	}
+
+	// This field is a proto.Message, which has a corresponding Go struct type. Generate
+	// it and its children recursively.
+	return genMessage(val)
+}
+
+func isZero(val reflect.Value) bool {
+	zero := reflect.Zero(val.Type()).Interface()
+	return reflect.DeepEqual(zero, val.Interface())
+}
+
+func isNothing(val reflect.Value) bool {
+	return isZero(val) || val.IsNil()
+}