[fxicfg] Write files to disk at the end of execution

Bug: IN-1102 #comment
Change-Id: Ia6343e67f64a410aef961c5b40627c8ae49055ac
diff --git a/cmd/fxicfg/generate.go b/cmd/fxicfg/generate.go
index b084ffc..4c35674 100644
--- a/cmd/fxicfg/generate.go
+++ b/cmd/fxicfg/generate.go
@@ -16,7 +16,11 @@
 	luci "go.chromium.org/luci/starlark/interpreter"
 )
 
-type GenerateCommand struct{}
+// TODO(IN-1102): Delete all files in the output dir that weren't generated by this
+//   run to prevent "zombie" configs.
+type GenerateCommand struct {
+	diffOnly bool
+}
 
 func (*GenerateCommand) Name() string {
 	return "generate"
@@ -30,7 +34,9 @@
 	return "generates infrastructure configs from Starlark sources"
 }
 
-func (cmd *GenerateCommand) SetFlags(f *flag.FlagSet) {}
+func (cmd *GenerateCommand) SetFlags(f *flag.FlagSet) {
+	flag.BoolVar(&cmd.diffOnly, "diff", false, "Whether to display changes and exit without writing files")
+}
 
 func (cmd *GenerateCommand) Execute(ctx context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
 	if err := cmd.validateFlags(f); err != nil {
@@ -46,8 +52,20 @@
 
 func (cmd *GenerateCommand) execute(ctx context.Context, path string) error {
 	root, main := filepath.Dir(path), filepath.Base(path)
-	_, _, err := fxicfg.Generate(luci.FileSystemLoader(root), main)
-	return err
+	state, _, err := fxicfg.Generate(luci.FileSystemLoader(root), main)
+	if err != nil {
+		return err
+	}
+
+	if cmd.diffOnly {
+		// TODO(IN-1102)
+		return errors.New("unimplemented: -diff")
+	}
+
+	if err := fxicfg.CommitState(state); err != nil {
+		return err
+	}
+	return nil
 }
 
 func (cmd *GenerateCommand) validateFlags(f *flag.FlagSet) error {
diff --git a/fxicfg/generate.go b/fxicfg/generate.go
index ae541b2..b14965b 100644
--- a/fxicfg/generate.go
+++ b/fxicfg/generate.go
@@ -6,6 +6,9 @@
 
 import (
 	"context"
+	"io/ioutil"
+	"os"
+	"path/filepath"
 
 	"fuchsia.googlesource.com/infra/infra/fxicfg/builtins"
 	"fuchsia.googlesource.com/infra/infra/fxicfg/loaders"
@@ -16,6 +19,11 @@
 	"go.starlark.net/starlark"
 )
 
+const (
+	// The default output directory, if one is not supplied.
+	defaultOutputDir = "generated"
+)
+
 // Generate executes LUCI's starlark interpreter with Fuchsia packages and globals.
 //
 // `loader` is the root luci.Loader that knows how to load all client starlark
@@ -66,3 +74,20 @@
 
 	return tm.State(), globals, nil
 }
+
+func CommitState(s state.State) error {
+	dir := s.OutputDir
+	if dir == "" {
+		dir = defaultOutputDir
+	}
+	if err := os.MkdirAll(dir, 0755); err != nil {
+		return err
+	}
+	for k, v := range s.OutputFiles {
+		path := filepath.Join(dir, k)
+		if err := ioutil.WriteFile(path, v, 0644); err != nil {
+			return err
+		}
+	}
+	return nil
+}