[fint] Expose Set() as a library function

I'm writing a new version of `fx set` in Go that will consume fint as a
library, instead of invoking the fint executable. Go doesn't allow
imports of main packages, so I have to move the `fint set`
implementation details into a separate library package.

This will require some changes to our Copybara configs, recipes.git, and
integration.git to reflect the new proto paths.

Bug: 68465
Change-Id: If78bf2884287143bbb5b6c49e5f697006d816644
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/475875
Reviewed-by: Gary Boone <gboone@google.com>
Commit-Queue: Oliver Newman <olivernewman@google.com>
diff --git a/tools/integration/BUILD.gn b/tools/integration/BUILD.gn
index ab1c6ca..250c6b8 100644
--- a/tools/integration/BUILD.gn
+++ b/tools/integration/BUILD.gn
@@ -16,6 +16,7 @@
 
   deps = [
     "//tools/integration/cmd/fint:tests",
+    "//tools/integration/fint:tests",
     "//tools/integration/testsharder:testsharder_tests",
   ]
 }
diff --git a/tools/integration/cmd/fint/BUILD.gn b/tools/integration/cmd/fint/BUILD.gn
index d2e20b5..f8c8012 100644
--- a/tools/integration/cmd/fint/BUILD.gn
+++ b/tools/integration/cmd/fint/BUILD.gn
@@ -8,61 +8,24 @@
 import("//build/testing/golden_file.gni")
 import("//third_party/protobuf/proto_library.gni")
 
-go_library("proto_lib") {
-  source_dir = "proto"
-  sources = [
-    "context.pb.go",
-    "set_artifacts.pb.go",
-    "static.pb.go",
-  ]
-  deps = [ "//third_party/golibs:github.com/golang/protobuf" ]
-}
-
-go_library("filetype_lib") {
-  sources = [
-    "filetype/filetype.go",
-    "filetype/filetype_test.go",
-  ]
-}
-
-go_test("filetype_tests") {
-  gopackages = [ "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/filetype" ]
-  deps = [ ":filetype_lib" ]
-}
-
 go_library("fint_lib") {
   sources = [
-    "fint_test.go",
     "main.go",
     "parse.go",
+    "parse_test.go",
     "set.go",
-    "set_test.go",
-    "should_build.go",
-    "should_build_test.go",
   ]
 
   deps = [
-    ":filetype_lib",
-    ":proto_lib",
     "//third_party/golibs:github.com/golang/protobuf",
-    "//third_party/golibs:github.com/google/go-cmp",
     "//third_party/golibs:github.com/google/subcommands",
+    "//tools/integration/fint:fint_lib",
+    "//tools/integration/fint:proto_lib",
     "//tools/lib/color",
     "//tools/lib/command",
     "//tools/lib/jsonutil",
     "//tools/lib/logger",
     "//tools/lib/osmisc",
-    "//tools/lib/runner",
-  ]
-}
-
-# This target encompasses all generated proto files, and is intended for use by
-# the update.sh script to avoid needing to specify all the protos again there.
-group("protos") {
-  deps = [
-    ":context",
-    ":set_artifacts",
-    ":static",
   ]
 }
 
@@ -76,65 +39,7 @@
   deps = [ ":fint_lib" ]
 }
 
-proto_library("context") {
-  sources = [ "proto/context.proto" ]
-  generate_cc = false
-  generate_go = true
-  generate_python = false
-}
-
-proto_library("static") {
-  sources = [ "proto/static.proto" ]
-  generate_cc = false
-  generate_descriptor_set = true
-  generate_go = true
-  generate_python = false
-}
-
-proto_library("set_artifacts") {
-  sources = [ "proto/set_artifacts.proto" ]
-  generate_cc = false
-  generate_go = true
-  generate_python = false
-}
-
-golden_file("context_pb_go_diff") {
-  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/cmd/fint/proto/context.pb.go"
-  golden = "proto/context.pb.go"
-  deps = [ ":context" ]
-  warn_on_changes = false
-}
-
-golden_file("static_pb_go_diff") {
-  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/cmd/fint/proto/static.pb.go"
-  golden = "proto/static.pb.go"
-  deps = [ ":static" ]
-  warn_on_changes = false
-}
-
-golden_file("set_artifacts_pb_go_diff") {
-  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/cmd/fint/proto/set_artifacts.pb.go"
-  golden = "proto/set_artifacts.pb.go"
-  deps = [ ":set_artifacts" ]
-  warn_on_changes = false
-}
-
-golden_file("static_desc_pb_diff") {
-  current = "${target_out_dir}/static.desc.pb"
-  golden = "proto/static.desc.pb"
-  deps = [ ":static" ]
-  warn_on_changes = false
-}
-
 group("tests") {
   testonly = true
-  deps = [
-    # The diff rules aren't really tests, but we want to ensure they're run in CQ.
-    ":context_pb_go_diff",
-    ":filetype_tests",
-    ":fint_tests",
-    ":set_artifacts_pb_go_diff",
-    ":static_desc_pb_diff",
-    ":static_pb_go_diff",
-  ]
+  deps = [ ":fint_tests" ]
 }
diff --git a/tools/integration/cmd/fint/parse.go b/tools/integration/cmd/fint/parse.go
index 96db3d9..63b8f20 100644
--- a/tools/integration/cmd/fint/parse.go
+++ b/tools/integration/cmd/fint/parse.go
@@ -6,7 +6,7 @@
 
 import (
 	"github.com/golang/protobuf/proto"
-	fintpb "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto"
+	fintpb "go.fuchsia.dev/fuchsia/tools/integration/fint/proto"
 )
 
 func parseStatic(text string) (*fintpb.Static, error) {
diff --git a/tools/integration/cmd/fint/fint_test.go b/tools/integration/cmd/fint/parse_test.go
similarity index 100%
rename from tools/integration/cmd/fint/fint_test.go
rename to tools/integration/cmd/fint/parse_test.go
diff --git a/tools/integration/cmd/fint/proto/examples/empty.textproto b/tools/integration/cmd/fint/proto/examples/empty.textproto
deleted file mode 100644
index e69de29..0000000
--- a/tools/integration/cmd/fint/proto/examples/empty.textproto
+++ /dev/null
diff --git a/tools/integration/cmd/fint/proto/examples/x64-release.textproto b/tools/integration/cmd/fint/proto/examples/x64-release.textproto
deleted file mode 100644
index 0d3d087..0000000
--- a/tools/integration/cmd/fint/proto/examples/x64-release.textproto
+++ /dev/null
@@ -1,11 +0,0 @@
-optimize: RELEASE
-board: "qemu"
-product: "workstation"
-exclude_images: false
-ninja_targets: "default"
-include_host_tests: false
-target_arch: X64
-enforce_size_limits: false
-collect_metrics: false
-include_archives: false
-skip_if_unaffected: true
diff --git a/tools/integration/cmd/fint/proto/update.sh b/tools/integration/cmd/fint/proto/update.sh
deleted file mode 100755
index 24a4a30..0000000
--- a/tools/integration/cmd/fint/proto/update.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-
-# Copyright 2020 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.
-
-# This script compiles the .proto files and copies the compiled versions
-# from the build dir back to the source tree. It should be run whenever
-# the .proto files are modified.
-
-set -o errexit
-set -o pipefail
-
-# cd to fuchsia checkout root.
-cd "$( dirname "${BASH_SOURCE[0]}" )/../../../../.."
-
-scripts/fx set core.x64 --with //tools/integration/cmd/fint:protos
-scripts/fx ninja -C out/default tools/integration/cmd/fint:protos
-
-cp out/default/gen/go-proto-gen/src/tools/integration/cmd/fint/proto/*.pb.go \
-  tools/integration/cmd/fint/proto/
-cp out/default/obj/tools/integration/cmd/fint/*.desc.pb \
-  tools/integration/cmd/fint/proto/
diff --git a/tools/integration/cmd/fint/set.go b/tools/integration/cmd/fint/set.go
index 596b698..de40b71 100644
--- a/tools/integration/cmd/fint/set.go
+++ b/tools/integration/cmd/fint/set.go
@@ -5,24 +5,19 @@
 package main
 
 import (
-	"bytes"
 	"context"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"runtime"
-	"sort"
-	"strings"
 
 	"github.com/golang/protobuf/jsonpb"
 	"github.com/google/subcommands"
-	fintpb "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto"
+	"go.fuchsia.dev/fuchsia/tools/integration/fint"
+	fintpb "go.fuchsia.dev/fuchsia/tools/integration/fint/proto"
 	"go.fuchsia.dev/fuchsia/tools/lib/logger"
 	"go.fuchsia.dev/fuchsia/tools/lib/osmisc"
-	"go.fuchsia.dev/fuchsia/tools/lib/runner"
 )
 
 const (
@@ -41,10 +36,6 @@
 	artifactsManifest = "set_artifacts.json"
 )
 
-type subprocessRunner interface {
-	Run(ctx context.Context, cmd []string, stdout, stderr io.Writer) error
-}
-
 type SetCommand struct {
 	staticSpecPath  string
 	contextSpecPath string
@@ -117,13 +108,7 @@
 		}
 	}
 
-	platform, err := getPlatform()
-	if err != nil {
-		return err
-	}
-
-	runner := &runner.SubprocessRunner{}
-	artifacts, runErr := runSteps(ctx, runner, staticSpec, contextSpec, platform)
+	artifacts, setErr := fint.Set(ctx, staticSpec, contextSpec)
 
 	if contextSpec.ArtifactDir != "" {
 		f, err := osmisc.CreateFile(filepath.Join(contextSpec.ArtifactDir, artifactsManifest))
@@ -137,7 +122,7 @@
 		}
 	}
 
-	return runErr
+	return setErr
 }
 
 func defaultContextSpec() (*fintpb.Context, error) {
@@ -150,277 +135,3 @@
 		BuildDir:    filepath.Join(checkoutDir, "out", "default"),
 	}, nil
 }
-
-// runSteps runs `gn gen` along with any post-processing steps, and returns a
-// SetArtifacts object containing metadata produced by GN and post-processing.
-func runSteps(
-	ctx context.Context,
-	runner subprocessRunner,
-	staticSpec *fintpb.Static,
-	contextSpec *fintpb.Context,
-	platform string,
-) (*fintpb.SetArtifacts, error) {
-	gnPath := filepath.Join(contextSpec.CheckoutDir, "prebuilt", "third_party", "gn", platform, "gn")
-	artifacts := &fintpb.SetArtifacts{}
-	genArgs, err := genArgs(staticSpec, contextSpec, platform)
-	if err != nil {
-		return nil, err
-	}
-	if contextSpec.ArtifactDir != "" {
-		artifacts.GnTracePath = filepath.Join(contextSpec.ArtifactDir, "gn_trace.json")
-	}
-	genStdout, err := runGen(ctx, runner, staticSpec, contextSpec, gnPath, artifacts.GnTracePath, genArgs)
-	if err != nil {
-		artifacts.FailureSummary = genStdout
-		return artifacts, err
-	}
-	if staticSpec.SkipIfUnaffected {
-		var changedFiles []string
-		for _, f := range contextSpec.ChangedFiles {
-			changedFiles = append(changedFiles, f.Path)
-		}
-		sb, err := shouldBuild(ctx, runner, contextSpec.BuildDir, contextSpec.CheckoutDir, gnPath, changedFiles)
-		if err != nil {
-			return artifacts, err
-		}
-		artifacts.SkipBuild = !sb
-	}
-	return artifacts, err
-}
-
-func runGen(
-	ctx context.Context,
-	runner subprocessRunner,
-	staticSpec *fintpb.Static,
-	contextSpec *fintpb.Context,
-	gnPath string,
-	gnTracePath string,
-	args []string,
-) (genStdout string, err error) {
-	genCmd := []string{
-		gnPath, "gen",
-		contextSpec.BuildDir,
-		"--check=system",
-		"--fail-on-unused-args",
-	}
-
-	if gnTracePath != "" {
-		genCmd = append(genCmd, fmt.Sprintf("--tracelog=%s", gnTracePath))
-	}
-	if staticSpec.GenerateCompdb {
-		genCmd = append(genCmd, "--export-compile-commands")
-	}
-	if staticSpec.GenerateIde {
-		genCmd = append(genCmd, "--ide=json")
-	}
-
-	genCmd = append(genCmd, fmt.Sprintf("--args=%s", strings.Join(args, " ")))
-
-	// When `gn gen` fails, it outputs a brief helpful error message to stdout.
-	var stdoutBuf bytes.Buffer
-	if err := runner.Run(ctx, genCmd, io.MultiWriter(&stdoutBuf, os.Stdout), os.Stderr); err != nil {
-		return stdoutBuf.String(), fmt.Errorf("error running gn gen: %w", err)
-	}
-	return stdoutBuf.String(), nil
-}
-
-func getPlatform() (string, error) {
-	os, ok := map[string]string{
-		"windows": "win",
-		"darwin":  "mac",
-		"linux":   "linux",
-	}[runtime.GOOS]
-	if !ok {
-		return "", fmt.Errorf("unsupported GOOS %q", runtime.GOOS)
-	}
-
-	arch, ok := map[string]string{
-		"amd64": "x64",
-		"arm64": "arm64",
-	}[runtime.GOARCH]
-	if !ok {
-		return "", fmt.Errorf("unsupported GOARCH %q", runtime.GOARCH)
-	}
-	return fmt.Sprintf("%s-%s", os, arch), nil
-}
-
-func genArgs(staticSpec *fintpb.Static, contextSpec *fintpb.Context, platform string) ([]string, error) {
-	// GN variables to set via args (mapping from variable name to value).
-	vars := make(map[string]interface{})
-	// GN list variables to which we want to append via args (mapping from
-	// variable name to list of values to append).
-	appends := make(map[string][]string)
-	// GN targets to import.
-	var imports []string
-
-	if staticSpec.TargetArch == fintpb.Static_ARCH_UNSPECIFIED {
-		return nil, fmt.Errorf("target_arch is unspecified or invalid")
-	}
-	vars["target_cpu"] = strings.ToLower(staticSpec.TargetArch.String())
-
-	if staticSpec.Optimize == fintpb.Static_OPTIMIZE_UNSPECIFIED {
-		return nil, fmt.Errorf("optimize is unspecified or invalid")
-	}
-	vars["is_debug"] = staticSpec.Optimize == fintpb.Static_DEBUG
-
-	if contextSpec.ClangToolchainDir != "" {
-		if staticSpec.UseGoma {
-			return nil, fmt.Errorf("goma is not supported for builds using a custom clang toolchain")
-		}
-		vars["clang_prefix"] = filepath.Join(contextSpec.ClangToolchainDir, "bin")
-	}
-	if contextSpec.GccToolchainDir != "" {
-		if staticSpec.UseGoma {
-			return nil, fmt.Errorf("goma is not supported for builds using a custom gcc toolchain")
-		}
-		vars["zircon_extra_args.gcc_tool_dir"] = filepath.Join(contextSpec.GccToolchainDir, "bin")
-	}
-	if contextSpec.RustToolchainDir != "" {
-		vars["rustc_prefix"] = filepath.Join(contextSpec.RustToolchainDir, "bin")
-	}
-
-	vars["use_goma"] = staticSpec.UseGoma
-	if staticSpec.UseGoma {
-		vars["goma_dir"] = filepath.Join(
-			contextSpec.CheckoutDir, "prebuilt", "third_party", "goma", platform,
-		)
-	}
-
-	if staticSpec.Product != "" {
-		basename := filepath.Base(staticSpec.Product)
-		vars["build_info_product"] = strings.Split(basename, ".")[0]
-		imports = append(imports, staticSpec.Product)
-	}
-
-	if staticSpec.Board != "" {
-		basename := filepath.Base(staticSpec.Board)
-		vars["build_info_board"] = strings.Split(basename, ".")[0]
-		imports = append(imports, staticSpec.Board)
-	}
-
-	if contextSpec.SdkId != "" {
-		vars["sdk_id"] = contextSpec.SdkId
-		vars["build_sdk_archives"] = true
-	}
-
-	if contextSpec.ReleaseVersion != "" {
-		vars["build_info_version"] = contextSpec.ReleaseVersion
-	}
-
-	if staticSpec.TestDurationsFile != "" {
-		testDurationsFile := staticSpec.TestDurationsFile
-		exists, err := osmisc.FileExists(filepath.Join(contextSpec.CheckoutDir, testDurationsFile))
-		if err != nil {
-			return nil, fmt.Errorf("failed to check if TestDurationsFile exists: %w", err)
-		}
-		if !exists {
-			testDurationsFile = staticSpec.DefaultTestDurationsFile
-		}
-		vars["test_durations_file"] = testDurationsFile
-	}
-
-	for varName, packages := range map[string][]string{
-		"base_package_labels":     staticSpec.BasePackages,
-		"cache_package_labels":    staticSpec.CachePackages,
-		"universe_package_labels": staticSpec.UniversePackages,
-	} {
-		if len(packages) == 0 {
-			continue
-		}
-		// If product is set, append to the corresponding list variable instead
-		// of overwriting it to avoid overwriting any packages set in the
-		// imported product file.
-		// TODO(olivernewman): Is it safe to always append whether or not
-		// product is set?
-		if staticSpec.Product == "" {
-			vars[varName] = packages
-		} else {
-			appends[varName] = packages
-		}
-	}
-
-	if len(staticSpec.Variants) != 0 {
-		vars["select_variant"] = staticSpec.Variants
-		if contains(staticSpec.Variants, "thinlto") {
-			vars["thinlto_cache_dir"] = filepath.Join(contextSpec.CacheDir, "thinlto")
-		}
-	}
-
-	var normalArgs []string
-	var importArgs []string
-	for _, arg := range staticSpec.GnArgs {
-		if strings.HasPrefix(arg, "import(") {
-			importArgs = append(importArgs, arg)
-		} else {
-			normalArgs = append(normalArgs, arg)
-		}
-	}
-
-	for k, v := range vars {
-		normalArgs = append(normalArgs, fmt.Sprintf("%s=%s", k, toGNValue(v)))
-	}
-	for k, v := range appends {
-		normalArgs = append(normalArgs, fmt.Sprintf("%s+=%s", k, toGNValue(v)))
-	}
-	sort.Strings(normalArgs)
-
-	for _, p := range imports {
-		importArgs = append(importArgs, fmt.Sprintf(`import("//%s")`, p))
-	}
-	sort.Strings(importArgs)
-
-	var finalArgs []string
-
-	// Ensure that imports come before args that set or modify variables, as
-	// otherwise the imported files might blindly redefine variables set or
-	// modified by other arguments.
-	finalArgs = append(finalArgs, importArgs...)
-	// Initialize `zircon_extra_args` before any variable-setting args, so that
-	// it's safe for subsequent args to do things like `zircon_extra_args.foo =
-	// "bar"` without worrying about initializing zircon_extra_args if it hasn't
-	// yet been defined. But do it after all imports in case one of the imported
-	// files sets `zircon_extra_args`.
-	finalArgs = append(finalArgs, "if (!defined(zircon_extra_args)) { zircon_extra_args = {} }")
-	finalArgs = append(finalArgs, normalArgs...)
-	return finalArgs, nil
-}
-
-// toGNValue converts a Go value to a string representation of the corresponding
-// GN value by inspecting the Go value's type. This makes the logic to set GN
-// args more readable and less error-prone, with no need for patterns like
-// fmt.Sprintf(`"%s"`) repeated everywhere.
-//
-// For example:
-// - toGNValue(true) => `true`
-// - toGNValue("foo") => `"foo"` (a string containing literal double-quotes)
-// - toGNValue([]string{"foo", "bar"}) => `["foo","bar"]`
-func toGNValue(x interface{}) string {
-	switch val := x.(type) {
-	case bool:
-		return fmt.Sprintf("%v", val)
-	case string:
-		// Apply double-quotes to strings, but not to GN scopes like
-		// {variant="asan-fuzzer" target_type=["fuzzed_executable"]}
-		if strings.HasPrefix(val, "{") && strings.HasSuffix(val, "}") {
-			return val
-		}
-		return fmt.Sprintf(`"%s"`, val)
-	case []string:
-		var values []string
-		for _, element := range val {
-			values = append(values, toGNValue(element))
-		}
-		return fmt.Sprintf("[%s]", strings.Join(values, ","))
-	default:
-		panic(fmt.Sprintf("unsupported arg value type %T", val))
-	}
-}
-
-func contains(items []string, target string) bool {
-	for _, item := range items {
-		if item == target {
-			return true
-		}
-	}
-	return false
-}
diff --git a/tools/integration/fint/BUILD.gn b/tools/integration/fint/BUILD.gn
new file mode 100644
index 0000000..5e9ec46
--- /dev/null
+++ b/tools/integration/fint/BUILD.gn
@@ -0,0 +1,133 @@
+# Copyright 2020 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.
+
+import("//build/go/go_binary.gni")
+import("//build/go/go_library.gni")
+import("//build/go/go_test.gni")
+import("//build/testing/golden_file.gni")
+import("//third_party/protobuf/proto_library.gni")
+
+go_library("proto_lib") {
+  source_dir = "proto"
+  sources = [
+    "context.pb.go",
+    "set_artifacts.pb.go",
+    "static.pb.go",
+  ]
+  deps = [ "//third_party/golibs:github.com/golang/protobuf" ]
+}
+
+go_library("filetype_lib") {
+  sources = [
+    "filetype/filetype.go",
+    "filetype/filetype_test.go",
+  ]
+}
+
+go_test("filetype_tests") {
+  gopackages = [ "go.fuchsia.dev/fuchsia/tools/integration/fint/filetype" ]
+  deps = [ ":filetype_lib" ]
+}
+
+go_library("fint_lib") {
+  sources = [
+    "common.go",
+    "set.go",
+    "set_test.go",
+    "should_build.go",
+    "should_build_test.go",
+  ]
+
+  deps = [
+    ":filetype_lib",
+    ":proto_lib",
+    "//third_party/golibs:github.com/golang/protobuf",
+    "//third_party/golibs:github.com/google/go-cmp",
+    "//third_party/golibs:github.com/google/subcommands",
+    "//tools/lib/color",
+    "//tools/lib/command",
+    "//tools/lib/jsonutil",
+    "//tools/lib/logger",
+    "//tools/lib/osmisc",
+    "//tools/lib/runner",
+  ]
+}
+
+# This target encompasses all generated proto files, and is intended for use by
+# the update.sh script to avoid needing to specify all the protos again there.
+group("protos") {
+  deps = [
+    ":context",
+    ":set_artifacts",
+    ":static",
+  ]
+}
+
+go_test("fint_lib_tests") {
+  gopackages = [ "go.fuchsia.dev/fuchsia/tools/integration/fint" ]
+  deps = [ ":fint_lib" ]
+}
+
+proto_library("context") {
+  sources = [ "proto/context.proto" ]
+  generate_cc = false
+  generate_go = true
+  generate_python = false
+}
+
+proto_library("static") {
+  sources = [ "proto/static.proto" ]
+  generate_cc = false
+  generate_descriptor_set = true
+  generate_go = true
+  generate_python = false
+}
+
+proto_library("set_artifacts") {
+  sources = [ "proto/set_artifacts.proto" ]
+  generate_cc = false
+  generate_go = true
+  generate_python = false
+}
+
+golden_file("context_pb_go_diff") {
+  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/fint/proto/context.pb.go"
+  golden = "proto/context.pb.go"
+  deps = [ ":context" ]
+  warn_on_changes = false
+}
+
+golden_file("static_pb_go_diff") {
+  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/fint/proto/static.pb.go"
+  golden = "proto/static.pb.go"
+  deps = [ ":static" ]
+  warn_on_changes = false
+}
+
+golden_file("set_artifacts_pb_go_diff") {
+  current = "${root_gen_dir}/go-proto-gen/src/tools/integration/fint/proto/set_artifacts.pb.go"
+  golden = "proto/set_artifacts.pb.go"
+  deps = [ ":set_artifacts" ]
+  warn_on_changes = false
+}
+
+golden_file("static_desc_pb_diff") {
+  current = "${target_out_dir}/static.desc.pb"
+  golden = "proto/static.desc.pb"
+  deps = [ ":static" ]
+  warn_on_changes = false
+}
+
+group("tests") {
+  testonly = true
+  deps = [
+    # The diff rules aren't really tests, but we want to ensure they're run in CQ.
+    ":context_pb_go_diff",
+    ":filetype_tests",
+    ":fint_lib_tests",
+    ":set_artifacts_pb_go_diff",
+    ":static_desc_pb_diff",
+    ":static_pb_go_diff",
+  ]
+}
diff --git a/tools/integration/fint/common.go b/tools/integration/fint/common.go
new file mode 100644
index 0000000..d5115e4
--- /dev/null
+++ b/tools/integration/fint/common.go
@@ -0,0 +1,14 @@
+// Copyright 2021 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 fint
+
+import (
+	"context"
+	"io"
+)
+
+type subprocessRunner interface {
+	Run(ctx context.Context, cmd []string, stdout, stderr io.Writer) error
+}
diff --git a/tools/integration/cmd/fint/filetype/filetype.go b/tools/integration/fint/filetype/filetype.go
similarity index 100%
rename from tools/integration/cmd/fint/filetype/filetype.go
rename to tools/integration/fint/filetype/filetype.go
diff --git a/tools/integration/cmd/fint/filetype/filetype_test.go b/tools/integration/fint/filetype/filetype_test.go
similarity index 100%
rename from tools/integration/cmd/fint/filetype/filetype_test.go
rename to tools/integration/fint/filetype/filetype_test.go
diff --git a/tools/integration/cmd/fint/proto/README.md b/tools/integration/fint/proto/README.md
similarity index 100%
rename from tools/integration/cmd/fint/proto/README.md
rename to tools/integration/fint/proto/README.md
diff --git a/tools/integration/cmd/fint/proto/context.pb.go b/tools/integration/fint/proto/context.pb.go
similarity index 97%
rename from tools/integration/cmd/fint/proto/context.pb.go
rename to tools/integration/fint/proto/context.pb.go
index 9ab1014..332ed1f 100644
--- a/tools/integration/cmd/fint/proto/context.pb.go
+++ b/tools/integration/fint/proto/context.pb.go
@@ -242,12 +242,11 @@
 	0x64, 0x69, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x72, 0x75, 0x73, 0x74, 0x54,
 	0x6f, 0x6f, 0x6c, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x44, 0x69, 0x72, 0x1a, 0x21, 0x0a, 0x0b, 0x43,
 	0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
-	0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x39,
-	0x5a, 0x37, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76,
+	0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x42, 0x35,
+	0x5a, 0x33, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76,
 	0x2f, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2f, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x69,
-	0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66,
-	0x69, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-	0x33,
+	0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x66, 0x69, 0x6e, 0x74, 0x2f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/tools/integration/cmd/fint/proto/context.proto b/tools/integration/fint/proto/context.proto
similarity index 94%
rename from tools/integration/cmd/fint/proto/context.proto
rename to tools/integration/fint/proto/context.proto
index 96c3b37..5eb9db7 100644
--- a/tools/integration/cmd/fint/proto/context.proto
+++ b/tools/integration/fint/proto/context.proto
@@ -6,7 +6,7 @@
 
 package fint;
 
-option go_package = "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto";
+option go_package = "go.fuchsia.dev/fuchsia/tools/integration/fint/proto";
 
 // Context contains all of the dynamic configuration values for building
 // Fuchsia. These values are context-dependent in that they vary based on git
diff --git a/tools/integration/cmd/fint/proto/set_artifacts.pb.go b/tools/integration/fint/proto/set_artifacts.pb.go
similarity index 95%
rename from tools/integration/cmd/fint/proto/set_artifacts.pb.go
rename to tools/integration/fint/proto/set_artifacts.pb.go
index d3d9465..586da74 100644
--- a/tools/integration/cmd/fint/proto/set_artifacts.pb.go
+++ b/tools/integration/fint/proto/set_artifacts.pb.go
@@ -112,11 +112,11 @@
 	0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x67, 0x6e, 0x54,
 	0x72, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6b, 0x69, 0x70,
 	0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x6b,
-	0x69, 0x70, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x6f, 0x2e, 0x66, 0x75,
+	0x69, 0x70, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x6f, 0x2e, 0x66, 0x75,
 	0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69,
 	0x61, 0x2f, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74,
-	0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x69, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f,
-	0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x69, 0x6f, 0x6e, 0x2f, 0x66, 0x69, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/tools/integration/cmd/fint/proto/set_artifacts.proto b/tools/integration/fint/proto/set_artifacts.proto
similarity index 90%
rename from tools/integration/cmd/fint/proto/set_artifacts.proto
rename to tools/integration/fint/proto/set_artifacts.proto
index 1b99bda..616e6a2 100644
--- a/tools/integration/cmd/fint/proto/set_artifacts.proto
+++ b/tools/integration/fint/proto/set_artifacts.proto
@@ -6,7 +6,7 @@
 
 package fint;
 
-option go_package = "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto";
+option go_package = "go.fuchsia.dev/fuchsia/tools/integration/fint/proto";
 
 // SetArtifacts contains information about the manifests and other metadata
 // produced by `fint set`.
diff --git a/tools/integration/cmd/fint/proto/static.desc.pb b/tools/integration/fint/proto/static.desc.pb
similarity index 91%
rename from tools/integration/cmd/fint/proto/static.desc.pb
rename to tools/integration/fint/proto/static.desc.pb
index dcd6f20..6e5d10c 100644
--- a/tools/integration/cmd/fint/proto/static.desc.pb
+++ b/tools/integration/fint/proto/static.desc.pb
Binary files differ
diff --git a/tools/integration/cmd/fint/proto/static.pb.go b/tools/integration/fint/proto/static.pb.go
similarity index 98%
rename from tools/integration/cmd/fint/proto/static.pb.go
rename to tools/integration/fint/proto/static.pb.go
index 1a55494..efea8e6 100644
--- a/tools/integration/cmd/fint/proto/static.pb.go
+++ b/tools/integration/fint/proto/static.pb.go
@@ -412,11 +412,11 @@
 	0x0a, 0x07, 0x52, 0x45, 0x4c, 0x45, 0x41, 0x53, 0x45, 0x10, 0x02, 0x22, 0x30, 0x0a, 0x04, 0x41,
 	0x72, 0x63, 0x68, 0x12, 0x14, 0x0a, 0x10, 0x41, 0x52, 0x43, 0x48, 0x5f, 0x55, 0x4e, 0x53, 0x50,
 	0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x41, 0x52, 0x4d,
-	0x36, 0x34, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x58, 0x36, 0x34, 0x10, 0x02, 0x42, 0x39, 0x5a,
-	0x37, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76, 0x2f,
+	0x36, 0x34, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x58, 0x36, 0x34, 0x10, 0x02, 0x42, 0x35, 0x5a,
+	0x33, 0x67, 0x6f, 0x2e, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2e, 0x64, 0x65, 0x76, 0x2f,
 	0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2f, 0x74, 0x6f, 0x6f, 0x6c, 0x73, 0x2f, 0x69, 0x6e,
-	0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x69,
-	0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x66, 0x69, 0x6e, 0x74, 0x2f, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
diff --git a/tools/integration/cmd/fint/proto/static.proto b/tools/integration/fint/proto/static.proto
similarity index 97%
rename from tools/integration/cmd/fint/proto/static.proto
rename to tools/integration/fint/proto/static.proto
index 70024b3..42354ca 100644
--- a/tools/integration/cmd/fint/proto/static.proto
+++ b/tools/integration/fint/proto/static.proto
@@ -6,7 +6,7 @@
 
 package fint;
 
-option go_package = "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto";
+option go_package = "go.fuchsia.dev/fuchsia/tools/integration/fint/proto";
 
 // Static contains all of the non-dynamic configuration values for building
 // Fuchsia. These values are "static" in the sense that they don't vary
diff --git a/tools/integration/fint/proto/update.sh b/tools/integration/fint/proto/update.sh
new file mode 100755
index 0000000..f1ea54e
--- /dev/null
+++ b/tools/integration/fint/proto/update.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+# Copyright 2020 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.
+
+# This script compiles the .proto files and copies the compiled versions
+# from the build dir back to the source tree. It should be run whenever
+# the .proto files are modified.
+
+set -o errexit
+set -o pipefail
+
+# cd to fuchsia checkout root.
+cd "$( dirname "${BASH_SOURCE[0]}" )/../../../.."
+
+scripts/fx set core.x64 --with //tools/integration/fint:protos
+scripts/fx ninja -C out/default tools/integration/fint:protos
+
+cp out/default/gen/go-proto-gen/src/tools/integration/fint/proto/*.pb.go \
+  tools/integration/fint/proto/
+cp out/default/obj/tools/integration/fint/*.desc.pb \
+  tools/integration/fint/proto/
diff --git a/tools/integration/fint/set.go b/tools/integration/fint/set.go
new file mode 100644
index 0000000..b3ea3b0
--- /dev/null
+++ b/tools/integration/fint/set.go
@@ -0,0 +1,307 @@
+// Copyright 2021 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 fint
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"runtime"
+	"sort"
+	"strings"
+
+	fintpb "go.fuchsia.dev/fuchsia/tools/integration/fint/proto"
+	"go.fuchsia.dev/fuchsia/tools/lib/osmisc"
+	"go.fuchsia.dev/fuchsia/tools/lib/runner"
+)
+
+// Set runs `gn gen` given a static and context spec. It's intended to be
+// consumed as a library function.
+func Set(ctx context.Context, staticSpec *fintpb.Static, contextSpec *fintpb.Context) (*fintpb.SetArtifacts, error) {
+	platform, err := getPlatform()
+	if err != nil {
+		return nil, err
+	}
+
+	runner := &runner.SubprocessRunner{}
+	return runSteps(ctx, runner, staticSpec, contextSpec, platform)
+}
+
+// runSteps runs `gn gen` along with any post-processing steps, and returns a
+// SetArtifacts object containing metadata produced by GN and post-processing.
+func runSteps(
+	ctx context.Context,
+	runner subprocessRunner,
+	staticSpec *fintpb.Static,
+	contextSpec *fintpb.Context,
+	platform string,
+) (*fintpb.SetArtifacts, error) {
+	gnPath := filepath.Join(contextSpec.CheckoutDir, "prebuilt", "third_party", "gn", platform, "gn")
+	artifacts := &fintpb.SetArtifacts{}
+	genArgs, err := genArgs(staticSpec, contextSpec, platform)
+	if err != nil {
+		return nil, err
+	}
+	if contextSpec.ArtifactDir != "" {
+		artifacts.GnTracePath = filepath.Join(contextSpec.ArtifactDir, "gn_trace.json")
+	}
+	genStdout, err := runGen(ctx, runner, staticSpec, contextSpec, gnPath, artifacts.GnTracePath, genArgs)
+	if err != nil {
+		artifacts.FailureSummary = genStdout
+		return artifacts, err
+	}
+	if staticSpec.SkipIfUnaffected {
+		var changedFiles []string
+		for _, f := range contextSpec.ChangedFiles {
+			changedFiles = append(changedFiles, f.Path)
+		}
+		sb, err := shouldBuild(ctx, runner, contextSpec.BuildDir, contextSpec.CheckoutDir, gnPath, changedFiles)
+		if err != nil {
+			return artifacts, err
+		}
+		artifacts.SkipBuild = !sb
+	}
+	return artifacts, err
+}
+
+func runGen(
+	ctx context.Context,
+	runner subprocessRunner,
+	staticSpec *fintpb.Static,
+	contextSpec *fintpb.Context,
+	gnPath string,
+	gnTracePath string,
+	args []string,
+) (genStdout string, err error) {
+	genCmd := []string{
+		gnPath, "gen",
+		contextSpec.BuildDir,
+		"--check=system",
+		"--fail-on-unused-args",
+	}
+
+	if gnTracePath != "" {
+		genCmd = append(genCmd, fmt.Sprintf("--tracelog=%s", gnTracePath))
+	}
+	if staticSpec.GenerateCompdb {
+		genCmd = append(genCmd, "--export-compile-commands")
+	}
+	if staticSpec.GenerateIde {
+		genCmd = append(genCmd, "--ide=json")
+	}
+
+	genCmd = append(genCmd, fmt.Sprintf("--args=%s", strings.Join(args, " ")))
+
+	// When `gn gen` fails, it outputs a brief helpful error message to stdout.
+	var stdoutBuf bytes.Buffer
+	if err := runner.Run(ctx, genCmd, io.MultiWriter(&stdoutBuf, os.Stdout), os.Stderr); err != nil {
+		return stdoutBuf.String(), fmt.Errorf("error running gn gen: %w", err)
+	}
+	return stdoutBuf.String(), nil
+}
+
+func getPlatform() (string, error) {
+	os, ok := map[string]string{
+		"windows": "win",
+		"darwin":  "mac",
+		"linux":   "linux",
+	}[runtime.GOOS]
+	if !ok {
+		return "", fmt.Errorf("unsupported GOOS %q", runtime.GOOS)
+	}
+
+	arch, ok := map[string]string{
+		"amd64": "x64",
+		"arm64": "arm64",
+	}[runtime.GOARCH]
+	if !ok {
+		return "", fmt.Errorf("unsupported GOARCH %q", runtime.GOARCH)
+	}
+	return fmt.Sprintf("%s-%s", os, arch), nil
+}
+
+func genArgs(staticSpec *fintpb.Static, contextSpec *fintpb.Context, platform string) ([]string, error) {
+	// GN variables to set via args (mapping from variable name to value).
+	vars := make(map[string]interface{})
+	// GN list variables to which we want to append via args (mapping from
+	// variable name to list of values to append).
+	appends := make(map[string][]string)
+	// GN targets to import.
+	var imports []string
+
+	if staticSpec.TargetArch == fintpb.Static_ARCH_UNSPECIFIED {
+		return nil, fmt.Errorf("target_arch is unspecified or invalid")
+	}
+	vars["target_cpu"] = strings.ToLower(staticSpec.TargetArch.String())
+
+	if staticSpec.Optimize == fintpb.Static_OPTIMIZE_UNSPECIFIED {
+		return nil, fmt.Errorf("optimize is unspecified or invalid")
+	}
+	vars["is_debug"] = staticSpec.Optimize == fintpb.Static_DEBUG
+
+	if contextSpec.ClangToolchainDir != "" {
+		if staticSpec.UseGoma {
+			return nil, fmt.Errorf("goma is not supported for builds using a custom clang toolchain")
+		}
+		vars["clang_prefix"] = filepath.Join(contextSpec.ClangToolchainDir, "bin")
+	}
+	if contextSpec.GccToolchainDir != "" {
+		if staticSpec.UseGoma {
+			return nil, fmt.Errorf("goma is not supported for builds using a custom gcc toolchain")
+		}
+		vars["zircon_extra_args.gcc_tool_dir"] = filepath.Join(contextSpec.GccToolchainDir, "bin")
+	}
+	if contextSpec.RustToolchainDir != "" {
+		vars["rustc_prefix"] = filepath.Join(contextSpec.RustToolchainDir, "bin")
+	}
+
+	vars["use_goma"] = staticSpec.UseGoma
+	if staticSpec.UseGoma {
+		vars["goma_dir"] = filepath.Join(
+			contextSpec.CheckoutDir, "prebuilt", "third_party", "goma", platform,
+		)
+	}
+
+	if staticSpec.Product != "" {
+		basename := filepath.Base(staticSpec.Product)
+		vars["build_info_product"] = strings.Split(basename, ".")[0]
+		imports = append(imports, staticSpec.Product)
+	}
+
+	if staticSpec.Board != "" {
+		basename := filepath.Base(staticSpec.Board)
+		vars["build_info_board"] = strings.Split(basename, ".")[0]
+		imports = append(imports, staticSpec.Board)
+	}
+
+	if contextSpec.SdkId != "" {
+		vars["sdk_id"] = contextSpec.SdkId
+		vars["build_sdk_archives"] = true
+	}
+
+	if contextSpec.ReleaseVersion != "" {
+		vars["build_info_version"] = contextSpec.ReleaseVersion
+	}
+
+	if staticSpec.TestDurationsFile != "" {
+		testDurationsFile := staticSpec.TestDurationsFile
+		exists, err := osmisc.FileExists(filepath.Join(contextSpec.CheckoutDir, testDurationsFile))
+		if err != nil {
+			return nil, fmt.Errorf("failed to check if TestDurationsFile exists: %w", err)
+		}
+		if !exists {
+			testDurationsFile = staticSpec.DefaultTestDurationsFile
+		}
+		vars["test_durations_file"] = testDurationsFile
+	}
+
+	for varName, packages := range map[string][]string{
+		"base_package_labels":     staticSpec.BasePackages,
+		"cache_package_labels":    staticSpec.CachePackages,
+		"universe_package_labels": staticSpec.UniversePackages,
+	} {
+		if len(packages) == 0 {
+			continue
+		}
+		// If product is set, append to the corresponding list variable instead
+		// of overwriting it to avoid overwriting any packages set in the
+		// imported product file.
+		// TODO(olivernewman): Is it safe to always append whether or not
+		// product is set?
+		if staticSpec.Product == "" {
+			vars[varName] = packages
+		} else {
+			appends[varName] = packages
+		}
+	}
+
+	if len(staticSpec.Variants) != 0 {
+		vars["select_variant"] = staticSpec.Variants
+		if contains(staticSpec.Variants, "thinlto") {
+			vars["thinlto_cache_dir"] = filepath.Join(contextSpec.CacheDir, "thinlto")
+		}
+	}
+
+	var normalArgs []string
+	var importArgs []string
+	for _, arg := range staticSpec.GnArgs {
+		if strings.HasPrefix(arg, "import(") {
+			importArgs = append(importArgs, arg)
+		} else {
+			normalArgs = append(normalArgs, arg)
+		}
+	}
+
+	for k, v := range vars {
+		normalArgs = append(normalArgs, fmt.Sprintf("%s=%s", k, toGNValue(v)))
+	}
+	for k, v := range appends {
+		normalArgs = append(normalArgs, fmt.Sprintf("%s+=%s", k, toGNValue(v)))
+	}
+	sort.Strings(normalArgs)
+
+	for _, p := range imports {
+		importArgs = append(importArgs, fmt.Sprintf(`import("//%s")`, p))
+	}
+	sort.Strings(importArgs)
+
+	var finalArgs []string
+
+	// Ensure that imports come before args that set or modify variables, as
+	// otherwise the imported files might blindly redefine variables set or
+	// modified by other arguments.
+	finalArgs = append(finalArgs, importArgs...)
+	// Initialize `zircon_extra_args` before any variable-setting args, so that
+	// it's safe for subsequent args to do things like `zircon_extra_args.foo =
+	// "bar"` without worrying about initializing zircon_extra_args if it hasn't
+	// yet been defined. But do it after all imports in case one of the imported
+	// files sets `zircon_extra_args`.
+	finalArgs = append(finalArgs, "if (!defined(zircon_extra_args)) { zircon_extra_args = {} }")
+	finalArgs = append(finalArgs, normalArgs...)
+	return finalArgs, nil
+}
+
+// toGNValue converts a Go value to a string representation of the corresponding
+// GN value by inspecting the Go value's type. This makes the logic to set GN
+// args more readable and less error-prone, with no need for patterns like
+// fmt.Sprintf(`"%s"`) repeated everywhere.
+//
+// For example:
+// - toGNValue(true) => `true`
+// - toGNValue("foo") => `"foo"` (a string containing literal double-quotes)
+// - toGNValue([]string{"foo", "bar"}) => `["foo","bar"]`
+func toGNValue(x interface{}) string {
+	switch val := x.(type) {
+	case bool:
+		return fmt.Sprintf("%v", val)
+	case string:
+		// Apply double-quotes to strings, but not to GN scopes like
+		// {variant="asan-fuzzer" target_type=["fuzzed_executable"]}
+		if strings.HasPrefix(val, "{") && strings.HasSuffix(val, "}") {
+			return val
+		}
+		return fmt.Sprintf(`"%s"`, val)
+	case []string:
+		var values []string
+		for _, element := range val {
+			values = append(values, toGNValue(element))
+		}
+		return fmt.Sprintf("[%s]", strings.Join(values, ","))
+	default:
+		panic(fmt.Sprintf("unsupported arg value type %T", val))
+	}
+}
+
+func contains(items []string, target string) bool {
+	for _, item := range items {
+		if item == target {
+			return true
+		}
+	}
+	return false
+}
diff --git a/tools/integration/cmd/fint/set_test.go b/tools/integration/fint/set_test.go
similarity index 99%
rename from tools/integration/cmd/fint/set_test.go
rename to tools/integration/fint/set_test.go
index 781ad1a..2d0d5cd 100644
--- a/tools/integration/cmd/fint/set_test.go
+++ b/tools/integration/fint/set_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package fint
 
 import (
 	"context"
@@ -14,7 +14,7 @@
 	"testing"
 
 	"github.com/golang/protobuf/proto"
-	fintpb "go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/proto"
+	fintpb "go.fuchsia.dev/fuchsia/tools/integration/fint/proto"
 	"go.fuchsia.dev/fuchsia/tools/lib/osmisc"
 )
 
diff --git a/tools/integration/cmd/fint/should_build.go b/tools/integration/fint/should_build.go
similarity index 98%
rename from tools/integration/cmd/fint/should_build.go
rename to tools/integration/fint/should_build.go
index e5548f8..cf3fdc3 100644
--- a/tools/integration/cmd/fint/should_build.go
+++ b/tools/integration/fint/should_build.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package fint
 
 import (
 	"context"
@@ -11,7 +11,7 @@
 	"os"
 	"path/filepath"
 
-	"go.fuchsia.dev/fuchsia/tools/integration/cmd/fint/filetype"
+	"go.fuchsia.dev/fuchsia/tools/integration/fint/filetype"
 	"go.fuchsia.dev/fuchsia/tools/lib/jsonutil"
 	"go.fuchsia.dev/fuchsia/tools/lib/logger"
 )
diff --git a/tools/integration/cmd/fint/should_build_test.go b/tools/integration/fint/should_build_test.go
similarity index 99%
rename from tools/integration/cmd/fint/should_build_test.go
rename to tools/integration/fint/should_build_test.go
index be742a5..f9e7c40 100644
--- a/tools/integration/cmd/fint/should_build_test.go
+++ b/tools/integration/fint/should_build_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-package main
+package fint
 
 import (
 	"context"